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.
This commit is contained in:
Peter Steinberger
2026-08-10 04:26:55 -07:00
committed by GitHub
parent 572fab0b5b
commit c3f4d5ee60
3 changed files with 41 additions and 7 deletions
@@ -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);
@@ -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,
+19 -7
View File
@@ -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<string, unknown>;