feat: show owners a live worker status snapshot
While a worker turn is running, the assistant prompt for the working proposal's owner includes a compact status card (elapsed time, last tool activity category with age, per-category counts) built from the ACP session/update stream the runtime already receives. Other users still see only the desensitized busy state, and raw update content never enters the assistant session.
This commit is contained in:
@@ -722,6 +722,53 @@ test("image attachments degrade to a text note when the agent has no image capab
|
||||
}
|
||||
});
|
||||
|
||||
test("the owner sees a live worker activity card while others get only a sanitized busy state", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
await harness.manager.prompt(request("create proposal: toolhang", "user-a"));
|
||||
const proposal = harness.proposals.list()[0]!;
|
||||
await harness.manager.prompt(request("confirm", "user-a"));
|
||||
const internals = harness.manager as unknown as {
|
||||
active?: { activity: { turnStartedAt?: number; lastActivityAt?: number; lastCategory?: string; counts: Record<string, number> } };
|
||||
};
|
||||
await waitFor(() => (internals.active?.activity.counts.read || 0) === 2);
|
||||
|
||||
const activity = internals.active!.activity;
|
||||
assert.equal(activity.lastCategory, "execute");
|
||||
assert.equal(activity.counts.read, 2);
|
||||
assert.equal(activity.counts.execute, 1);
|
||||
assert.ok(activity.turnStartedAt);
|
||||
assert.ok(activity.lastActivityAt);
|
||||
|
||||
await harness.manager.prompt(request("进度如何", "user-a"));
|
||||
await harness.manager.prompt(request("进度如何", "user-b"));
|
||||
const progressPrompts = readLog(harness.logFile).filter((entry) => entry.method === "session/prompt"
|
||||
&& entry.text?.startsWith("[User message]\n进度如何"));
|
||||
assert.equal(progressPrompts.length, 2);
|
||||
const ownerPrompt = progressPrompts[0]!.text!;
|
||||
assert.match(ownerPrompt, /title="Test proposal" running=\d{2}:\d{2}/);
|
||||
assert.match(ownerPrompt, /lastActivity=execute \(\d+s ago\)/);
|
||||
assert.match(ownerPrompt, /counts: read×2 execute×1/);
|
||||
assert.match(ownerPrompt, /real-time snapshot of the worker/);
|
||||
const otherPrompt = progressPrompts[1]!.text!;
|
||||
assert.match(otherPrompt, /details hidden/);
|
||||
assert.doesNotMatch(otherPrompt, /lastActivity=/);
|
||||
assert.doesNotMatch(otherPrompt, /Test proposal/);
|
||||
|
||||
// Once the worker is no longer working, the card disappears.
|
||||
assert.equal(await harness.manager.stop("webhook", "chat-1", "user-a"), true);
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "pending");
|
||||
await harness.manager.prompt(request("hello", "user-a"));
|
||||
const idlePrompts = readLog(harness.logFile).filter((entry) => entry.method === "session/prompt"
|
||||
&& entry.text?.startsWith("[User message]\nhello"));
|
||||
assert.equal(idlePrompts.length, 1);
|
||||
assert.match(idlePrompts[0]!.text!, /no active worker/);
|
||||
assert.doesNotMatch(idlePrompts[0]!.text!, /lastActivity=/);
|
||||
} finally {
|
||||
await closeHarness(harness);
|
||||
}
|
||||
});
|
||||
|
||||
test("an assistant session idle beyond the reset threshold is dropped when the owner has no unfinished proposal", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
|
||||
Vendored
+21
@@ -107,6 +107,27 @@ const app = acp.agent({ name: "fake-acp-agent" })
|
||||
}
|
||||
if (text.startsWith("Execute this confirmed proposal")) {
|
||||
const goal = ((text.split("Goal: ")[1] || "").split("\n")[0] || "").trim();
|
||||
if (goal.includes("toolhang")) {
|
||||
await client.notify(acp.methods.client.session.update, {
|
||||
sessionId: params.sessionId,
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "read-1", title: "Read package.json", kind: "read", status: "in_progress" }
|
||||
});
|
||||
await client.notify(acp.methods.client.session.update, {
|
||||
sessionId: params.sessionId,
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "read-2", title: "Read README.md", kind: "read", status: "in_progress" }
|
||||
});
|
||||
await client.notify(acp.methods.client.session.update, {
|
||||
sessionId: params.sessionId,
|
||||
update: { sessionUpdate: "tool_call", toolCallId: "exec-1", title: "bash npm test", kind: "execute", status: "in_progress" }
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
const done = () => resolve(undefined);
|
||||
pending.set(params.sessionId, done);
|
||||
signal.addEventListener("abort", done, { once: true });
|
||||
});
|
||||
pending.delete(params.sessionId);
|
||||
return { stopReason: "cancelled" };
|
||||
}
|
||||
if (goal.includes("hang")) {
|
||||
await new Promise((resolve) => {
|
||||
const done = () => resolve(undefined);
|
||||
|
||||
Reference in New Issue
Block a user