From 28d1e89b35306888ef789ca9705df763270f2580 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 8 Nov 2026 05:28:13 -0600 Subject: [PATCH] fix(runner): let comment_on_task request follow-up so a queued requirement survives completion The comment API keeps an agent's comment on another agent's task as a note unless it sends resume: true, and a deferred wake without that intent is cancelled when the assignee completes the task. The native comment_on_task tool only sent { body }, so a lead's requirement change saved behind an active run was dropped once the worker finished Done. Add an optional resume flag, forward it to the comment API, and tell agents to set it for requirement changes. Co-Authored-By: Claude Opus 5.5 --- .../native-runtime/paperclip-runner-tool-authority.ts | 4 ++-- .../native-runtime/runner-bridge-tools.test.ts | 11 +++++++++++ .../services/native-runtime/runner-bridge-tools.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/services/native-runtime/paperclip-runner-tool-authority.ts b/server/src/services/native-runtime/paperclip-runner-tool-authority.ts index 3b57ae63768..2db9f54c878 100644 --- a/server/src/services/native-runtime/paperclip-runner-tool-authority.ts +++ b/server/src/services/native-runtime/paperclip-runner-tool-authority.ts @@ -291,11 +291,11 @@ const TASK_API_PATHS: Readonly> = { write_task_document: ["PUT", "/api/issues/{id}/documents/{key}"], }; -function taskBridgeApiRequest(tool: string, input: Record): RunnerApiCall { +export function taskBridgeApiRequest(tool: string, input: Record): RunnerApiCall { const [method, path] = TASK_API_PATHS[tool]!; const operation = runnerApiCatalog().find(operation => operation.method === method && operation.path === path); if (!operation) throw new Error("runner_task_api_operation_unavailable"); - const body = tool === "comment_on_task" ? { body: input.body } : tool === "write_task_document" + const body = tool === "comment_on_task" ? { body: input.body, ...(input.resume === true ? { resume: true } : {}) } : tool === "write_task_document" ? { title: input.title, format: "markdown", body: input.body, baseRevisionId: input.baseRevisionId } : undefined; return { operationId: operation.operationId, pathParams: { id: input.taskId, ...(input.key ? { key: input.key } : {}) }, ...(body ? { body } : {}) }; } diff --git a/server/src/services/native-runtime/runner-bridge-tools.test.ts b/server/src/services/native-runtime/runner-bridge-tools.test.ts index 11151ffe319..b46cf65e215 100644 --- a/server/src/services/native-runtime/runner-bridge-tools.test.ts +++ b/server/src/services/native-runtime/runner-bridge-tools.test.ts @@ -1,6 +1,7 @@ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { materializeAsset } from "./runtime-context.js"; +import { taskBridgeApiRequest } from "./paperclip-runner-tool-authority.js"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { afterEach, describe, expect, it } from "vitest"; @@ -93,7 +94,17 @@ describe("Runner workspace bridge", () => { expect(comment?.description).toContain("existing task's assignee a new or changed requirement"); expect(comment?.description).toContain("rather than creating a duplicate task or editing that task's workspace files"); expect(comment?.description).toContain("delivers it at the start of the assignee's next turn on that task"); + expect(comment?.description).toContain("set resume: true for such a requirement"); + expect(comment?.description).toContain("delivery still happens if the assignee completes the task first"); expect(comment?.description).toContain("not as already applied"); + expect((comment?.inputSchema as { properties?: Record }).properties?.resume).toEqual({ type: "boolean" }); + }); + it("forwards a comment's explicit resume intent to the comment API and omits it otherwise", () => { + const taskId = "8ba8b5a2-066a-4bb2-b591-e396942ecdf5"; + expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "Set the headline to Launch sooner.", resume: true }).body) + .toEqual({ body: "Set the headline to Launch sooner.", resume: true }); + expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "FYI only." }).body).toEqual({ body: "FYI only." }); + expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "FYI only.", resume: false }).body).toEqual({ body: "FYI only." }); }); it.skipIf(process.platform === "linux")("does not expose or execute commands without descendant containment", async () => { diff --git a/server/src/services/native-runtime/runner-bridge-tools.ts b/server/src/services/native-runtime/runner-bridge-tools.ts index 20a079fcf8e..f66327e559f 100644 --- a/server/src/services/native-runtime/runner-bridge-tools.ts +++ b/server/src/services/native-runtime/runner-bridge-tools.ts @@ -14,7 +14,7 @@ const specs = [ ["get_identity", "Read your agent identity, responsible person, permissions and currently available tools.", z.object({}).strict(), "read"], ["list_people", "List active company people and their IDs for human assignment; roles identify the owner.", z.object({ after: z.string().optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), "read"], ["get_task", "Read another task through normal task permissions.", z.object({ taskId: z.uuid() }).strict(), "read"], - ["comment_on_task", "Post an attributed comment on an authorized task, without reopening or interrupting it. Use it to give an existing task's assignee a new or changed requirement, rather than creating a duplicate task or editing that task's workspace files. If the assignee is running on that task now, the comment does not reach the run in progress: Paperclip saves it in the task's queue and delivers it at the start of the assignee's next turn on that task. Report it as saved for that next turn, not as already applied.", z.object({ taskId: z.uuid(), body: z.string().trim().min(1).max(20000) }).strict(), "mutation"], + ["comment_on_task", "Post an attributed comment on an authorized task, without reopening or interrupting it. Use it to give an existing task's assignee a new or changed requirement, rather than creating a duplicate task or editing that task's workspace files, and set resume: true for such a requirement. If the assignee is running on that task now, the comment does not reach the run in progress: Paperclip saves it in the task's queue and delivers it at the start of the assignee's next turn on that task. With resume: true that delivery still happens if the assignee completes the task first; without it, a comment on finished work is only a note. Report a queued requirement as saved for the next turn, not as already applied.", z.object({ taskId: z.uuid(), body: z.string().trim().min(1).max(20000), resume: z.boolean().optional() }).strict(), "mutation"], ["list_task_documents", "List documents on an authorized task.", z.object({ taskId: z.uuid() }).strict(), "read"], ["read_task_document", "Read a document on an authorized task.", z.object({ taskId: z.uuid(), key: documentKey }).strict(), "read"], ["write_task_document", "Write a document on an authorized task using its current revision. Permissions and document locks apply.", z.object({ taskId: z.uuid(), key: documentKey, title: z.string().min(1).max(200), body: z.string().max(20000), baseRevisionId: z.uuid().nullable() }).strict(), "mutation"],