From eb3c2bc7f0de992b6895120549831ffa6eefb76b Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:23 +0800 Subject: [PATCH 1/6] feat: persist timeoutMs and companionVersion on codex, grok, and fusion worker records --- plugins/codex/scripts/codex-companion.mjs | 28 ++++++++++++++++++--- plugins/codex/scripts/lib/state.mjs | 10 ++++++++ plugins/fusion/scripts/lib/worker-state.mjs | 23 +++++++++++++++++ plugins/grok/scripts/grok-companion.mjs | 25 ++++++++++++++++++ plugins/grok/scripts/lib/state.mjs | 1 + tests/codex-companion.test.mjs | 3 +++ tests/codex-state.test.mjs | 2 ++ tests/fusion-worker-state.test.mjs | 10 ++++++++ tests/state.test.mjs | 4 +++ 9 files changed, 103 insertions(+), 3 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index bb04318..d433084 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -64,6 +64,7 @@ const SELF_PATH = fileURLToPath(import.meta.url); const ROOT_DIR = path.resolve(path.dirname(SELF_PATH), ".."); const ADVERSARIAL_REVIEW_PROMPT_FILE = path.join(ROOT_DIR, "prompts", "adversarial-review.md"); const ADVERSARIAL_REVIEW_SCHEMA_FILE = path.join(ROOT_DIR, "schemas", "adversarial-review-verdict.schema.json"); +const COMPANION_MANIFEST_FILE = path.join(ROOT_DIR, ".claude-plugin", "plugin.json"); const FOREGROUND_TIMEOUT_ENV = "CODEX_COMPANION_TIMEOUT_MS"; const BACKGROUND_TIMEOUT_ENV = "CODEX_COMPANION_BACKGROUND_TIMEOUT_MS"; const BACKGROUND_DELIVERY_ENV = "CODEX_COMPANION_BACKGROUND_DELIVERY"; @@ -101,6 +102,8 @@ const SEMANTIC_FAILURE_KINDS = new Set(["intent_override", "scope_rewrite", "wro const SOL_FOREGROUND_WRITE_WARNING = "warning: sol p90 wall clock exceeds the 600s foreground cap. Split the brief or name gpt-5.6-terra."; const IMPLICIT_SUBDIRECTORY_CWD_WARNING = (cwd, workspaceRoot) => `Codex cwd ${cwd} sits below repository top level ${workspaceRoot} without an explicit --cwd; the sandbox roots at this subdirectory. Pass --cwd to pin the intended workspace.`; let activeCommandArgv = null; +let cachedCompanionVersion = null; +let companionVersionRead = false; class CompanionError extends Error { constructor(message, failureKind = "error") { @@ -292,6 +295,18 @@ function resolveExecutionTimeout(background, env = process.env) { return positiveEnvMs(background ? BACKGROUND_TIMEOUT_ENV : FOREGROUND_TIMEOUT_ENV, background ? DEFAULT_BACKGROUND_TIMEOUT_MS : DEFAULT_FOREGROUND_TIMEOUT_MS, env); } +function companionVersion() { + if (companionVersionRead) { + return cachedCompanionVersion; + } + companionVersionRead = true; + try { + const manifest = JSON.parse(fs.readFileSync(COMPANION_MANIFEST_FILE, "utf8")); + cachedCompanionVersion = typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : null; + } catch {} + return cachedCompanionVersion; +} + function backgroundDelivery(env = process.env) { return env[BACKGROUND_DELIVERY_ENV] === "managed" ? "managed" : "manual"; } @@ -1209,7 +1224,7 @@ function inheritedRouting(record, options, defaultServiceTier) { return { effort, inherited, model, serviceTier }; } -function createReservedJob({ background, brief, codexVersion, cwd, dataDir, explicitCwd, jobClass, mode, request }) { +function createReservedJob({ background, brief, codexVersion, companionVersion, cwd, dataDir, explicitCwd, jobClass, mode, request, timeoutMs }) { brief = boundedPrompt(brief); const ownerIdentity = background ? null : currentProcessIdentity(); const reserveWorkspace = (reservedRequest) => withWorkspaceLock(dataDir, cwd, () => { @@ -1229,6 +1244,7 @@ function createReservedJob({ background, brief, codexVersion, cwd, dataDir, expl briefFile, claudeSessionId: currentClaudeSessionId(), codexVersion, + companionVersion, cwd, delivery: background ? backgroundDelivery() : "foreground", eventsFile: jobEventsPath(dataDir, cwd, id), @@ -1241,6 +1257,7 @@ function createReservedJob({ background, brief, codexVersion, cwd, dataDir, expl pidIdentity: ownerIdentity, pidOwnsProcessGroup: false, request: explicitCwd ? { ...reservedRequest, explicitCwd: true } : reservedRequest, + timeoutMs, workspaceRoot: canonicalWorkspaceRoot(cwd) }); const file = jobFilePath(dataDir, cwd, id); @@ -1384,6 +1401,7 @@ async function executeRecord(found) { heartbeatAt: nowIso(), phase: record.background ? "codex-spawning" : "executing", serviceTier, + timeoutMs, pid: process.pid, pidIdentity: ownerIdentity, pidOwnsProcessGroup: record.background && process.platform !== "win32", @@ -1753,12 +1771,14 @@ async function handleTask(rawArgv, transport = {}) { background: Boolean(options.background), brief: prompt, codexVersion: probe.version, + companionVersion: companionVersion(), cwd, dataDir, explicitCwd: options.cwd != null, jobClass: "task", mode: write ? "write" : "consult", - request + request, + timeoutMs: resolveExecutionTimeout(Boolean(options.background)) }); await dispatchJob(found, Boolean(options.json)); } @@ -1845,12 +1865,14 @@ async function handleReview(rawArgv, adversarial = false, transport = {}) { background: Boolean(options.background), brief, codexVersion: probe.version, + companionVersion: companionVersion(), cwd, dataDir, explicitCwd: options.cwd != null, jobClass: adversarial ? "adversarial-review" : "review", mode: "consult", - request + request, + timeoutMs: resolveExecutionTimeout(Boolean(options.background)) }); await dispatchJob(found, Boolean(options.json)); } diff --git a/plugins/codex/scripts/lib/state.mjs b/plugins/codex/scripts/lib/state.mjs index b8da340..ca1c519 100644 --- a/plugins/codex/scripts/lib/state.mjs +++ b/plugins/codex/scripts/lib/state.mjs @@ -444,6 +444,12 @@ function assertJobRecord(record) { if (record.acceptanceRecordedAt != null && (typeof record.acceptanceRecordedAt !== "string" || !Number.isFinite(Date.parse(record.acceptanceRecordedAt)))) { throw new TypeError("Job acceptance recorded timestamp is invalid."); } + if (record.timeoutMs != null && (!Number.isSafeInteger(record.timeoutMs) || record.timeoutMs <= 0)) { + throw new TypeError("Job timeout is invalid."); + } + if (record.companionVersion != null && (typeof record.companionVersion !== "string" || !record.companionVersion.trim())) { + throw new TypeError("Job companion version is invalid."); + } } function ownerSessionId(record) { @@ -467,6 +473,8 @@ function normalizeRecord(record) { appliedServiceTier: record.appliedServiceTier ?? null, resolvedModel: record.resolvedModel ?? null, resolvedEffort: record.resolvedEffort ?? null, + timeoutMs: record.timeoutMs ?? null, + companionVersion: record.companionVersion ?? null, sessionId, claudeSessionId: sessionId }; @@ -729,6 +737,8 @@ export function createJobRecord(fields) { failureKind: fields.failureKind ?? null, cancelRequestedAt: fields.cancelRequestedAt ?? null, codexVersion: fields.codexVersion ?? null, + timeoutMs: fields.timeoutMs ?? null, + companionVersion: fields.companionVersion ?? null, request: fields.request ?? null }; assertJobRecord(record); diff --git a/plugins/fusion/scripts/lib/worker-state.mjs b/plugins/fusion/scripts/lib/worker-state.mjs index 95b46c3..9781eb9 100644 --- a/plugins/fusion/scripts/lib/worker-state.mjs +++ b/plugins/fusion/scripts/lib/worker-state.mjs @@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; const DATA_DIR_ENV = "FUSION_DATA_DIR"; const WORKER_STATE_ENV = "FUSION_WORKER_STATE_DIR"; @@ -50,6 +51,27 @@ const AGENT_TYPES = new Map([ ["fusion:job-collector", "fusion:job-collector"], ["job-collector", "fusion:job-collector"] ]); +let cachedFusionCompanionVersion; + +function resolvePluginRoot(env = process.env) { + if (env.CLAUDE_PLUGIN_ROOT && env.CLAUDE_PLUGIN_ROOT.trim()) { + return path.resolve(env.CLAUDE_PLUGIN_ROOT.trim()); + } + return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +} + +function resolveFusionCompanionVersion(env = process.env) { + if (cachedFusionCompanionVersion !== undefined) { + return cachedFusionCompanionVersion; + } + try { + const manifest = JSON.parse(fs.readFileSync(path.join(resolvePluginRoot(env), ".claude-plugin", "plugin.json"), "utf8")); + cachedFusionCompanionVersion = typeof manifest?.version === "string" ? manifest.version : null; + } catch { + cachedFusionCompanionVersion = null; + } + return cachedFusionCompanionVersion; +} export function resolveLockTimeoutMs(env = process.env) { const raw = env[LOCK_TIMEOUT_ENV]; @@ -290,6 +312,7 @@ export function createWorkerRecord(record, env = process.env) { const now = new Date().toISOString(); const value = { schemaVersion: 1, + companionVersion: resolveFusionCompanionVersion(env), taskId: record.taskId, sessionId: record.sessionId, dispatchToolUseId: record.dispatchToolUseId ?? null, diff --git a/plugins/grok/scripts/grok-companion.mjs b/plugins/grok/scripts/grok-companion.mjs index 5f224f5..14a6ea3 100644 --- a/plugins/grok/scripts/grok-companion.mjs +++ b/plugins/grok/scripts/grok-companion.mjs @@ -73,6 +73,28 @@ const SELF_PATH = fileURLToPath(import.meta.url); const ROOT_DIR = path.resolve(path.dirname(SELF_PATH), ".."); const REVIEW_PROMPT_FILE = path.join(ROOT_DIR, "prompts", "review.md"); const STOP_GATE_PROMPT_FILE = path.join(ROOT_DIR, "prompts", "stop-gate.md"); +let cachedGrokCompanionVersion; + +function resolvePluginRoot(env = process.env) { + if (env.CLAUDE_PLUGIN_ROOT && env.CLAUDE_PLUGIN_ROOT.trim()) { + return path.resolve(env.CLAUDE_PLUGIN_ROOT.trim()); + } + return path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +} + +function resolveGrokCompanionVersion(env = process.env) { + if (cachedGrokCompanionVersion !== undefined) { + return cachedGrokCompanionVersion; + } + try { + const manifest = JSON.parse(fs.readFileSync(path.join(resolvePluginRoot(env), ".claude-plugin", "plugin.json"), "utf8")); + cachedGrokCompanionVersion = typeof manifest?.version === "string" ? manifest.version : null; + } catch { + cachedGrokCompanionVersion = null; + } + return cachedGrokCompanionVersion; +} + const STOP_GATE_OPTION_ENV = "CLAUDE_PLUGIN_OPTION_STOP_GATE"; const CONTINUITY_POLICY_ENV = "GROK_COMPANION_CONTINUITY_POLICY"; const STOP_GATE_TIMEOUT_MS = 240000; @@ -1566,6 +1588,7 @@ async function handleTask(argv, transport = {}) { const record = { ...createJobRecord({ id: jobId, + companionVersion: resolveGrokCompanionVersion(), pid: background ? null : process.pid, mode, cwd, @@ -1982,6 +2005,7 @@ async function handleReview(argv, transport = {}) { const record = { ...createJobRecord({ id: jobId, + companionVersion: resolveGrokCompanionVersion(), pid: background ? null : process.pid, mode: "consult", cwd, @@ -2929,6 +2953,7 @@ async function handleStopGate() { createJobRecordFile(jobFile, { ...createJobRecord({ id: jobId, + companionVersion: resolveGrokCompanionVersion(), pid: process.pid, mode: "consult", cwd, diff --git a/plugins/grok/scripts/lib/state.mjs b/plugins/grok/scripts/lib/state.mjs index df09e68..230bea1 100644 --- a/plugins/grok/scripts/lib/state.mjs +++ b/plugins/grok/scripts/lib/state.mjs @@ -96,6 +96,7 @@ export function createJobRecord(fields) { return withStructuredStatuses({ schemaVersion: 1, engine: "grok", + companionVersion: fields.companionVersion ?? null, id: fields.id, pid, pidIdentity: Object.hasOwn(fields, "pidIdentity") ? fields.pidIdentity : pid ? getProcessIdentity(pid) : null, diff --git a/tests/codex-companion.test.mjs b/tests/codex-companion.test.mjs index a4c1508..ac59a0f 100644 --- a/tests/codex-companion.test.mjs +++ b/tests/codex-companion.test.mjs @@ -1093,6 +1093,7 @@ test("record-acceptance rejects unknown job ids", (t) => { test("foreground timeout persists recovered partial delivery and incomplete cumulative usage", (t) => { const sandbox = makeSandbox(t); + const expectedCompanionVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, "plugins", "codex", ".claude-plugin", "plugin.json"), "utf8")).version; const result = runCompanion(["task", "--json", "implement until timeout"], { cwd: sandbox.workDir, env: envFor(sandbox, { @@ -1105,6 +1106,8 @@ test("foreground timeout persists recovered partial delivery and incomplete cumu const record = JSON.parse(result.stdout); assert.equal(record.status, "error"); assert.equal(record.failureKind, "timeout"); + assert.equal(record.timeoutMs, 50); + assert.equal(record.companionVersion, expectedCompanionVersion); assert.equal(record.resultText, null); const resumeCommand = `'${process.execPath}' '${companion}' task --resume 'thread-123' --cwd '${fs.realpathSync(sandbox.workDir)}'`; const footer = `Resume Codex job ${record.id}: ${resumeCommand}`; diff --git a/tests/codex-state.test.mjs b/tests/codex-state.test.mjs index b408e6d..1b35299 100644 --- a/tests/codex-state.test.mjs +++ b/tests/codex-state.test.mjs @@ -224,6 +224,8 @@ test("job ids contain 128 bits and records expose the canonical Codex fields", ( assert.strictEqual(record.appliedServiceTier, null); assert.strictEqual(record.resolvedModel, "gpt-test"); assert.strictEqual(record.resolvedEffort, "high"); + assert.strictEqual(record.timeoutMs, null); + assert.strictEqual(record.companionVersion, null); assert.strictEqual(record.collectedAt, null); assert.strictEqual(record.sessionId, "session-one"); assert.strictEqual(record.claudeSessionId, "session-one"); diff --git a/tests/fusion-worker-state.test.mjs b/tests/fusion-worker-state.test.mjs index bc5d1ea..1fd305d 100644 --- a/tests/fusion-worker-state.test.mjs +++ b/tests/fusion-worker-state.test.mjs @@ -35,6 +35,16 @@ test("settlement seam identifies pending and settled worker records", () => { assert.strictEqual(isSettledWorker(settled), true); }); +test("created worker records stamp the Fusion companion version", (t) => { + const directory = sandbox(t); + const env = { FUSION_WORKER_STATE_DIR: path.join(directory, "worker-state") }; + const expectedVersion = JSON.parse(fs.readFileSync(new URL("../plugins/fusion/.claude-plugin/plugin.json", import.meta.url), "utf8")).version; + const record = createWorkerRecord({ taskId: "fusion-version-stamp", sessionId: "session-version", dispatchToolUseId: "tool-version", agentType: "fusion:fast-worker", workspaceRoot: directory, limits: {} }, env); + + assert.strictEqual(typeof record.companionVersion, "string"); + assert.strictEqual(record.companionVersion, expectedVersion); +}); + test("markWorkerCollected preserves an already settled acceptance", () => { const settled = markWorkerCollected( unverifiedRecord({ collectedAt: null, acceptance: "rejected", acceptanceRecordedAt: "2026-07-22T00:01:00.000Z", acceptanceSource: "main-loop", awaitingVerdict: false, awaitingVerdictArmedAt: null }), diff --git a/tests/state.test.mjs b/tests/state.test.mjs index a0eacc4..89c5662 100644 --- a/tests/state.test.mjs +++ b/tests/state.test.mjs @@ -147,11 +147,14 @@ test("creating a job record never overwrites an existing record", async (t) => { const jobFile = stateModule.jobFilePath(sandbox.dataDir, sandbox.workDir, jobId); const first = stateModule.createJobRecord({ id: jobId, + companionVersion: "0.0.41", mode: "consult", cwd: sandbox.workDir, briefFile: path.join(sandbox.dataDir, "first.md"), background: false, }); + assert.strictEqual(first.companionVersion, "0.0.41"); + assert.strictEqual(stateModule.createJobRecord({ ...first, id: `${jobId}-default`, companionVersion: undefined }).companionVersion, null); stateModule.createJobRecordFile(jobFile, first); let error; @@ -164,6 +167,7 @@ test("creating a job record never overwrites an existing record", async (t) => { ); assert.strictEqual(error.failureKind, "resource"); assert.deepStrictEqual(stateModule.readJobRecordFile(jobFile), first); + assert.strictEqual(stateModule.readJobRecordFile(jobFile).companionVersion, "0.0.41"); }); test("a running resume owner holds the workspace lease until it becomes terminal", async (t) => { From b698a58c381ebabbb2eb5dc14df2ac4bc25eac5a Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:24 +0800 Subject: [PATCH 2/6] feat: rescue wrapper salvages foreground timeouts with one scripted wind down resume --- docs/codex-contract.md | 2 +- plugins/codex/agents/codex-rescue.md | 1 + plugins/codex/skills/codex-cli-runtime/SKILL.md | 1 + plugins/codex/skills/codex-result-handling/SKILL.md | 1 + plugins/codex/skills/rescue/SKILL.md | 2 +- plugins/codex/skills/task/SKILL.md | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/codex-contract.md b/docs/codex-contract.md index e90f111..41faf0a 100644 --- a/docs/codex-contract.md +++ b/docs/codex-contract.md @@ -61,7 +61,7 @@ The companion does not add a shell command allow list and does not rewrite the u Model and reasoning effort remain unset unless explicitly requested. The Codex configuration remains authoritative for defaults. The companion explicitly sets web search to disabled and workspace-write network access to false by default. `--web` opts a read-only or write task into live Codex web search. `--network` requires `--write` and opts the workspace-write sandbox into network access. Task mode `--output-schema ` validates a regular JSON Schema file up to 256 KiB and parses the final agent message once, while the Codex CLI `review` subcommand accepts but silently ignores `--output-schema`, unchanged from 0.144.6 through the live-verified 0.145.0. -The companion writes the final prompt to Codex stdin. Prompts are never interpolated into a shell command. Programmatic callers use `--request-stdin` and pipe the complete raw companion request through stdin, leaving fixed argv free of request bytes. Claude Code's Bash tool does not expose stdin, so slash commands and wrapper Agents retain a private token-named staging compatibility path: the command model writes the raw request through its Write tool before invoking a fixed companion command containing only that token. Once the staging write has landed, the parser preserves positional whitespace, newlines, quotes, and backslashes byte for byte while removing recognized adapter options. The staged compatibility path is not an end-to-end byte-exact transport because a model performs the Write step. SessionEnd removes verified staging files owned by that Claude session, and later transport creation prunes other verified staging files after one hour. Final response text comes from the bounded JSONL stream, so Codex never receives an unbounded fallback output path. Prompts, individual JSONL events, final responses, raw event ledgers, logs, and rendered diagnostics have explicit byte limits. An oversized single JSONL event is skipped with a diagnostic and the run continues; the run fails as `resource` when the protocol cannot complete after a skip, and oversized prompts or final responses still fail as `resource` before they can inflate durable state. Resume accepts only a persisted Codex thread ID or a companion-selected terminal task record. `--resume-last` is restricted to the current Claude session when its id is available and otherwise selects the newest eligible workspace task. Active jobs are never resume candidates. `--fresh` rejects combination with either resume form and forces a new thread. +The companion writes the final prompt to Codex stdin. Prompts are never interpolated into a shell command. Programmatic callers use `--request-stdin` and pipe the complete raw companion request through stdin, leaving fixed argv free of request bytes. Claude Code's Bash tool does not expose stdin, so slash commands and wrapper Agents retain a private token-named staging compatibility path: the command model writes the raw request through its Write tool before invoking a fixed companion command containing only that token. Once the staging write has landed, the parser preserves positional whitespace, newlines, quotes, and backslashes byte for byte while removing recognized adapter options. The staged compatibility path is not an end-to-end byte-exact transport because a model performs the Write step. SessionEnd removes verified staging files owned by that Claude session, and later transport creation prunes other verified staging files after one hour. Final response text comes from the bounded JSONL stream, so Codex never receives an unbounded fallback output path. Prompts, individual JSONL events, final responses, raw event ledgers, logs, and rendered diagnostics have explicit byte limits. An oversized single JSONL event is skipped with a diagnostic and the run continues; the run fails as `resource` when the protocol cannot complete after a skip, and oversized prompts or final responses still fail as `resource` before they can inflate durable state. Resume accepts only a persisted Codex thread ID or a companion-selected terminal task record. `--resume-last` is restricted to the current Claude session when its id is available and otherwise selects the newest eligible workspace task. Active jobs are never resume candidates. `--fresh` rejects combination with either resume form and forces a new thread. A foreground timeout with a resumable thread is salvaged by the wrapper's single scripted wind down resume; the resumed job links through `request.resumeThreadId`, and a second timeout terminalizes the package. ## Review execution diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md index 3877df3..d0c9c0d 100644 --- a/plugins/codex/agents/codex-rescue.md +++ b/plugins/codex/agents/codex-rescue.md @@ -24,6 +24,7 @@ Forwarding rules: - If the Read call fails, the file is not empty, or the Write call fails, use a foreground Bash call to run `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" transport-discard --raw-args-token TOKEN` with the validated token before returning the failure. Do not expose the token or transport file to the user. - Do not use Read for any path except the newly allocated empty transport file, and do not read it after writing. Do not search, run Git, execute tests, inspect job state, or perform any check, collection, cancellation, or companion operation beyond the fixed task operation. - When an explicit background request returns a receipt, return it unchanged. A direct slash command user inspects progress through status and collects the deliverable through result; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected. +- When a task operation's companion output ends with `state: error` and `failure: timeout` and its body contains a line beginning `Resume Codex job`, run the command printed on that line exactly once, unchanged except for appending a space, `--`, a space, and this double quoted wind down prompt: "Wind down: do not start new work. Finish the smallest coherent deliverable from the work already completed and report the files changed and the verification output." Run it as one additional foreground Bash call and relay the second companion output verbatim in place of the first. This is the single authorized exception to the one operation rule: exactly one resume per task operation, never chained; any second timeout, any other failure, or any output without that line is relayed as received. - Return the companion stdout exactly as received. Do not summarize, paraphrase, prefix, suffix, or continue the work. - Relay the companion's stdout verbatim inside a fenced block. Never retype, summarize, or re-spell any part of it, including footers. Put commentary outside the fence. - If the companion invocation fails, return the failure exactly as Bash reports it. Do not generate a substitute answer. diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md index 8ad2a8d..7634726 100644 --- a/plugins/codex/skills/codex-cli-runtime/SKILL.md +++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md @@ -34,6 +34,7 @@ Execution rules: - A task is read only unless `--write` is present. Review commands are always read only. - Leave `--model` and `--effort` unset unless explicitly requested so Codex configuration remains authoritative. - Use `--resume ` only with a real thread identifier returned by Codex. Use `--resume-last` for the newest eligible task thread launched by the current Claude session, or the newest eligible workspace task when no Claude session id is available. +- A foreground timeout with a resumable thread is salvaged by the wrapper's single scripted wind down resume; the resumed job links through `request.resumeThreadId`, and a second timeout terminalizes the package. - Use `--fresh` only when the caller explicitly requests a new thread. It cannot be combined with either resume form. - Use `--web` only when explicitly requested. Use `--network` only with `--write` and only when explicitly requested. - Use `--output-schema ` only for task mode. The companion resolves the path, requires a regular JSON file at most 256 KiB, and records one JSON parsing result without retrying the task. Native `review` ignores output schemas on tested CLI versions. Review shaped task briefs can use `${CLAUDE_PLUGIN_ROOT}/schemas/adversarial-review-verdict.schema.json`; adversarial review uses that schema automatically. diff --git a/plugins/codex/skills/codex-result-handling/SKILL.md b/plugins/codex/skills/codex-result-handling/SKILL.md index 916198d..8629e72 100644 --- a/plugins/codex/skills/codex-result-handling/SKILL.md +++ b/plugins/codex/skills/codex-result-handling/SKILL.md @@ -9,6 +9,7 @@ user-invocable: false - Return companion stdout verbatim. Do not summarize, paraphrase, reformat, prefix, or suffix it. - Preserve the complete result, file paths, line numbers, Codex thread identifier, job identifier, delivery mode, semantic status, and `state: done`, `state: error`, or `state: cancelled` footer. Preserve the failure kind for error and cancelled outcomes. A transport `state: done` with `semantic: rejected` is not a successful deliverable. - Preserve `state: running` when an explicit background launch or bounded wait has not reached a terminal outcome. Do not present a receipt as completed work. +- A timeout result whose body carries a `Resume Codex job` line authorizes exactly one scripted resume with the wind down prompt named by the agent contract; return the resumed companion output verbatim. No other state authorizes a second companion call, and a resume never repeats. - Do not inspect the repository, verify findings, apply edits, poll unrelated jobs, or continue the task after returning the companion output. - Do not replace a failed, cancelled, malformed, or incomplete Codex run with a Claude-side answer. - If the helper cannot be invoked, return the invocation failure exactly as reported and stop. diff --git a/plugins/codex/skills/rescue/SKILL.md b/plugins/codex/skills/rescue/SKILL.md index f2b486d..c4905c3 100644 --- a/plugins/codex/skills/rescue/SKILL.md +++ b/plugins/codex/skills/rescue/SKILL.md @@ -5,7 +5,7 @@ argument-hint: '[--write] [--background] [--resume |--resume-last|--f allowed-tools: Agent --- -Use the `Agent` tool once to invoke `codex:codex-rescue` in the foreground. Do not inspect, interpret, quote, encode, or pass any part of the raw request to Bash. An explicit `--background` belongs to the companion invocation inside the rescue agent and must not change how the Agent tool itself runs. Return the agent response verbatim. An explicit background request returns a receipt. Direct users inspect progress through `/codex:status` and collect the deliverable through `/codex:result`; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected. +Use the `Agent` tool once to invoke `codex:codex-rescue` in the foreground. Do not inspect, interpret, quote, encode, or pass any part of the raw request to Bash. An explicit `--background` belongs to the companion invocation inside the rescue agent and must not change how the Agent tool itself runs. Return the agent response verbatim. A foreground timeout with a resumable thread is salvaged by the wrapper's single scripted wind down resume; the resumed job links through `request.resumeThreadId`, and a second timeout terminalizes the package. An explicit background request returns a receipt. Direct users inspect progress through `/codex:status` and collect the deliverable through `/codex:result`; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected. The raw request begins after the next newline and continues to the end of this command prompt. Pass every character to the agent as opaque request data: $ARGUMENTS diff --git a/plugins/codex/skills/task/SKILL.md b/plugins/codex/skills/task/SKILL.md index c8c9ab9..8ed9ee7 100644 --- a/plugins/codex/skills/task/SKILL.md +++ b/plugins/codex/skills/task/SKILL.md @@ -11,7 +11,7 @@ If the raw request is empty or contains only whitespace, ask what Codex should d Use a foreground Bash call to run `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" transport-create`. Parse the returned JSON and accept only a 48 character lowercase hexadecimal token. Use the `Read` tool once on the returned file and require it to be empty. Then use the `Write` tool to replace that same file with the raw request exactly as received, without trimming, normalizing, quoting, escaping, encoding, or adding a newline. Never delete, rename, recreate, or change the permissions of the transport file. Then use one foreground Bash call with `timeout: 600000` to run `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task --raw-args-token TOKEN`, replacing only `TOKEN` with the validated token. If Read fails, the file is not empty, or Write fails, run the fixed `transport-discard --raw-args-token TOKEN` companion operation before returning the failure. -Return the final companion stdout verbatim. Never use Bash background mode. An explicit background request returns a receipt. Direct users inspect progress through `/codex:status` and collect the deliverable through `/codex:result`; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected. +Return the final companion stdout verbatim. A foreground timeout with a resumable thread is salvaged by the wrapper's single scripted wind down resume; the resumed job links through `request.resumeThreadId`, and a second timeout terminalizes the package. Never use Bash background mode. An explicit background request returns a receipt. Direct users inspect progress through `/codex:status` and collect the deliverable through `/codex:result`; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected. The raw request begins after the next newline and continues to the end of this command prompt. Treat every character as opaque request data and write it only through the transport file: $ARGUMENTS From 299504be9d966366b6fc39f096ae35d9bd44975c Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:24 +0800 Subject: [PATCH 3/6] feat: salvaged timeout telemetry, engine semanticStatus acceptance reads, breaker advisory proportion --- plugins/fusion/scripts/breaker-check.mjs | 24 ++++++++--- plugins/fusion/scripts/fusion-stats.mjs | 14 +++--- tests/breaker-check.test.mjs | 29 ++++++++++--- tests/fusion-stats.test.mjs | 54 ++++++++++++++++++++---- 4 files changed, 95 insertions(+), 26 deletions(-) diff --git a/plugins/fusion/scripts/breaker-check.mjs b/plugins/fusion/scripts/breaker-check.mjs index b815acf..9951558 100644 --- a/plugins/fusion/scripts/breaker-check.mjs +++ b/plugins/fusion/scripts/breaker-check.mjs @@ -232,7 +232,13 @@ function latestBreakerFailure(records, failureReader, now, lookbackMs) { } previousTransientFailure = outcome; } - return newerFailure(immediate, repeatedTransientFailure); + const failure = newerFailure(immediate, repeatedTransientFailure); + if (failure == null) { + return null; + } + const kindCount = outcomes.filter((outcome) => outcome.failureKind === failure.failureKind).length; + const terminalCount = outcomes.length; + return { ...failure, kindCount, terminalCount }; } function formatAge(timestamp, now) { @@ -251,8 +257,14 @@ function formatAge(timestamp, now) { return `${days} day${days === 1 ? "" : "s"} ago`; } -function advisoryLine(engine, failure, now) { - return `fusion breaker advisory: treat the ${engine} breaker as open unless verified recovered; last failure ${failure.failureKind} ${formatAge(failure.timestamp, now)}. Route new work to another eligible healthy lane.`; +function formatLookbackHours(lookbackMs) { + const hours = lookbackMs / (60 * 60 * 1000); + return hours % 1 === 0 ? String(hours) : hours.toFixed(1); +} + +function advisoryLine(engine, failure, now, lookbackMs = DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1000) { + const hoursText = formatLookbackHours(lookbackMs); + return `fusion breaker advisory: treat the ${engine} breaker as open unless verified recovered; last failure ${failure.failureKind} ${formatAge(failure.timestamp, now)} (${failure.kindCount} ${failure.failureKind} across ${failure.terminalCount} terminal jobs, ${hoursText}h window). Route new work to another eligible healthy lane.`; } function workerFailure(record, now, lookbackMs) { @@ -279,14 +291,14 @@ function run(env = process.env, now = Date.now()) { const codex = latestBreakerFailure(readDeduplicatedJobRecords(resolveCodexStateRoots(env)), codexFailure, now, lookbackMs); const lines = []; if (grok) { - lines.push(advisoryLine("grok", grok, now)); + lines.push(advisoryLine("grok", grok, now, lookbackMs)); } if (codex) { - lines.push(advisoryLine("codex", codex, now)); + lines.push(advisoryLine("codex", codex, now, lookbackMs)); } const fastWorker = latestBreakerFailure(workerBreakerRecords(env), workerFailure, now, lookbackMs); if (fastWorker) { - lines.push(advisoryLine("fusion:fast-worker", fastWorker, now)); + lines.push(advisoryLine("fusion:fast-worker", fastWorker, now, lookbackMs)); } if (lines.length > 0) { process.stdout.write(`${lines.join("\n")}\n`); diff --git a/plugins/fusion/scripts/fusion-stats.mjs b/plugins/fusion/scripts/fusion-stats.mjs index 12ee27e..ee82f86 100644 --- a/plugins/fusion/scripts/fusion-stats.mjs +++ b/plugins/fusion/scripts/fusion-stats.mjs @@ -124,7 +124,7 @@ export function normalizeCollectionMethod(value) { } function semanticAcceptance(raw) { - const recorded = raw?.acceptance; + const recorded = raw?.semanticStatus ?? raw?.acceptance; return recorded === "accepted" || recorded === "rejected" ? recorded : "unverified"; } @@ -1057,10 +1057,13 @@ function summarizeCodexSkuTelemetry(entries) { if (entry.createdAtMs < cutoff) { continue; } - const row = trendRows.get(entry.sku) ?? { sku: entry.sku, jobs: 0, outputTokens: 0, outputTokenOverflow: false, timeouts: 0, nearCap: 0 }; + const row = trendRows.get(entry.sku) ?? { sku: entry.sku, jobs: 0, outputTokens: 0, outputTokenOverflow: false, timeouts: 0, salvagedTimeouts: 0, nearCap: 0 }; row.jobs += 1; if (entry.failureKind === "timeout") { row.timeouts += 1; + if (entry.acceptance === "accepted") { + row.salvagedTimeouts += 1; + } } if (entry.failureKind === "timeout" || (entry.durationSeconds != null && entry.timeoutMs != null && entry.timeoutMs / 1000 - entry.durationSeconds <= 30)) { row.nearCap += 1; @@ -1208,7 +1211,7 @@ export function fileBasedEngineStats(descriptor, { all = false, env = process.en pendingTransportJobs += 1; } const historical = Number.isFinite(Date.parse(raw?.finishedAt ?? "")) && Date.parse(raw.finishedAt) < acceptanceEpoch.timestamp; - if (jobId && job.status === "error" && semanticAcceptance(raw) === "accepted") { + if (jobId && job.status === "error" && semanticAcceptance(raw) === "accepted" && nonEmptyString(raw?.failureKind) !== "timeout") { if (historical) { historicalAcceptanceAnomalies += 1; } else { @@ -2149,9 +2152,10 @@ function renderCodexSkuTrend(lines, rows) { if (!Array.isArray(rows) || rows.length === 0) { return; } - lines.push("", "By SKU, last 7 days:", "SKU | jobs | output tokens | timeout share | near-cap share"); + lines.push("", "By SKU, last 7 days:", "SKU | jobs | output tokens | timeout share | near-cap share | salvaged"); for (const row of rows) { - lines.push(`${row.sku} | ${row.jobs} | ${row.outputTokenOverflow ? "overflow" : row.outputTokens} | ${formatShare(row.timeoutShare)} | ${formatShare(row.nearCapShare)}`); + const salvaged = `${row.salvagedTimeouts ?? 0}/${row.timeouts ?? 0}`; + lines.push(`${row.sku} | ${row.jobs} | ${row.outputTokenOverflow ? "overflow" : row.outputTokens} | ${formatShare(row.timeoutShare)} | ${formatShare(row.nearCapShare)} | ${salvaged}`); } } diff --git a/tests/breaker-check.test.mjs b/tests/breaker-check.test.mjs index 8ada4ba..54d9a86 100644 --- a/tests/breaker-check.test.mjs +++ b/tests/breaker-check.test.mjs @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { codexFailureKind, resolveCodexStateDir, resolveCodexStateRoots } from "../plugins/fusion/scripts/breaker-check.mjs"; +import { advisoryLine, codexFailureKind, resolveCodexStateDir, resolveCodexStateRoots } from "../plugins/fusion/scripts/breaker-check.mjs"; const repoRoot = path.join(import.meta.dirname, ".."); const script = path.join(repoRoot, "plugins", "fusion", "scripts", "breaker-check.mjs"); @@ -67,10 +67,27 @@ test("an in-window quota failure prints a grok breaker advisory", (t) => { const result = run(sandbox); assert.strictEqual(result.status, 0); assert.match(result.stdout, /treat the grok breaker as open unless verified recovered/); - assert.match(result.stdout, /last failure quota \d+ minutes? ago/); + assert.match(result.stdout, /last failure quota \d+ minutes? ago \(1 quota across 1 terminal jobs, 12h window\)/); assert.strictEqual(result.stderr, ""); }); +test("advisoryLine renders kind and terminal proportions with singular and plural counts", () => { + const now = Date.parse("2026-01-01T12:00:00.000Z"); + const lookbackMs = 12 * 60 * 60 * 1000; + assert.strictEqual( + advisoryLine("grok", { failureKind: "quota", timestamp: now - 30 * 60000, kindCount: 1, terminalCount: 1 }, now, lookbackMs), + "fusion breaker advisory: treat the grok breaker as open unless verified recovered; last failure quota 30 minutes ago (1 quota across 1 terminal jobs, 12h window). Route new work to another eligible healthy lane." + ); + assert.strictEqual( + advisoryLine("codex", { failureKind: "rate_limited", timestamp: now - 2 * 60000, kindCount: 2, terminalCount: 3 }, now, lookbackMs), + "fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure rate_limited 2 minutes ago (2 rate_limited across 3 terminal jobs, 12h window). Route new work to another eligible healthy lane." + ); + assert.strictEqual( + advisoryLine("grok", { failureKind: "auth", timestamp: now - 90 * 60000, kindCount: 1, terminalCount: 2 }, now, 0.1 * 60 * 60 * 1000), + "fusion breaker advisory: treat the grok breaker as open unless verified recovered; last failure auth 1 hour ago (1 auth across 2 terminal jobs, 0.1h window). Route new work to another eligible healthy lane." + ); +}); + test("breaker quota recovery matches the companion diagnostic vocabulary", () => { for (const diagnostic of [ "usage limit reached", @@ -159,7 +176,7 @@ test("a consecutive second Codex rate limit opens the breaker", (t) => { const result = run(sandbox); assert.strictEqual(result.status, 0); - assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure rate_limited \d+ minutes? ago\. Route new work to another eligible healthy lane\.\n$/); + assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure rate_limited \d+ minutes? ago \(2 rate_limited across 2 terminal jobs, 12h window\)\. Route new work to another eligible healthy lane\.\n$/); assert.strictEqual(result.stderr, ""); }); @@ -244,7 +261,7 @@ test("two Grok rate limits inside the configured window open only the Grok break const result = run(sandbox); assert.strictEqual(result.status, 0); - assert.match(result.stdout, /^fusion breaker advisory: treat the grok breaker as open unless verified recovered; last failure rate_limited \d+ minutes? ago\. Route new work to another eligible healthy lane\.\n$/); + assert.match(result.stdout, /^fusion breaker advisory: treat the grok breaker as open unless verified recovered; last failure rate_limited \d+ minutes? ago \(2 rate_limited across 2 terminal jobs, 12h window\)\. Route new work to another eligible healthy lane\.\n$/); assert.strictEqual(result.stderr, ""); }); @@ -278,7 +295,7 @@ test("a typed Codex adapter error uses the canonical status and finished timesta const result = run(sandbox); assert.strictEqual(result.status, 0); - assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure auth \d+ minutes? ago\. Route new work to another eligible healthy lane\.\n$/); + assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure auth \d+ minutes? ago \(1 auth across 1 terminal jobs, 12h window\)\. Route new work to another eligible healthy lane\.\n$/); }); test("a typed Codex protocol failure opens the compatibility breaker", (t) => { @@ -292,7 +309,7 @@ test("a typed Codex protocol failure opens the compatibility breaker", (t) => { const result = run(sandbox); assert.strictEqual(result.status, 0); - assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure protocol \d+ minutes? ago\. Route new work to another eligible healthy lane\.\n$/); + assert.match(result.stdout, /^fusion breaker advisory: treat the codex breaker as open unless verified recovered; last failure protocol \d+ minutes? ago \(1 protocol across 1 terminal jobs, 12h window\)\. Route new work to another eligible healthy lane\.\n$/); }); test("a collaboration policy violation is treated as a protocol breaker but an ordinary policy denial is not", (t) => { diff --git a/tests/fusion-stats.test.mjs b/tests/fusion-stats.test.mjs index 497747e..9650f21 100644 --- a/tests/fusion-stats.test.mjs +++ b/tests/fusion-stats.test.mjs @@ -304,8 +304,8 @@ test("aggregates the canonical Codex lifecycle and direct token usage", (t) => { assert.strictEqual(stats.tokenUsage.jobsWithoutUsage, 1); assert.deepStrictEqual(stats.tokenUsage.totals, { inputTokens: 120, cachedInputTokens: 40, outputTokens: 30, reasoningOutputTokens: 12, totalTokens: 150 }); assert.deepStrictEqual(stats.last7DaysBySku, [ - { sku: "gpt-5.4@high", jobs: 1, outputTokens: 30, outputTokenOverflow: false, timeouts: 0, nearCap: 0, timeoutShare: 0, nearCapShare: 0 }, - { sku: "unknown@unavailable", jobs: 1, outputTokens: 0, outputTokenOverflow: false, timeouts: 1, nearCap: 1, timeoutShare: 1, nearCapShare: 1 } + { sku: "gpt-5.4@high", jobs: 1, outputTokens: 30, outputTokenOverflow: false, timeouts: 0, salvagedTimeouts: 0, nearCap: 0, timeoutShare: 0, nearCapShare: 0 }, + { sku: "unknown@unavailable", jobs: 1, outputTokens: 0, outputTokenOverflow: false, timeouts: 1, salvagedTimeouts: 0, nearCap: 1, timeoutShare: 1, nearCapShare: 1 } ]); }); @@ -430,21 +430,30 @@ test("Codex renders per-SKU trends from the seven days ending at the newest reco writeCodexJob(stateRoot, dir, "recent-timeout", { status: "error", jobClass: "task", - acceptance: "rejected", + semanticStatus: "rejected", failureKind: "timeout", createdAt: "2026-07-20T00:00:00.000Z", request: { model: "gpt-trend", effort: "xhigh" }, tokenUsageAvailability: "available", tokenUsage: usage(13) }); + writeCodexJob(stateRoot, dir, "recent-salvaged-timeout", { + status: "error", + jobClass: "task", + semanticStatus: "accepted", + failureKind: "timeout", + createdAt: "2026-07-20T01:00:00.000Z", + request: { model: "gpt-trend", effort: "xhigh" }, + tokenUsageAvailability: "available", + tokenUsage: usage(5) + }); const stats = codexStats({ env: { FUSION_CODEX_STATE: stateRoot, FUSION_DATA_DIR: fusionData }, cwd: dir }); assert.deepStrictEqual(stats.last7DaysBySku, [ - { sku: "gpt-trend@high", jobs: 1, outputTokens: 7, outputTokenOverflow: false, timeouts: 0, nearCap: 0, timeoutShare: 0, nearCapShare: 0 }, - { sku: "gpt-trend@xhigh", jobs: 2, outputTokens: 24, outputTokenOverflow: false, timeouts: 1, nearCap: 2, timeoutShare: 0.5, nearCapShare: 1 } + { sku: "gpt-trend@xhigh", jobs: 3, outputTokens: 29, outputTokenOverflow: false, timeouts: 2, salvagedTimeouts: 1, nearCap: 3, timeoutShare: 2 / 3, nearCapShare: 1 } ]); const rendered = renderFusionStats({ scope: dir, codex: stats }); - assert.match(rendered, /By SKU, last 7 days:\nSKU \| jobs \| output tokens \| timeout share \| near-cap share\ngpt-trend@high \| 1 \| 7 \| 0\.0% \| 0\.0%\ngpt-trend@xhigh \| 2 \| 24 \| 50\.0% \| 100\.0%/); + assert.match(rendered, /By SKU, last 7 days:\nSKU \| jobs \| output tokens \| timeout share \| near-cap share \| salvaged\ngpt-trend@xhigh \| 3 \| 29 \| 66\.7% \| 100\.0% \| 1\/2/); assert.doesNotMatch(rendered, /gpt-old@low \|/); }); @@ -1287,7 +1296,7 @@ test("terminal ledgers supplement cleaned jobs and safely dedupe live state", (t assert.strictEqual(recovered.evidence.recoveredTerminalJobs, 1); assert.deepStrictEqual(recovered.tokenUsage.totals, { inputTokens: 100, cachedInputTokens: 40, outputTokens: 20, reasoningOutputTokens: 5, totalTokens: 120 }); assert.deepStrictEqual(recovered.last7DaysBySku, [ - { sku: "ledger-model@high", jobs: 1, outputTokens: 20, outputTokenOverflow: false, timeouts: 1, nearCap: 1, timeoutShare: 1, nearCapShare: 1 } + { sku: "ledger-model@high", jobs: 1, outputTokens: 20, outputTokenOverflow: false, timeouts: 1, salvagedTimeouts: 0, nearCap: 1, timeoutShare: 1, nearCapShare: 1 } ]); writeCodexJob(stateRoot, dir, "cleaned-job", { @@ -1443,11 +1452,11 @@ test("semantic acceptance reads the job record and excludes non-terminal transpo assert.strictEqual(before.pendingTransportJobs, 1); assert.strictEqual(before.acceptanceScope, "terminal transport jobs only"); - fs.writeFileSync(completedJob, JSON.stringify({ id: "completed-job", workspaceRoot: dir, status: "done", jobClass: "task", acceptance: "accepted", semanticAcceptance: "rejected" })); + fs.writeFileSync(completedJob, JSON.stringify({ id: "completed-job", workspaceRoot: dir, status: "done", jobClass: "task", semanticStatus: "accepted", semanticAcceptance: "rejected" })); const after = codexStats({ env: { FUSION_CODEX_STATE: stateRoot, FUSION_DATA_DIR: path.join(dir, "fusion") }, cwd: dir }); assert.deepStrictEqual(after.byAcceptance, { accepted: 1 }); - fs.writeFileSync(completedJob, JSON.stringify({ id: "completed-job", workspaceRoot: dir, status: "done", jobClass: "task", acceptance: "rejected" })); + fs.writeFileSync(completedJob, JSON.stringify({ id: "completed-job", workspaceRoot: dir, status: "done", jobClass: "task", semanticStatus: "rejected" })); const rejected = codexStats({ env: { FUSION_CODEX_STATE: stateRoot, FUSION_DATA_DIR: path.join(dir, "fusion") }, cwd: dir }); assert.deepStrictEqual(rejected.byAcceptance, { rejected: 1 }); }); @@ -2028,6 +2037,33 @@ test("Codex reports acceptance anomalies only when the job record and transport assert.match(rendered, /Acceptance anomalies:\n- Accepted ledger entries with error transport: 1 \(eeeeeeee\)\n- Done jobs without acceptance records: 1 \(ffffffffffffffffffffffffffffffff\)/); }); +test("Codex acceptedWithErrorTransport excludes salvaged timeouts but keeps other error kinds", (t) => { + const dir = sandbox(t); + const stateRoot = path.join(dir, "state"); + const fusionData = path.join(dir, "fusion"); + const nonTimeoutAcceptedId = "1".repeat(32); + const salvagedTimeoutId = "2".repeat(32); + writeCodexJob(stateRoot, dir, nonTimeoutAcceptedId, { + status: "error", + jobClass: "task", + semanticStatus: "accepted", + failureKind: "tool_error" + }); + writeCodexJob(stateRoot, dir, salvagedTimeoutId, { + status: "error", + jobClass: "task", + semanticStatus: "accepted", + failureKind: "timeout" + }); + + const stats = codexStats({ env: { FUSION_CODEX_STATE: stateRoot, FUSION_DATA_DIR: fusionData }, cwd: dir }); + assert.deepStrictEqual(stats.acceptanceAnomalies, { + acceptedWithErrorTransport: [nonTimeoutAcceptedId], + doneWithoutAcceptance: [] + }); + assert.ok(!stats.acceptanceAnomalies.acceptedWithErrorTransport.includes(salvagedTimeoutId)); +}); + test("Codex groups pre-epoch acceptance anomalies and falls back from invalid epochs", (t) => { const dir = sandbox(t); const stateRoot = path.join(dir, "state"); From b111aea11d356593da9296ceb8e9330659001bea Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:24 +0800 Subject: [PATCH 4/6] refactor: stop gate demand and reverify share runtimeTaskMissing and needsCancellation predicates --- plugins/fusion/scripts/worker-lifecycle.mjs | 50 +++++++++++---------- tests/fusion-worker-lifecycle.test.mjs | 50 ++++++++++++++++++++- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/plugins/fusion/scripts/worker-lifecycle.mjs b/plugins/fusion/scripts/worker-lifecycle.mjs index cfc9cbe..729ef02 100644 --- a/plugins/fusion/scripts/worker-lifecycle.mjs +++ b/plugins/fusion/scripts/worker-lifecycle.mjs @@ -1021,6 +1021,18 @@ function settleReapedWorker(current, now, env) { }, now, env); } +function settleReapedIfActive(taskId, now, env, extraGuard) { + let settled = false; + updateLifecycleWorkerRecord(taskId, env, (current) => { + if (!current || isTerminalWorkerStatus(current.transportStatus) || !extraGuard(current)) { + return null; + } + settled = true; + return settleReapedWorker(current, now, env); + }); + return settled; +} + function handlePostToolUse(input, env, failed = false) { if (!input.agent_id && ["Read", "TaskOutput", "TaskStop"].includes(input.tool_name)) { const taskId = typeof input.tool_input?.task_id === "string" ? input.tool_input.task_id : null; @@ -1029,12 +1041,7 @@ function handlePostToolUse(input, env, failed = false) { if (["TaskOutput", "TaskStop"].includes(input.tool_name) && taskId && noTaskFoundError(input, failed)) { const now = new Date().toISOString(); for (const record of records.filter((candidate) => candidate.sessionId === input.session_id && !isTerminalWorkerStatus(candidate.transportStatus) && [candidate.backgroundTaskId, candidate.agentId].includes(taskId))) { - updateLifecycleWorkerRecord(record.taskId, env, (current) => { - if (!current || current.sessionId !== input.session_id || isTerminalWorkerStatus(current.transportStatus) || ![current.backgroundTaskId, current.agentId].includes(taskId)) { - return null; - } - return settleReapedWorker(current, now, env); - }); + settleReapedIfActive(record.taskId, now, env, (current) => current.sessionId === input.session_id && [current.backgroundTaskId, current.agentId].includes(taskId)); } return; } @@ -1789,12 +1796,8 @@ function collectorStopGate(input, env) { return false; } -function unverifiedCollectedWorkers(records) { - return records.filter(isPendingSettlement); -} - function writeAcceptanceAdvisory(records, env) { - const unverified = unverifiedCollectedWorkers(rereadPendingRecords(records, isPendingSettlement, env)); + const unverified = settleOnlyRecords(rereadPendingRecords(records, isPendingSettlement, env)); if (unverified.length === 0) { return; } @@ -1811,7 +1814,7 @@ function terminalCollectionInstruction(record) { : collectId ? `Call TaskOutput with block=true for terminal owned task ${collectId} and collect Fusion task ${worker} before finishing.` : `Collect terminal owned task ${worker} before finishing.`; } -function settleOnlyRecords(records) { +export function settleOnlyRecords(records) { return records.filter(isPendingSettlement); } @@ -1873,10 +1876,18 @@ export function reverifyInFlightRecords(records, tasks, env = process.env) { .filter((record) => record && !terminalCollectedRecord(record) && !terminalTransportObserved(record, runtimeTaskForRecord(record, tasks))); } +export function runtimeTaskMissing(record, hasBackgroundTasks, tasks) { + return hasBackgroundTasks ? !runtimeTaskForRecord(record, tasks) : (record.cancelAttemptCount ?? 0) >= 2; +} + +export function needsCancellation(record) { + return record.transportStatus === "cancel_requested" || (!record.userBackgroundAuthorized && !["ready_uncollected", "ready_background"].includes(record.transportStatus) && (record.stopBlockCount ?? 0) >= 6); +} + export function reverifyCancellationRecords(records, env = process.env) { return records .map((record) => readWorkerRecord(record.taskId, env)) - .filter((record) => record && !isTerminalWorkerStatus(record.transportStatus) && !terminalCollectedRecord(record)); + .filter((record) => record && !terminalCollectedRecord(record) && needsCancellation(record)); } function claimStopAdvisory(sessionId, kind, signature, env) { @@ -1950,7 +1961,7 @@ function handleStop(input, env) { }); pending.push(updated); } - const cancellations = pending.filter((record) => record.transportStatus === "cancel_requested" || (!record.userBackgroundAuthorized && !["ready_uncollected", "ready_background"].includes(record.transportStatus) && (record.stopBlockCount ?? 0) >= 6)); + const cancellations = pending.filter(needsCancellation); const verifiedCancellations = reverifyCancellationRecords(cancellations, env); if (verifiedCancellations.length > 0) { const now = new Date().toISOString(); @@ -1965,17 +1976,10 @@ function handleStop(input, env) { } const reapedTaskIds = []; for (const record of verifiedCancellations.filter((candidate) => candidate.backgroundTaskId || candidate.agentId)) { - if (hasBackgroundTasks ? runtimeTaskForRecord(record, tasks) : (record.cancelAttemptCount ?? 0) < 2) { + if (!runtimeTaskMissing(record, hasBackgroundTasks, tasks)) { continue; } - let settled = false; - updateLifecycleWorkerRecord(record.taskId, env, (current) => { - if (!current || isTerminalWorkerStatus(current.transportStatus)) { - return null; - } - settled = true; - return settleReapedWorker(current, now, env); - }); + const settled = settleReapedIfActive(record.taskId, now, env, () => true); if (settled) { reapedTaskIds.push(record.taskId); } diff --git a/tests/fusion-worker-lifecycle.test.mjs b/tests/fusion-worker-lifecycle.test.mjs index cc39da8..6cb9ffa 100644 --- a/tests/fusion-worker-lifecycle.test.mjs +++ b/tests/fusion-worker-lifecycle.test.mjs @@ -7,7 +7,7 @@ import { test } from "node:test"; import { fusionRepositoryKey } from "../plugins/fusion/scripts/fusion-stats.mjs"; import { createWorkerRecord, readWorkerSessionState, readWorkerRecords, recordWorkerAcceptance, updateWorkerRecord, WORKER_COLLECTION_METHODS } from "../plugins/fusion/scripts/lib/worker-state.mjs"; -import { reverifyCancellationRecords, reverifyInFlightRecords, validateWorkerBrief, workerBudgetFailure, workerLimits } from "../plugins/fusion/scripts/worker-lifecycle.mjs"; +import { needsCancellation, reverifyCancellationRecords, reverifyInFlightRecords, runtimeTaskMissing, settleOnlyRecords, validateWorkerBrief, workerBudgetFailure, workerLimits } from "../plugins/fusion/scripts/worker-lifecycle.mjs"; const repoRoot = path.join(import.meta.dirname, ".."); const script = path.join(repoRoot, "plugins", "fusion", "scripts", "worker-lifecycle.mjs"); @@ -4577,3 +4577,51 @@ test("hooks configuration wires lifecycle events through an executable shell com const dispatchHandlers = hooks.PostToolUse.flatMap((group) => group.hooks.map((hook) => ({ matcher: group.matcher, command: hook.command }))).filter((hook) => hook.command?.includes("inline-delegation-guard.mjs")); assert.deepStrictEqual(dispatchHandlers, [{ matcher: "^(Agent|Task)$", command: 'node "${CLAUDE_PLUGIN_ROOT}/scripts/inline-delegation-guard.mjs"' }]); }); + +test("cancellation demand predicate covers both disjuncts and terminal-ready exclusions", () => { + const cases = [ + [{ transportStatus: "cancel_requested" }, true], + [{ transportStatus: "pending_async", userBackgroundAuthorized: false, stopBlockCount: 6 }, true], + [{ transportStatus: "pending_async", userBackgroundAuthorized: false, stopBlockCount: 5 }, false], + [{ transportStatus: "pending_async", userBackgroundAuthorized: true, stopBlockCount: 6 }, false], + [{ transportStatus: "ready_uncollected", userBackgroundAuthorized: false, stopBlockCount: 6 }, false], + [{ transportStatus: "ready_background", userBackgroundAuthorized: false, stopBlockCount: 6 }, false] + ]; + + for (const [record, expected] of cases) { + assert.strictEqual(needsCancellation(record), expected); + } +}); + +test("runtime task absence uses runtime visibility or cancellation attempts", () => { + const record = { backgroundTaskId: "runtime-task", cancelAttemptCount: 0 }; + + assert.strictEqual(runtimeTaskMissing(record, true, [{ id: "runtime-task" }]), false); + assert.strictEqual(runtimeTaskMissing(record, true, []), true); + assert.strictEqual(runtimeTaskMissing({ cancelAttemptCount: 0 }, false, []), false); + assert.strictEqual(runtimeTaskMissing({ cancelAttemptCount: 1 }, false, []), false); + assert.strictEqual(runtimeTaskMissing({ cancelAttemptCount: 2 }, false, []), true); +}); + +test("settle-only predicate keeps only pending settlement records", () => { + const records = [ + { taskId: "pending", completionContract: "worker", transportStatus: "done", collectedAt: "2026-07-28T00:00:00.000Z", awaitingVerdict: true }, + { taskId: "settled", completionContract: "worker", transportStatus: "done", collectedAt: "2026-07-28T00:00:00.000Z", awaitingVerdict: false } + ]; + + assert.deepStrictEqual(settleOnlyRecords(records), [records[0]]); +}); + +test("cancellation demand and re-verification agree for an uncollected failed record", (t) => { + const box = sandbox(t); + const worker = createWorkerRecord({ + taskId: `fusion-${"e".repeat(24)}`, + sessionId: "session-1", + agentType: "fusion:fast-worker", + workspaceRoot: box.cwd + }, envFor(box)); + const candidate = updateWorkerRecord(worker.taskId, envFor(box), (current) => ({ ...current, transportStatus: "failed", stopBlockCount: 6 })); + + assert.strictEqual(needsCancellation(candidate), true); + assert.deepStrictEqual(reverifyCancellationRecords([candidate], envFor(box)).map((record) => record.taskId), [candidate.taskId]); +}); From 32f9bde957d34a6106a0f7fae90850cd4226372e Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:24 +0800 Subject: [PATCH 5/6] feat: quick tier 570s sizing clause, luna light middle band, fusion smoke skill --- CONTRIBUTING.md | 1 + README.md | 3 ++- plugins/fusion/rules-manifest.json | 1 + plugins/fusion/rules/orchestration.md | 6 +++--- plugins/fusion/rules/troubleshooting.md | 4 ++-- plugins/fusion/skills/smoke/SKILL.md | 16 ++++++++++++++++ 6 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 plugins/fusion/skills/smoke/SKILL.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d11d0f..c3b7cca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ Any change to `plugins/fusion/rules/orchestration.md` or `plugins/fusion/rules/t - A Codex package that cannot fit the foreground cap is split or routed elsewhere. Complexity and duration never silently detach it. - Independent packages fan out in a single message rather than being queued one at a time. Dependence is never removed by switching engines. - Agent scheduling, companion delivery, and CLI process supervision remain distinct. A manual receipt created by Fusion orchestration gets one same turn bounded collection attempt; a managed Grok detachment stays inside its owning Agent until terminal and marks collection. The monitor suppresses collected jobs but emits a delayed fallback after the grace period when a terminal managed job remains uncollected, as a best effort owner loss fallback. Direct Codex or Grok slash commands with explicit `--background` return manual receipts for status and result collection. + - After refreshing installed plugin caches, run `/fusion:smoke` before real delegated work. - `fusion:deep-reasoner` is read only advice, never an implementation retry. `fusion:fast-worker` owns resolved Claude tool or privacy packages, and `fusion:trivial-worker` is an exact tiny fallback when no eligible peer lane fits. - The question policy whitelist still routes questions and problem descriptions to read only main loop work and requested changes into implementation posture. - The session execution posture protocol (coordinate, implement, triage) still persists per goal and still forces implement before product edits once accumulation triggers fire. diff --git a/README.md b/README.md index 2eaeaf5..c013d60 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Then, in a new session, run `/fusion:setup` once per machine (it writes the rout | `/fusion:panel ` | Blind multi-model panel with attributed adjudication, for decisions where a wrong answer is expensive. Also fires from plain language ("help me decide", "compare these") | | `/fusion:ultra ` | Fans a large task out as a fleet while preserving lane ownership: Codex keeps the primary deep implementation seat and Grok supplies protected burst, research, large context, and independent breadth. It synthesizes and verifies the combined result, fires from "go deep", "audit everything", or "be exhaustive", and returns small tasks to ordinary routing. | | `/fusion:setup` | Install or update the routing rules into `~/.claude/rules/`; offers the optional permission allow | +| `/fusion:smoke` | Run the post-update live smoke wave across version parity and the three dispatch lanes before real delegated work | | `/fusion:config` | Read the local model configuration across engines, enumerate available models, and change defaults interactively | | `/fusion:doctor` | Audit model pins, environment overrides, peer model defaults (Grok and Codex config keys), rules drift, and stale agent copies | @@ -110,7 +111,7 @@ CI also runs `node bench/manifest.mjs --check`; contributors should run it after - `.claude-plugin/marketplace.json`: the marketplace manifest; installs as marketplace `claude-code-fusion`. - `plugins/codex/`: the Codex integration (companion runtime, `codex-rescue` agent, `/codex:*` commands). - `plugins/grok/`: the Grok integration (companion runtime, `grok-rescue` agent, `/grok:*` commands). -- `plugins/fusion/`: the orchestration layer: tier agents (`agents/`), the routing policy payload (`rules/`), and the `/fusion:panel`, `/fusion:setup`, `/fusion:doctor`, `/fusion:stats`, `/fusion:ultra`, and `/fusion:config` commands. +- `plugins/fusion/`: the orchestration layer: tier agents (`agents/`), the routing policy payload (`rules/`), and the `/fusion:panel`, `/fusion:setup`, `/fusion:smoke`, `/fusion:doctor`, `/fusion:stats`, `/fusion:ultra`, and `/fusion:config` commands. - `bench/`: the benchmark methodology, harness, and task suite; no published results yet, see the publication gate in [bench/METHODOLOGY.md](bench/METHODOLOGY.md). - `tests/`: the fake Codex, Grok, and Claude driven test suite. diff --git a/plugins/fusion/rules-manifest.json b/plugins/fusion/rules-manifest.json index 4f7f810..494947c 100644 --- a/plugins/fusion/rules-manifest.json +++ b/plugins/fusion/rules-manifest.json @@ -25,6 +25,7 @@ "aead643e6dee6b0de04e97c5dbbb2f9f8c5e04dfbd12e8305b0abb97ab33524e", "b3649bf8c8fb1f3ede0ccfc7fb727a8adf1e13628a18f4a84d08efbee75bd934", "b5283c0356c9004385157f3360ca81a3601253e370216b390049d069a29a2250", + "c29808811e61113fd7861f5e973354c18d59341645e3fb5217fa89a81c5cd3da", "c435cc1486eeff00f5b3a1c76017d46d8f05d30ebd248435d575a1461d7da5d4", "c5cb1e9d17cd2771e0f320e760a249188d69658ef3a63b31556000a64fed88e1", "c68785a9d73cc2cb83b740db7a80744ddd293e6da011c6a80a17b045f9219e17", diff --git a/plugins/fusion/rules/orchestration.md b/plugins/fusion/rules/orchestration.md index c7f99e7..d68e79d 100644 --- a/plugins/fusion/rules/orchestration.md +++ b/plugins/fusion/rules/orchestration.md @@ -83,9 +83,9 @@ An isolated Codex worktree changes the execution workspace, not merely the prose | Brief shape | Lane | Notes | |---|---|---| | Spec grade: explicit completion criteria, output contract, boundaries, verification command | Codex primary implementation lane; Grok only inside a protected role | A foreground Codex companion call is capped at 10 minutes by the Bash tool. Codex stays foreground by default regardless of complexity, estimated duration, review size, or model. If a package is unlikely to fit the cap, split it into bounded packages. If it cannot be split, bypass Codex and route a bounded isolated package to Grok under `burst`, or use the matching Claude fallback. Never add `--background` as an implicit sizing decision. Only an incoming user request that already includes `--background` may detach Codex, and a receipt from a job created by Fusion orchestration requires one same turn bounded collection attempt through fusion:job-collector. Both forcing an oversized package into the foreground and silently detaching it are routing errors | -| Quick scoped package: edits with a recipe, codemods, small fixes across a few files | Codex quick tier, gpt-5.6-terra, by default; Grok under `burst` | A package that fits this row routes to the named Codex volume lane even when Grok is idle and follows the admission ladder above when that workspace is occupied. The dispatch passes `--model gpt-5.6-terra --effort xhigh` as leading companion options in the rescue direct prompt, and the brief header line repeats them for audit; prose inside the brief never selects a model. A Grok burst brief names low effort, or the account's fast coding SKU from the live model listing, because the lane default is its current flagship. A Grok package with a long verification chain or a change surface likely to approach its write turn budget of 60 turns instead consolidates into a bounded spec grade Codex brief, carries an explicit `--max-turns` override, or routes to `fusion:fast-worker`. A max turns death is a routing error, not a retry candidate. Fixture heavy test authoring routes to the Codex quick tier or `fusion:fast-worker` by default. A Grok brief for fixture heavy test authoring is admissible only when the fixtures arrive pre built in the brief, because fixture construction burns Grok turns and stalls `apply_patch` | +| Quick scoped package: edits with a recipe, codemods, small fixes across a few files | Codex quick tier, gpt-5.6-terra, by default; Grok under `burst` | A package that fits this row routes to the named Codex volume lane even when Grok is idle and follows the admission ladder above when that workspace is occupied. The dispatch passes `--model gpt-5.6-terra --effort xhigh` as leading companion options in the rescue direct prompt, and the brief header line repeats them for audit; prose inside the brief never selects a model. A Grok burst brief names low effort, or the account's fast coding SKU from the live model listing, because the lane default is its current flagship. A Grok package with a long verification chain or a change surface likely to approach its write turn budget of 60 turns instead consolidates into a bounded spec grade Codex brief, carries an explicit `--max-turns` override, or routes to `fusion:fast-worker`. A max turns death is a routing error, not a retry candidate. Fixture heavy test authoring routes to the Codex quick tier or `fusion:fast-worker` by default. A Grok brief for fixture heavy test authoring is admissible only when the fixtures arrive pre built in the brief, because fixture construction burns Grok turns and stalls `apply_patch`. A quick brief is sized to one 570 second foreground flight: its verification command runs the narrow suites that exercise the touched files, while the complete suite list still rides in the brief for the orchestrator's collection rerun in a full environment. A package whose verification chain alone approaches the flight budget, or whose edit surface spans subsystems, consolidates into a bounded spec grade brief or splits before dispatch. A timeout that survives the wrapper's single scripted resume is a sizing error to fix at the brief, not a redispatch candidate. | | Needs the Claude Code tool surface (hooks, subagent files, MCP), the Claude privacy boundary, or a resolved brief with no eligible peer lane | fusion:fast-worker | Claude execution fallback, not the default for spec grade or quick scoped work | -| Trivial and high volume light work: single file renames, small doc fixes, short mechanical checks, drafts, review comment triage, and research digest backup | Codex volume tier, gpt-5.6-luna, by default; Grok under `burst`; fallback fusion:trivial-worker | A package that fits this row routes to the named Codex volume lane even when Grok is idle and follows the admission ladder above when that workspace is occupied. Research digests, review triage, doc summaries, and mechanical checks default to gpt-5.6-luna at effort xhigh. A package whose source material is X or another live platform read leaves this row for Grok under `live-web` regardless of its digest shape; platform access is a lane monopoly and no volume tier can substitute. Independent bounded packages overflow to Grok under `burst` when the Codex slot is busy. Leaving the volume tiers idle while this work runs on premium lanes is a routing defect. The dispatch passes `--model gpt-5.6-luna --effort xhigh` as leading companion options in the rescue direct prompt, and the brief header line repeats them for audit; prose inside the brief never selects a model. A Grok burst brief names low effort; batch codemods may name the fast SKU from the live listing, currently grok-composer-2.5-fast. Live web research uses Grok consult under `live-web`; very large context reads use `large-context`. Use `fusion:trivial-worker` only when Claude-only tools or privacy are required, or no eligible peer lane remains | +| Trivial and high volume light work: single file renames, small doc fixes, short mechanical checks, drafts, review comment triage, and research digest backup | Codex volume tier, gpt-5.6-luna, by default; Grok under `burst`; fallback fusion:trivial-worker | A package that fits this row routes to the named Codex volume lane even when Grok is idle and follows the admission ladder above when that workspace is occupied. Research digests, review triage, doc summaries, and mechanical checks default to gpt-5.6-luna at effort xhigh. A package whose source material is X or another live platform read leaves this row for Grok under `live-web` regardless of its digest shape; platform access is a lane monopoly and no volume tier can substitute. Independent bounded packages overflow to Grok under `burst` when the Codex slot is busy. Leaving the volume tiers idle while this work runs on premium lanes is a routing defect. The dispatch passes `--model gpt-5.6-luna --effort xhigh` as leading companion options in the rescue direct prompt, and the brief header line repeats them for audit; prose inside the brief never selects a model. A Grok burst brief names low effort; batch codemods may name the fast SKU from the live listing, currently grok-composer-2.5-fast. Live web research uses Grok consult under `live-web`; very large context reads use `large-context`. Use `fusion:trivial-worker` only when Claude-only tools or privacy are required, or no eligible peer lane remains. Light middle band packages, a single subsystem fix with a short suite, doc and comment sweeps, and mechanical test updates, default to gpt-5.6-luna when the recipe is explicit; terra is reserved for quick packages whose edits span files or need judgment inside the recipe. | | Review shaped work: PR and issue verification, reproducing reported bugs | /grok:review for the fast mechanical sweep; /codex:adversarial-review for depth | Interactive judgment and taste calls stay in the main loop | | Codebase search, inventory, "where is X" | built in Explore agent | Simple scoped searches may pin the Explore agent to haiku through the Agent tool's model parameter | | Hard reasoning: architecture, root cause on stubborn bugs, correctness, concurrency, security, high stakes quality | fusion:deep-reasoner | Read only adviser on Fable at maximum effort; it recommends and the main session decides | @@ -162,7 +162,7 @@ Every brief is self contained: goal, constraints, relevant paths, and what done - Independent packages and independent sections of one task are decomposed and dispatched in a single message; sequential dispatch of independent work is a defect. Repeated width one dispatch turns on a goal with multiple independent packages are the signature of that defect, and the narrow wave watch in `/fusion:stats` surfaces it. Repeated narrow waves on a fleet shaped goal without a visible `fleet-decline: ` line are the same defect. - Agent scheduling, companion delivery, engine process supervision, and Grok's native background tool waiting are separate layers. Every ordinary Agent is foreground delivery. The harness launches Agent calls asynchronously by default, but a launch receipt is not delivery: completion arrives as a task notification, which arms mandatory collection. Every dispatch still requires collection and verification before the package counts as done. Launch independent Agents together in one tool message for concurrency, but do not set `run_in_background` and do not accept an async launch receipt as delivery. `grok:grok-review-runner` is the sole managed exception allowed to use Agent `run_in_background`. Codex and Grok wrapper Agents always remain foreground. Only an incoming user request that explicitly contains companion `--background` authorizes the companion child to detach inside its foreground wrapper and return a manual receipt; it never authorizes background wrapper delivery. Fusion records every Claude worker as an owned task; if Claude Code still launches one asynchronously, the Stop hook blocks turn completion and requires `TaskOutput` collection, or `TaskStop` when a lifecycle budget has expired. Codex companion Bash calls stay foreground with the 10 minute tool timeout. Grok companion calls also remain foreground inside their owning Agent; expected duration does not authorize managed detachment. Ordinary Grok calls prefer `--no-wait-for-background` because their hard `Agent` deny also removes the native task lifecycle; a binary without that flag gets an explicit `--background-wait-timeout` shorter than the companion's outer deadline. That bounded internal wait is not companion detachment. The CLI child running in its own supervised process group is never by itself a user background job. The main loop never runs a work package or launches a detached shell. The coordinate micro step, the single triage edit, and judgment dense authorship under the authorship rule are not work packages. - Heartbeat rule: a legitimate watch style wakeup on delegated work in flight emits one short user visible status line naming what is still in flight and when the next check happens. A wakeup with only tool calls and no visible text is a silent turn and is banned, and so is a hand rolled Bash or ScheduleWakeup polling loop used in place of fusion:job-collector. A wakeup with in flight work emits its status line and otherwise takes no tool action; informationless tool calls on advisory wakeups are a defect. -- Same turn collection attempt: this obligation applies to a manual receipt that crosses into the main session from a detached job launched by Fusion orchestration. Follow it in the same turn through fusion:job-collector with a closed direct prompt containing exactly one standalone `engine: codex|grok` line and one standalone `job: <32 lowercase hexadecimal characters>` line. Do not describe or repeat either identity elsewhere in the prompt. The lifecycle binds the collected marker to this dispatch identity. The collector resolves the engine companion itself, performs global job lookup without a working-directory selector, and invokes status plus blocking result through fixed argv without shell command strings. Its single collection window is at most 540000ms. A timeout, dead job, or status error leaves the package uncollected and must reach the user with the Fusion task id, peer job id, the literal state `uncollected`, and the exact `/codex:result ` or `/grok:result ` command. Direct Codex and Grok slash commands with explicit `--background` return manual receipts for the user to inspect and collect through their status and result commands; monitors are notification only. A dispatch whose receipt is never followed by same-goal collection and verification is a contract failure. +- Same turn collection attempt: this obligation applies to a manual receipt that crosses into the main session from a detached job launched by Fusion orchestration. Follow it in the same turn through fusion:job-collector with a closed direct prompt containing exactly one standalone `engine: codex|grok` line and one standalone `job: <32 lowercase hexadecimal characters>` line. Do not describe or repeat either identity elsewhere in the prompt. The lifecycle binds the collected marker to this dispatch identity. The collector resolves the engine companion itself, performs global job lookup without a working-directory selector, and invokes status plus blocking result through fixed argv without shell command strings. Its single collection window is at most 540000ms. A timeout, dead job, or status error leaves the package uncollected and must reach the user with the Fusion task id, peer job id, the literal state `uncollected`, and the exact `/codex:result ` or `/grok:result ` command. Direct Codex and Grok slash commands with explicit `--background` return manual receipts for the user to inspect and collect through their status and result commands; monitors are notification only. A dispatch whose receipt is never followed by same-goal collection and verification is a contract failure. A foreground Codex timeout with a resumable thread is salvaged inside the rescue wrapper itself by one scripted wind down resume; the orchestrator treats the salvaged relay like any other collected result, and a second timeout terminalizes the package as a sizing error. - Transport completion is not semantic acceptance. The main session verifies every peer and Claude worker result before relying on it. Settlement is batched per wave: collect and verify every result in a wave, then record all verdicts in one `/fusion:stats --record` call with multiple `=` pairs, where each id is the fusion task id or the engine job id. Per package settlement turns are reserved for waves of one. One settlement validates every pair before writing and is idempotent to rerun after a partial transport failure; it writes the worker ledger and linked engine record for every recorded verdict. Strict record forms may pass as direct arguments, and anything carrying free text still uses the raw-args transport. Record accepted only when the verification command or explicit acceptance criteria pass. Record `rejected` when the collected result or verification fails. A rejected verdict carries `--reason` and, when the cause is judgment shaped rather than mechanical, a `--failure-kind` of `intent_override`, `scope_rewrite`, `wrong_approach`, or `style_mismatch`, so intent and taste failures become measurable instead of living only in free text. Leave it `unverified` only when no reliable judgment was possible. Every collected result is settled in the wave in which it is collected. `unverified` is temporary and must not survive the goal's completion. Stats default every terminal job without a recorded judgment to `unverified`; never infer acceptance from `state: done`, `delivery: complete`, or any transport status. - A non deliverable final message starts an obligation, not an outcome: a background receipt created by Fusion and a truncated run ending in forward looking narration or missing verification are both unfinished. Resume a truncated, non forwarder agent with SendMessage to finish and report with verification; for a background receipt created by Fusion, dispatch fusion:job-collector instead. A foreground Codex receipt remains a contract failure under the preceding rule. - Related follow up packages reuse the existing worker thread via SendMessage instead of cold starting a new agent, unless the thread rotation rules in the fusion plugin's troubleshooting rules (shipped with the plugin, consulted on demand) say otherwise. diff --git a/plugins/fusion/rules/troubleshooting.md b/plugins/fusion/rules/troubleshooting.md index 59726f6..92dcfc5 100644 --- a/plugins/fusion/rules/troubleshooting.md +++ b/plugins/fusion/rules/troubleshooting.md @@ -38,7 +38,7 @@ When an engine is broken, use the other peer only where its lane ownership or a The upstream Grok CLI has no default turn limit. An unset limit is unlimited. One turn is one main-agent model call plus its tool cycle; subagent calls are excluded. The companion now supplies `--max-turns 60` for both consult and write. Consult previously defaulted to 25 turns. When Grok reaches this limit, it writes the complete final JSON envelope to stdout before exiting 1 and writes `Error: max turns reached` to stderr. The companion salvages the stdout envelope, preserves partial text and usage fields, and classifies the terminal outcome as `failureKind: "turn_limit"`. -Fixture heavy test authoring briefs are the dominant `turn_limit` and 570 second timeout failure family on the Grok lane. Route them to Codex or `fusion:fast-worker`, or pre build the fixtures into the brief. +Fixture heavy test authoring briefs are the dominant `turn_limit` and 570 second timeout failure family on the Grok lane. Route them to Codex or `fusion:fast-worker`, or pre build the fixtures into the brief. The rescue wrapper performs a single scripted wind down resume before any redispatch is considered. The Grok headless JSON has no top-level `model` field. A model name appears only as a key in `modelUsage` when usage attaches, so model capture depends on that map. Error-path capture also depends on salvaging the envelope before classifying the turn limit failure. Missing `modelUsage` means that the resolved model remains unavailable. @@ -48,7 +48,7 @@ Two truncation classes verified in production require recovery. A hard cap cut e For either signature, resume with SendMessage and instruct `write the deliverable now, no more scanning`. The resumed worker receives fresh turn headroom and typically finishes in minutes. Ledger turn counts after resuming hide the original cut, so diagnose it from the `.output` transcript tail. -The Fusion job collection bounded window is 540s while the Codex companion result wait allows 570s, so a job finishing inside that 30s gap times out the collector and is collected manually via `/codex:result`. +The Fusion job collection bounded window is 540s while the Codex companion result wait allows 570s, so a job finishing inside that 30s gap times out the collector and is collected manually via `/codex:result`. A package dying twice on timeout is resized, not retried. The fleet mode state file has no writer within the plugin, so disable the fleet default with `FUSION_FLEET_MODE=off` or write that file externally. diff --git a/plugins/fusion/skills/smoke/SKILL.md b/plugins/fusion/skills/smoke/SKILL.md new file mode 100644 index 0000000..7b50dd4 --- /dev/null +++ b/plugins/fusion/skills/smoke/SKILL.md @@ -0,0 +1,16 @@ +--- +name: smoke +description: Runs a three probe live smoke wave after a plugin update and reports gate chain health before real work rides it. +argument-hint: '' +when_to_use: After a plugin cache refresh or version update, before dispatching real delegated work. +allowed-tools: 'Agent, Bash, Read' +--- + +Verify the update landed, then prove the dispatch chain end to end with three tiny probes before real packages ride it. + +1. Version parity: read `~/.claude/plugins/installed_plugins.json` and compare the fusion, codex, and grok entries against `.claude-plugin/marketplace.json` in this repository when present. Report each version pair; a mismatch is a FAIL naming the stale plugin. +2. Fan out three probes as parallel foreground Agent calls in one message: a `codex:codex-rescue` task passing `--model gpt-5.6-luna --effort low -- Reply with exactly this single line and nothing else: FUSION-SMOKE-CODEX-OK`; a `grok:grok-rescue` task whose direct prompt passes `--effort low --` followed by a brief whose header line is `grok-role: burst | effort low | acceptance: exact marker line` and whose body asks for exactly the line FUSION-SMOKE-GROK-OK; a `fusion:trivial-worker` brief in the `fusion-brief: v1` envelope with `context-mode: isolated`, a goal of returning exactly the line FUSION-SMOKE-WORKER-OK, a scope of no file access, and an acceptance of the exact marker line. +3. Collect all three, then settle them in one `/fusion:stats --record` call with one pair per probe: accepted only when the exact marker line came back, rejected otherwise. +4. Report one PASS or FAIL line per gate: version parity, codex dispatch and envelope, grok dispatch and sandbox handshake, worker dispatch and deliverable, settlement write. A FAIL names the failing surface and advises holding real dispatches on that lane until repaired. + +The wave is a probe, not work: never attach real packages to it, and never skip the settlement step, because the settlement write is itself one of the gates under test. From 22ed79bff949a49a57bc2bd61a09cf77c5cf041b Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 28 Jul 2026 13:29:24 +0800 Subject: [PATCH 6/6] chore: release 0.0.42 --- .claude-plugin/marketplace.json | 8 ++++---- CHANGELOG.md | 10 ++++++++++ plugins/codex/.claude-plugin/plugin.json | 2 +- plugins/fusion/.claude-plugin/plugin.json | 2 +- plugins/grok/.claude-plugin/plugin.json | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 215c7cd..0c96229 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,14 +6,14 @@ "email": "hi@okis.dev" }, "description": "Multi-model orchestration marketplace for Claude Code.", - "version": "0.0.41", + "version": "0.0.42", "plugins": [ { "name": "grok", "source": "./plugins/grok", "displayName": "Grok Companion", "description": "Local Grok CLI delegation: task, review, resumable history, best-of-n tournaments, background jobs, stats, and setup health checks.", - "version": "0.0.41", + "version": "0.0.42", "author": { "name": "Harry Yep" }, @@ -34,7 +34,7 @@ "source": "./plugins/codex", "displayName": "Codex Companion", "description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.", - "version": "0.0.41", + "version": "0.0.42", "author": { "name": "Harry Yep" }, @@ -55,7 +55,7 @@ "source": "./plugins/fusion", "displayName": "Fusion Orchestrator", "description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.", - "version": "0.0.41", + "version": "0.0.42", "author": { "name": "Harry Yep" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8cb86..ac36e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # changelog +## 0.0.42 + +- the codex rescue wrapper salvages foreground timeouts itself: a companion result ending `state: error` with `failure: timeout` and a `Resume Codex job` line authorizes exactly one scripted resume carrying a fixed wind down prompt, run as one additional foreground Bash call and relayed verbatim in place of the first output, never chained; the exception lands consistently in the rescue agent contract, the result handling, cli runtime, rescue, and task skills, the codex contract doc, the orchestration rules, and troubleshooting (15 of 20 recorded timeout deaths had ended salvage accepted only after a manual resume round; the wrapper now spends that round itself) +- quick tier briefs get a time budget: the quick scoped routing row sizes a brief to one 570 second foreground flight, with the narrow suites for the touched files in the verification command and the complete list reserved for the orchestrator's collection rerun, an oversized package consolidates or splits before dispatch, and a timeout that survives the single scripted resume is a sizing error, not a redispatch candidate; the trivial row names the light middle band (explicit recipe single subsystem fixes, doc sweeps, mechanical test updates) as gpt-5.6-luna work, reserving terra for quick packages whose edits span files or need judgment inside the recipe +- job records carry their runtime provenance: codex records persist `timeoutMs` and `companionVersion`, grok records and fusion worker ledger records gain `companionVersion`, each plugin reading its own manifest through a cached `CLAUDE_PLUGIN_ROOT` aware helper; persisting `timeoutMs` makes the 0.0.41 near cap counter count near cap finishes for real (it had been reading a field no writer ever set), and the version stamps make release watch mining sliceable by plugin version instead of timestamp inference +- fusion stats reads real engine acceptance: `semanticAcceptance` prefers `semanticStatus`, the key engine records actually write, over the worker ledger's `acceptance` key, so byAcceptance, lane signal drift, and the accepted with error transport anomaly stop being blind to engine records; the per SKU trend adds a `salvaged` column counting timeout deaths later accepted, and the anomaly excludes designed timeout salvage while still flagging every other failure kind +- the breaker advisory carries proportion: `latestBreakerFailure` also returns the failing kind's count and the engine's terminal job count inside the lookback window, and the advisory renders `( across terminal jobs, h window)` so a singleton failure reads as a singleton instead of a lane wide alarm +- stop gate demand and satisfaction share predicates, round two of the 0.0.39 class: `runtimeTaskMissing` and `needsCancellation` are extracted as exported predicates evaluated by both the demand filters and the reverify pass, reaped settlement funnels through one `settleReapedIfActive` shape from both the stop partition and the no task found consumption site, and the duplicate `unverifiedCollectedWorkers` folds into `settleOnlyRecords` +- `/fusion:smoke` runs a post update live smoke wave before real work rides a refreshed install: version parity between installed caches and the marketplace manifest, one tiny probe per dispatch lane, and settlement recorded through `/fusion:stats` as one of the tested gates; the readme commands table and layout bullet list it, and the contributing release checklist runs it after every cache refresh + ## 0.0.41 - the worker token budget stops eating deliverables: a one shot wind down lands at 85 percent of the output token budget telling the worker to stop calling tools and write the deliverable, and after the limit trips exactly one final `Write` is still permitted (`terminalWriteGraceUsedAt`) while every other tool stays denied with the deny reason naming the grace; the subagent start announcement and all three worker prompts state both (observed live: three of four mining workers lost their final report write to the old hard deny) diff --git a/plugins/codex/.claude-plugin/plugin.json b/plugins/codex/.claude-plugin/plugin.json index b9f53b0..e04442d 100644 --- a/plugins/codex/.claude-plugin/plugin.json +++ b/plugins/codex/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "codex", "displayName": "Codex Companion", - "version": "0.0.41", + "version": "0.0.42", "description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.", "author": { "name": "Harry Yep" diff --git a/plugins/fusion/.claude-plugin/plugin.json b/plugins/fusion/.claude-plugin/plugin.json index baead71..1eec9a4 100644 --- a/plugins/fusion/.claude-plugin/plugin.json +++ b/plugins/fusion/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "fusion", "displayName": "Fusion Orchestrator", - "version": "0.0.41", + "version": "0.0.42", "description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.", "author": { "name": "Harry Yep" diff --git a/plugins/grok/.claude-plugin/plugin.json b/plugins/grok/.claude-plugin/plugin.json index 20eb971..3d4e204 100644 --- a/plugins/grok/.claude-plugin/plugin.json +++ b/plugins/grok/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "grok", "displayName": "Grok Companion", - "version": "0.0.41", + "version": "0.0.42", "description": "Local Grok CLI delegation: task, review, resumable history, background jobs, stats, and setup health checks.", "author": { "name": "Harry Yep"