fix(codex): successful sessions_spawn and goal tool results recorded as failures#96856
Conversation
…s success isCodexToolResultError fail-closes every tool-result status not in its non-error allowlist, but the allowlist omitted several success statuses emitted by OpenClaw tools that are exposed to codex agents: - sessions_spawn accepted launches -> details.status "accepted" - create_goal / update_goal results -> details.status "created" / "updated" So a successful accepted spawn (openclaw#96833), and successful goal create/update, were classified as errors: reported to codex as success: false (mapped to a Failed item status) and persisted on the transcript as isError: true. This adds those statuses to the allowlist alongside their sibling success statuses (completed/recorded/started/running). Genuinely failed or forbidden results (status "error"/"forbidden") stay fail-closed. Adds regression tests: accepted spawn and created/updated goal results are reported as successful dynamic tool calls; a forbidden spawn still fails.
|
Codex review: needs maintainer review before merge. Reviewed June 25, 2026, 5:13 PM ET / 21:13 UTC. Summary PR surface: Source +3, Tests +94. Total +97 across 2 files. Reproducibility: yes. Source inspection shows current main fail-closes Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Next step before merge
Security Review detailsBest possible solution: Land the narrow Codex classifier fix with its focused tests if CI remains green; keep the compaction/core PRs separate because they do not replace this bridge path. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main fail-closes Is this the best way to solve the issue? Yes. Updating the Codex dynamic-tool classifier is the narrow origin fix for this bridge; compaction guards or core embedded-runner changes alone do not fix goal-tool statuses through Codex app-server dynamic tools. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 13ecca5408cb. Label changesLabel justifications:
Evidence reviewedPR surface: Source +3, Tests +94. Total +97 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…s success (openclaw#96856) isCodexToolResultError fail-closes every tool-result status not in its non-error allowlist, but the allowlist omitted several success statuses emitted by OpenClaw tools that are exposed to codex agents: - sessions_spawn accepted launches -> details.status "accepted" - create_goal / update_goal results -> details.status "created" / "updated" So a successful accepted spawn (openclaw#96833), and successful goal create/update, were classified as errors: reported to codex as success: false (mapped to a Failed item status) and persisted on the transcript as isError: true. This adds those statuses to the allowlist alongside their sibling success statuses (completed/recorded/started/running). Genuinely failed or forbidden results (status "error"/"forbidden") stay fail-closed. Adds regression tests: accepted spawn and created/updated goal results are reported as successful dynamic tool calls; a forbidden spawn still fails.
…s success (openclaw#96856) isCodexToolResultError fail-closes every tool-result status not in its non-error allowlist, but the allowlist omitted several success statuses emitted by OpenClaw tools that are exposed to codex agents: - sessions_spawn accepted launches -> details.status "accepted" - create_goal / update_goal results -> details.status "created" / "updated" So a successful accepted spawn (openclaw#96833), and successful goal create/update, were classified as errors: reported to codex as success: false (mapped to a Failed item status) and persisted on the transcript as isError: true. This adds those statuses to the allowlist alongside their sibling success statuses (completed/recorded/started/running). Genuinely failed or forbidden results (status "error"/"forbidden") stay fail-closed. Adds regression tests: accepted spawn and created/updated goal results are reported as successful dynamic tool calls; a forbidden spawn still fails.
…l calls (#98659) isCodexToolResultError fail-closes any dynamic-tool result whose details.status is absent from its non-error allowlist. get_goal returns details.status "found" or "missing" (a successful read of the thread goal, or its absence), neither of which was in the allowlist, so every get_goal call was classified as an error: reported to codex as success: false and persisted on the transcript with isError: true. Sibling #96856 added the write-side goal statuses (created/updated) and the accepted spawn status but missed the read-side get_goal statuses. This adds found/missing alongside them. Genuinely failed statuses stay fail-closed.
…s success (openclaw#96856) isCodexToolResultError fail-closes every tool-result status not in its non-error allowlist, but the allowlist omitted several success statuses emitted by OpenClaw tools that are exposed to codex agents: - sessions_spawn accepted launches -> details.status "accepted" - create_goal / update_goal results -> details.status "created" / "updated" So a successful accepted spawn (openclaw#96833), and successful goal create/update, were classified as errors: reported to codex as success: false (mapped to a Failed item status) and persisted on the transcript as isError: true. This adds those statuses to the allowlist alongside their sibling success statuses (completed/recorded/started/running). Genuinely failed or forbidden results (status "error"/"forbidden") stay fail-closed. Adds regression tests: accepted spawn and created/updated goal results are reported as successful dynamic tool calls; a forbidden spawn still fails.
…l calls (openclaw#98659) isCodexToolResultError fail-closes any dynamic-tool result whose details.status is absent from its non-error allowlist. get_goal returns details.status "found" or "missing" (a successful read of the thread goal, or its absence), neither of which was in the allowlist, so every get_goal call was classified as an error: reported to codex as success: false and persisted on the transcript with isError: true. Sibling openclaw#96856 added the write-side goal statuses (created/updated) and the accepted spawn status but missed the read-side get_goal statuses. This adds found/missing alongside them. Genuinely failed statuses stay fail-closed.
…s success (openclaw#96856) isCodexToolResultError fail-closes every tool-result status not in its non-error allowlist, but the allowlist omitted several success statuses emitted by OpenClaw tools that are exposed to codex agents: - sessions_spawn accepted launches -> details.status "accepted" - create_goal / update_goal results -> details.status "created" / "updated" So a successful accepted spawn (openclaw#96833), and successful goal create/update, were classified as errors: reported to codex as success: false (mapped to a Failed item status) and persisted on the transcript as isError: true. This adds those statuses to the allowlist alongside their sibling success statuses (completed/recorded/started/running). Genuinely failed or forbidden results (status "error"/"forbidden") stay fail-closed. Adds regression tests: accepted spawn and created/updated goal results are reported as successful dynamic tool calls; a forbidden spawn still fails.
…l calls (openclaw#98659) isCodexToolResultError fail-closes any dynamic-tool result whose details.status is absent from its non-error allowlist. get_goal returns details.status "found" or "missing" (a successful read of the thread goal, or its absence), neither of which was in the allowlist, so every get_goal call was classified as an error: reported to codex as success: false and persisted on the transcript with isError: true. Sibling openclaw#96856 added the write-side goal statuses (created/updated) and the accepted spawn status but missed the read-side get_goal statuses. This adds found/missing alongside them. Genuinely failed statuses stay fail-closed.
…l calls (openclaw#98659) isCodexToolResultError fail-closes any dynamic-tool result whose details.status is absent from its non-error allowlist. get_goal returns details.status "found" or "missing" (a successful read of the thread goal, or its absence), neither of which was in the allowlist, so every get_goal call was classified as an error: reported to codex as success: false and persisted on the transcript with isError: true. Sibling openclaw#96856 added the write-side goal statuses (created/updated) and the accepted spawn status but missed the read-side get_goal statuses. This adds found/missing alongside them. Genuinely failed statuses stay fail-closed.
Related: #96833
What Problem This Solves
Fixes an issue where a Codex-runtime agent's successful dynamic tool calls are recorded as tool failures. When an agent launches a subagent, the accepted launch (
sessions_spawn) is persisted on the session transcript as a failed tool result; the same happens for successfulcreate_goal/update_goalcalls. The model then sees its own successful actions as failures in the live transcript, and the compaction "Tool Failures" summary lists them as failures, making an agent's failure list untrustworthy.This is the source-side counterpart to #96842 (which guards the compaction summary on the read side). This PR fixes the classification at its origin so the wrong terminal state is never produced for these results.
Why This Change Was Made
In the Codex app-server runtime,
isCodexToolResultError(extensions/codex/src/app-server/dynamic-tools.ts) classifies a dynamic tool result by a non-error status allowlist and fail-closes any status not in the list. A result classified as an error becomessuccess: falsein the response sent to Codex, which Codex maps to aFaileddynamic-tool item status, and which OpenClaw persists on the transcript astoolResult.isError: true.Several success statuses emitted by OpenClaw tools that are exposed to Codex agents were missing from the allowlist:
sessions_spawnaccepted launches →details.status: "accepted"create_goal/update_goalresults →details.status: "created"/"updated"The fix adds these statuses to the allowlist alongside their sibling success statuses (
completed,recorded,started,running, …). Genuinely failed or rejected results (status: "error"/"forbidden") remain fail-closed and continue to be reported as failures.Scope boundary: this changes only the Codex dynamic-tool result classifier. The Pi agent-loop runtime is unaffected — it uses an inverse failure-status allowlist (
src/agents/tool-result-error.ts) under whichaccepted/created/updatedare already non-errors. The message tool'ssuppressedstatus is intentionally left out, since a suppressed outbound send may be intended as a model-facing failure (a product decision, not a clear bug).User Impact
Codex-runtime agents no longer have their successful subagent launches and successful goal create/update calls recorded as tool failures. The live transcript and the compaction summary reflect only genuinely failed tool calls. As a side benefit, post-tool-use hooks now fire correctly for accepted launches (the Codex runtime gates those hooks on the same success flag).
Evidence
Tier: internal deterministic classifier (
isCodexToolResultErroris pure logic over a tool result), but its effect is observable end-to-end through the real persistence path, so a real-runtime before/after is included below in addition to focused tests.Focused regression tests
extensions/codex/src/app-server/dynamic-tools.test.ts, exercised through the realcreateCodexDynamicToolBridgeexecutor:sessions_spawnresult is reported as a successful dynamic tool call (fails before the change, passes after);create_goal("created") andupdate_goal("updated") results are reported as successful;forbiddensessions_spawnresult is still reported as a failure (deny symmetry, guards against over-correction);Touched-file gates (local):
oxfmt --checkclean;oxlintclean.Real behavior proof
sessions_spawnresult is persisted on the session transcript withtoolResult.isError: true(and reported to Codex assuccess: false) instead of as a success.runCodexAppServerAttemptorchestration driving the real dynamic-tool bridge →isCodexToolResultError→ event projector → session-JSONL transcript writer. The tools that return the accepted/created/updated payloads are supplied via the test tool registry; the classification and persistence path are the production code paths. (The classifier, not the Codex binary, computesisError: the Rust app-server only relayssuccessand maps it to the item status — verified against the upstream Codex source.)sessions_spawn(accepted), acreate_goal(created), and anupdate_goal(updated) dynamic-tool result, then read the persistedsession.jsonland inspect eachtoolResultentry. Run the identical scenario on the base branch (no change) and on this branch.session.jsonlentry for the accepted launch (copied from the run; thecreated/updatedentries are identical with their own tool/status):{"role":"toolResult","toolCallId":"call-accepted","toolName":"sessions_spawn","isError":false, "content":[{"type":"toolResult","name":"sessions_spawn","content":"Accepted: launching child session to scan logs."}]}accepted/created/updated), protocolsuccess = trueand persistedtoolResult.isError = false. On the base branch (without the change), the identical run persiststoolResult.isError = trueand reportssuccess = falsefor all three.Security / Compatibility
No auth, secret, dependency, permission, migration, config, or data-access surface changes. No persisted-state shape change. The change only widens which statuses are classified as non-errors; failure statuses are unaffected.
Risks and Mitigations
The allowlist is tool-name-agnostic, so any dynamic tool result carrying
status: "accepted"/"created"/"updated"is now treated as a success. A survey of producers found these statuses are only emitted by successful operations (failures useerror/forbidden), and the deny-symmetry test guards the failure path. The allowlist remains hand-maintained and could diverge from producers again as new tools are added; a follow-up to derive it from a shared typed status contract would make this class of divergence statically impossible.AI-assisted; the contributor manually reviewed the change and is responsible for it.