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:
zenord
2026-08-19 16:39:42 +08:00
parent bd96595ba8
commit 4f7b4a9115
5 changed files with 112 additions and 6 deletions
+47
View File
@@ -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 {
+21
View File
@@ -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);