From c3f4d5ee601ad6f3753daf72b0deac2ed171a286 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 04:26:55 -0700 Subject: [PATCH] fix(codex): deliver task-suggestion tools to Codex-harness runs with calibrated guidance (#121568) - forward taskSuggestionDeliveryMode through the Codex app-server dynamic tool build (same sibling-harness omission class previously fixed for clientCaps); spawn_task/dismiss_task silently never existed for GPT-backed sessions - regression test asserting the forward, mirroring the existing clientCaps case - spawn_task/dismiss_task descriptions rewritten with usage calibration: when to flag, when not to, scope-vs-flagging, prose-is-lost, operator-phrasing bridge, prompt self-containment, git-checkout cwd, registry ephemerality; param descriptions state where each field renders Release-note context: GPT-backed sessions can now create suggested-task cards (the tools were silently absent on the Codex harness), and the tools carry much clearer model-facing guidance. --- .../src/app-server/dynamic-tool-build.test.ts | 19 ++++++++++++++ .../src/app-server/dynamic-tool-build.ts | 3 +++ src/agents/tools/task-suggestion-tools.ts | 26 ++++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/extensions/codex/src/app-server/dynamic-tool-build.test.ts b/extensions/codex/src/app-server/dynamic-tool-build.test.ts index 46a720205f0a..609f1bfb767c 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.test.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.test.ts @@ -442,6 +442,25 @@ describe("Codex app-server dynamic tool build", () => { }); }); + it("forwards the task-suggestion delivery mode", async () => { + // Regression: spawn_task/dismiss_task silently never existed on the Codex + // app-server path because this harness dropped params.taskSuggestionDeliveryMode. + const workspaceDir = path.join(tempDir, "workspace"); + const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); + params.disableTools = false; + params.runtimePlan = createCodexRuntimePlanFixture(); + params.taskSuggestionDeliveryMode = "gateway"; + let receivedOptions: unknown; + setOpenClawCodingToolsFactoryForTests((options) => { + receivedOptions = options; + return [createRuntimeDynamicTool("message")]; + }); + + await buildDynamicToolsForTest(params, workspaceDir); + + expect(receivedOptions).toMatchObject({ taskSuggestionDeliveryMode: "gateway" }); + }); + it("preserves the host-provided OpenClaw tool through the Codex allowlist", async () => { const workspaceDir = path.join(tempDir, "workspace"); const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); diff --git a/extensions/codex/src/app-server/dynamic-tool-build.ts b/extensions/codex/src/app-server/dynamic-tool-build.ts index 26fdd77af5bc..ce784a5fe108 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.ts @@ -328,6 +328,9 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) { requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey), sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, + // Same sibling-harness rule as clientCaps above: without this forward, + // spawn_task/dismiss_task silently never exist for Codex-harness runs. + taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode, disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool, forceMessageTool: shouldForceMessageTool(messagePolicyParams), enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true, diff --git a/src/agents/tools/task-suggestion-tools.ts b/src/agents/tools/task-suggestion-tools.ts index 571e13be6a70..b2b496e7a619 100644 --- a/src/agents/tools/task-suggestion-tools.ts +++ b/src/agents/tools/task-suggestion-tools.ts @@ -17,17 +17,20 @@ const SpawnTaskToolSchema = Type.Object( title: Type.String({ minLength: 1, maxLength: 60, - description: "Imperative task title under 60 characters.", + description: + "Imperative task title under 60 characters (start with a verb); shown as the card title and the started session's name.", }), prompt: Type.String({ minLength: 1, maxLength: 32_768, - description: "Self-contained task prompt with relevant file paths and context.", + description: + "Self-contained task prompt with file paths and enough context to act without this conversation.", }), tldr: Type.String({ minLength: 1, maxLength: 1_024, - description: "One or two plain-language sentences explaining the value; no code or paths.", + description: + "One or two plain-language sentences shown on the card explaining the value; no code or paths.", }), cwd: Type.Optional( Type.String({ @@ -74,8 +77,14 @@ export function createTaskSuggestionTools(params: { name: "spawn_task", displaySummary: SPAWN_TASK_TOOL_DISPLAY_SUMMARY, description: [ - "Suggest confirmed valuable out-of-scope follow-up: dead code, stale docs, missing coverage, verified TODO, security issue.", - "Operator suggestion only; does not start work. cwd must be an absolute path inside a git checkout.", + "Flag an out-of-scope issue as a separate follow-up task instead of ignoring it, fixing it inline, or only mentioning it in your reply — a follow-up described in prose is lost; recording it here is what surfaces it to the operator.", + "This is the tool behind requests like 'flag it as a follow-up', 'note that for later', or 'make a task for that'; whenever you would write 'Follow-up:' in a reply, call this instead.", + "Use this whenever work you were not asked to do surfaces along the way: dead code, stale docs, missing coverage, a confirmed TODO, or a security issue spotted in passing.", + "Requests to stay scoped or skip cleanup apply to doing the work, not to flagging it: this only records a suggestion card in the operator's UI; nothing runs unless they accept it, and your current turn continues uninterrupted.", + "Do not flag vague code-smell observations or low-confidence hunches.", + "The prompt must stand alone: the started task sees only that text, never this conversation.", + "cwd must be an absolute path inside a git checkout.", + "Suggestions are ephemeral; ids do not survive a gateway restart.", ].join(" "), parameters: SpawnTaskToolSchema, outputSchema: SpawnTaskOutputSchema, @@ -110,8 +119,11 @@ export function createTaskSuggestionTools(params: { label: "Dismiss Task", name: "dismiss_task", displaySummary: DISMISS_TASK_TOOL_DISPLAY_SUMMARY, - description: - "Withdraw stale/irrelevant pending spawn_task. Accepted suggestion cannot withdraw.", + description: [ + "Withdraw a pending suggestion card you created with spawn_task when it is now stale, superseded, or already handled in this session.", + "To replace a card, call spawn_task with the better suggestion first, then dismiss the old task_id.", + "Only cards the operator has not acted on can be withdrawn; accepted ones cannot.", + ].join(" "), parameters: DismissTaskToolSchema, execute: async (_toolCallId, args) => { const input = args as Record;