fix(server): Codex subagent approvals show up in the parent thread - #13703
juliusmarminge merged 1 commit into
Conversation
A multi-agent V2 subagent inherits the root's approval policy, and Codex asks for its approvals on the child's own native thread and turn. The adapter wrote the approval node, runtime request and card to the child thread, which the sidebar hides, so the user never saw the request and the parent looked like it was still working. The approval is now asked on the top-level thread and run, hung off the subagent that asked. The pending map is still keyed by request id, so answering from the parent completes Codex's server request. This covers command, file change, permission and MCP elicitation approvals and the legacy exec/apply-patch approvals, which all build through buildApprovalRequestArtifacts. The subagent_v2_approval fixture is recorded live on Codex 0.156.1 with gpt-6-luna in approval-required mode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a localized Codex adapter bug fix that makes subagent approvals visible in the parent thread while preserving the child’s native execution and request handling. The added replay fixture and assertions cover the routing and pending-state behavior without changing product defaults or static-analysis configuration. You can add or adjust custom eligibility rules. Learn more. |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
e1b6476
into
t3code/codex-turn-mapping
A Codex multi-agent V2 subagent inherits the root's approval policy, and Codex asks for its approvals on the child's own native thread and turn. The adapter wrote the approval node, runtime request and card to the child thread. The sidebar hides that thread, and the parent's "Pending Approval" pill only reads the parent's own requests. So the user never saw the request, and the parent looked like it was still working while Codex waited.
Follows #13624, which has merged.
What changed
buildApprovalRequestArtifactsinCodexAdapterV2walks from the requesting turn up to the top-level turn. It writes the approval node, the runtime request and theapproval_requestcard to that turn's thread, run and provider turn. For nested subagents this is the top-level ancestor. This follows the OpenCode pattern in theopencode_child_approvalfixture.execCommandApproval/applyPatchApproval.subagent_v2_approvalwas recorded live on Codex 0.156.1 withgpt-6-luna, using the adapter's approval-required defaults (untrusted,readOnly). The parent spawns one subagent, which must write a file. The recording showsitem/commandExecution/requestApprovalarriving with the child'sthreadIdandturnId. The recorder gains this scenario.approve_next_runtime_requestcan take a shell snapshot while the request is pending. The fixture uses it to check the parent shell'spendingRuntimeRequest.Not attributed on the card
Neither the
approval_requestturn item norThreadPendingApprovalhas a field that names the requesting subagent. OpenCode's parent-side card does not say which subagent asked either. Labeling the card would need a new optional contract field plus web and mobile rendering, so it is left for a follow-up. For now the link exists only in the projection, asnode.parentNodeId.Kept: the child-thread question path from #13624
I did not remove the composer under
ProviderSubagentBaror theruntimeLayer.test.tschild-question test. OpenCode can still leave a pending request on a native child thread.requestOwnerStatereturns the child session's own state when the child has an active turn (OpenCodeAdapterV2.ts~2039).createChildTurngives that turn the child's app thread (~2313).permission.askedorquestion.askedfrom a child that is already running lands on the child thread. Only asks that arrive before the child relation is known go throughrouteChildRequestto the root. Those are whatopencode_child_approvalcovers.ask, perOpenCodeAdapterV2.test.ts.Codex itself can no longer do this.
request_user_inputreturns an error for non-root agents (core/src/tools/handlers/request_user_input.rs:69), and the async tools are only registered for the root (core/src/tools/spec_plan.rs:1124,:1147).request_user_inputdeny non-root agents (core/src/mcp_tool_call.rs:1675).core/src/mcp_skill_dependencies.rs:48). T3 ist3code_desktop.Claude, ACP (Grok and registry), Cursor and Pi build every request from the root turn's context. Moving OpenCode's child asks to the root would be a separate change.
Verification
vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts -t subagent_v2_approvalfails on the unfixed adapter: no pending request ever appears on the parent thread, and the respond step times out after 60 s. With the fix it passes.OrchestratorReplayFixtures.integration.test.ts(full suite),CodexReplayFixtures.integration.test.ts,OrchestratorReplayFixtures.contract.test.ts,Adapters/CodexAdapterV2.test.tsandruntimeLayer.test.ts. 255 passed.vp exec tsc --noEmit -p .inapps/server: no errors or warnings.vp run knip:checkpasses.vp linton the touched files shows only warnings already on the base branch: two unused declarations inCodexAdapterV2.tsandassertUserMessagesExclude.CODEX_HOME=<isolated copy> T3_CODEX_BIN=<codex 0.156.1> node scripts/record-codex-app-server-replay-fixture.ts --scenario subagent_v2_approvalin a disposable git repo. The recorder's scrubbing replaced the home path, hostname and installationId. The copiedCODEX_HOMEwas deleted afterwards.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code