From f10658fe3a64386018116c02006fd68f5bac080b Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 17 Jul 2026 16:05:48 +0000 Subject: [PATCH 1/2] docs(coding-agent): clarified async job lifecycle contract - Documented settled snapshot delivery consumption and process-local retention. - Clarified completion semantics in task receipts and hub guidance. - Added model-facing contract regression coverage. Fixes #5869 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/prompts/tools/hub.md | 6 ++++-- .../src/prompts/tools/task-async-contract.md | 1 + packages/coding-agent/src/prompts/tools/task.md | 11 +++++++++-- packages/coding-agent/src/task/index.ts | 7 +++++-- .../coding-agent/test/job-tool-agent-roster.test.ts | 8 ++++++++ packages/coding-agent/test/task/task-spawn.test.ts | 10 +++++++++- 7 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 packages/coding-agent/src/prompts/tools/task-async-contract.md diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 493bd5332..760d70649 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -26,6 +26,7 @@ ### Fixed +- Clarified async task and hub guidance: inspecting a settled job consumes its automatic delivery, job IDs expire from process memory after roughly five minutes, and completion does not verify claimed artifacts ([#5869](https://github.com/can1357/oh-my-pi/issues/5869)). - Fixed loading issues for linked legacy extensions importing `DefaultPackageManager` or `linkedom`. - Fixed the advisor retrying terminal, non-retriable provider failures (e.g., blocked prompts), ensuring they fail immediately while transient failures still retry. - Fixed an issue where reassigning the `plan` role model mid-planning did not take effect until the next plan-mode entry; it now applies at the next turn boundary. diff --git a/packages/coding-agent/src/prompts/tools/hub.md b/packages/coding-agent/src/prompts/tools/hub.md index 73be57b32..0d48b70e9 100644 --- a/packages/coding-agent/src/prompts/tools/hub.md +++ b/packages/coding-agent/src/prompts/tools/hub.md @@ -3,7 +3,7 @@ Use `op: "list"` to discover peers. Address peers by exact roster ID — NEVER i # Messaging & Jobs -Background jobs deliver their results automatically the moment they finish. You NEVER need to poll for output — intervene only to block, kill, or inspect. +Background jobs auto-deliver when they finish. You NEVER need to poll; if `jobs`/`wait` observes a settled job first, that snapshot is the delivery and suppresses duplicate `async-result`. - **`send`** (with `to`): fire-and-forget, NEVER blocks. Delivery receipts (`delivered`/`failed`) immediate; `failed` → peer gone, don't retry. Sending wakes `idle`/`parked` peers. Answering: lead with answer, NEVER quote, set `replyTo`. @@ -12,7 +12,9 @@ Background jobs deliver their results automatically the moment they finish. You - Bare `wait` watches every running job AND incoming messages. NEVER pass an array of every running ID; `ids` narrows to specific jobs, `from` to one peer (or use `await: true` on send). - **`inbox`**: drain queued messages without blocking. - **`cancel`**: kill background jobs by `ids` when they have hung, stalled, or are no longer needed. Returns immediately. -- **`jobs`**: status snapshot of every job without waiting. Also names running subagents with no job entry — coordinate with those via `send`. +- **`jobs`**: status snapshot of every job without waiting. A settled row consumes auto-delivery. Also names running subagents with no job entry — coordinate with those via `send`. +- Job rows are process-local and expire roughly five minutes after settlement. Afterward, use the agent ID with `send`, `agent://`, or `history://`. +- `completed` means successful yield/job exit, not artifact acceptance. Verify claimed changes. - NEVER use shell tools, grep, or read other sessions' files to figure out what a peer is doing. Message them directly. - NEVER use hub messaging for something a tool can answer (e.g., grepping codebase, running a build). diff --git a/packages/coding-agent/src/prompts/tools/task-async-contract.md b/packages/coding-agent/src/prompts/tools/task-async-contract.md new file mode 100644 index 000000000..95d6efeae --- /dev/null +++ b/packages/coding-agent/src/prompts/tools/task-async-contract.md @@ -0,0 +1 @@ +No polling is needed. Inspecting a settled job with `hub jobs` or `hub wait` makes that snapshot its delivery, so no duplicate `async-result` follows. Job IDs live in process memory for roughly five minutes after settlement; afterward, use the agent ID with `hub send`, `agent://`, or `history://`. `completed` means the subagent yielded successfully, not that claimed artifacts were verified. diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index d41cf71e3..08fc46285 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -1,7 +1,14 @@ {{#if asyncEnabled}}{{#if batchEnabled}}Delegate work to background subagents by passing multiple items in a single `tasks[]` batch. -Execution does not block — you receive IDs immediately; results deliver when subagents finish.{{else}}Delegate work to ONE background subagent per call. -Execution does not block — you receive an ID immediately; the result delivers when the subagent finishes.{{/if}}{{#if hasBlockingAgents}} +Execution does not block — you receive IDs immediately.{{else}}Delegate work to ONE background subagent per call. +Execution does not block — you receive an ID immediately.{{/if}}{{#if hasBlockingAgents}} Agents marked BLOCKING run inline — results return in this call; non-blocking items in the same batch still spawn as background jobs.{{/if}}{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch. Execution blocks until all work finishes.{{else}}Run ONE subagent synchronously. Execution blocks until work finishes.{{/if}}{{/if}} +{{#if asyncEnabled}} + +# Async Job Contract +- Results auto-deliver. A settled `hub jobs`/`hub wait` snapshot is the delivery; no duplicate `async-result` follows. +- Job IDs are process-local and expire roughly five minutes after settlement. Afterward, use the agent ID with `hub send`, `agent://`, or `history://`. +- `completed` means successful yield/job exit, not artifact acceptance. Verify claimed changes. +{{/if}} # Task Design - **Agent typing:** Pick each item's `agent` type. Read-only research MUST use `agent: "scout"` (faster model). Use default worker only when no specialist fits. diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index 2e787c297..fadf3ff97 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -26,6 +26,7 @@ import type { Theme } from "../modes/theme/theme"; import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" }; import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" }; import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" }; +import taskAsyncContractTemplate from "../prompts/tools/task-async-contract.md" with { type: "text" }; import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" }; import { truncateForPrompt } from "../tools/approval"; import { isIrcEnabled } from "../tools/hub"; @@ -798,14 +799,16 @@ export class TaskTool implements AgentTool 0 ? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.` : ""; - const coordinationHint = + const coordinationHint = [ started.length === 1 ? ircEnabled ? `DM \`${started[0].agentId}\` via \`hub\` send to coordinate while it runs; use \`hub\` only to inspect (\`jobs\`), wait, or cancel a stuck task.` : `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task.` : ircEnabled ? `DM these ids via \`hub\` send to coordinate while they run; use \`hub\` only to inspect (\`jobs\`), wait, or cancel a stuck task.` - : `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task by id.`; + : `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task by id.`, + taskAsyncContractTemplate.trim(), + ].join("\n"); if (syncSpawns.length === 0) { if (spawns.length === 1) { diff --git a/packages/coding-agent/test/job-tool-agent-roster.test.ts b/packages/coding-agent/test/job-tool-agent-roster.test.ts index ed34a437b..99d6c04f1 100644 --- a/packages/coding-agent/test/job-tool-agent-roster.test.ts +++ b/packages/coding-agent/test/job-tool-agent-roster.test.ts @@ -56,6 +56,14 @@ afterEach(async () => { }); describe("hub jobs snapshot", () => { + test("description explains settled delivery, expiry, and completion semantics", () => { + const tool = new HubTool(createToolSession({ manager: createManager(), agentId: "Main" })); + + expect(tool.description).toContain("that snapshot is the delivery and suppresses duplicate `async-result`"); + expect(tool.description).toContain("expire roughly five minutes after settlement"); + expect(tool.description).toContain("`completed` means successful yield/job exit, not artifact acceptance"); + }); + test("empty jobs snapshot reports 'no jobs' instead of empty output", async () => { const tool = new HubTool(createToolSession({ manager: createManager(), agentId: "Main" })); diff --git a/packages/coding-agent/test/task/task-spawn.test.ts b/packages/coding-agent/test/task/task-spawn.test.ts index 782ff83d3..0d04d6f54 100644 --- a/packages/coding-agent/test/task/task-spawn.test.ts +++ b/packages/coding-agent/test/task/task-spawn.test.ts @@ -105,7 +105,7 @@ describe("task spawn routing", () => { AgentRegistry.resetGlobalForTests(); }); - it("returns immediately on spawn and delivers the follow-up hint when the job completes", async () => { + it("returns immediately with the async job contract and delivers the follow-up hint", async () => { vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents: [taskAgent], projectAgentsDir: null, @@ -118,6 +118,8 @@ describe("task spawn routing", () => { const manager = createManager(); const tool = await TaskTool.create(createSession({ manager })); + expect(tool.description).toContain("A settled `hub jobs`/`hub wait` snapshot is the delivery"); + expect(tool.description).toContain("`completed` means successful yield/job exit, not artifact acceptance"); const result = await tool.execute("tc-spawn", { agent: "task", @@ -131,6 +133,12 @@ describe("task spawn routing", () => { const jobId = result.details?.async?.jobId; expect(jobId).toBeTruthy(); expect(text).toContain(`job \`${jobId}\``); + expect(text).toContain("settled job with `hub jobs` or `hub wait` makes that snapshot its delivery"); + expect(text).toContain("Job IDs live in process memory for roughly five minutes after settlement"); + expect(text).toContain("use the agent ID with `hub send`, `agent://`, or `history://`"); + expect(text).toContain( + "`completed` means the subagent yielded successfully, not that claimed artifacts were verified", + ); const job = manager.getJob(jobId!); expect(job?.status).toBe("running"); expect(job?.resultText).toBeUndefined(); From 73e98b494eca67b222762aa6336820ea07466482 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 17 Jul 2026 16:10:31 +0000 Subject: [PATCH 2/2] test(coding-agent): removed brittle prompt prose assertions Kept lifecycle behavior covered without pinning model-facing wording. Fixes #5869 --- .../coding-agent/test/job-tool-agent-roster.test.ts | 8 -------- packages/coding-agent/test/task/task-spawn.test.ts | 10 +--------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/coding-agent/test/job-tool-agent-roster.test.ts b/packages/coding-agent/test/job-tool-agent-roster.test.ts index 99d6c04f1..ed34a437b 100644 --- a/packages/coding-agent/test/job-tool-agent-roster.test.ts +++ b/packages/coding-agent/test/job-tool-agent-roster.test.ts @@ -56,14 +56,6 @@ afterEach(async () => { }); describe("hub jobs snapshot", () => { - test("description explains settled delivery, expiry, and completion semantics", () => { - const tool = new HubTool(createToolSession({ manager: createManager(), agentId: "Main" })); - - expect(tool.description).toContain("that snapshot is the delivery and suppresses duplicate `async-result`"); - expect(tool.description).toContain("expire roughly five minutes after settlement"); - expect(tool.description).toContain("`completed` means successful yield/job exit, not artifact acceptance"); - }); - test("empty jobs snapshot reports 'no jobs' instead of empty output", async () => { const tool = new HubTool(createToolSession({ manager: createManager(), agentId: "Main" })); diff --git a/packages/coding-agent/test/task/task-spawn.test.ts b/packages/coding-agent/test/task/task-spawn.test.ts index 0d04d6f54..782ff83d3 100644 --- a/packages/coding-agent/test/task/task-spawn.test.ts +++ b/packages/coding-agent/test/task/task-spawn.test.ts @@ -105,7 +105,7 @@ describe("task spawn routing", () => { AgentRegistry.resetGlobalForTests(); }); - it("returns immediately with the async job contract and delivers the follow-up hint", async () => { + it("returns immediately on spawn and delivers the follow-up hint when the job completes", async () => { vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents: [taskAgent], projectAgentsDir: null, @@ -118,8 +118,6 @@ describe("task spawn routing", () => { const manager = createManager(); const tool = await TaskTool.create(createSession({ manager })); - expect(tool.description).toContain("A settled `hub jobs`/`hub wait` snapshot is the delivery"); - expect(tool.description).toContain("`completed` means successful yield/job exit, not artifact acceptance"); const result = await tool.execute("tc-spawn", { agent: "task", @@ -133,12 +131,6 @@ describe("task spawn routing", () => { const jobId = result.details?.async?.jobId; expect(jobId).toBeTruthy(); expect(text).toContain(`job \`${jobId}\``); - expect(text).toContain("settled job with `hub jobs` or `hub wait` makes that snapshot its delivery"); - expect(text).toContain("Job IDs live in process memory for roughly five minutes after settlement"); - expect(text).toContain("use the agent ID with `hub send`, `agent://`, or `history://`"); - expect(text).toContain( - "`completed` means the subagent yielded successfully, not that claimed artifacts were verified", - ); const job = manager.getJob(jobId!); expect(job?.status).toBe("running"); expect(job?.resultText).toBeUndefined();