You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A validation_loop retry re-runs the agent as a fresh process in the same sandbox: ClearIterationArtifacts sweeps stray processes, deletes the iteration's output and the runtime's session store, and iteration 2 starts a new session with the previous validation failure injected into the prompt (#1050, #6494). The agent keeps the working tree but loses everything else it knew: what it tried, why, and what it already ruled out. The process kill between iterations is also what forces the stray-process sweep from #6753 into the validation loop, which #6961 objects to.
#6961 proposes the alternative: do not terminate the agent process; validate when the agent says it is done. #6959 already built the machinery for that shape, for work-item updates: one sandbox, and one session per steerable rt.Run, with the runner talking to the live session at result boundaries.
Shipped: session-id capture for all three runtimes in RunMetrics.SessionID (Claude system/initsession_id, Codex thread.startedthread_id, pi session id). Under Steerable, the runner is notified at the runtime's result boundary before deciding to Steer or Settle (steerTurnEndHandler in internal/cli/steer.go; coalescing channel, depth 16): Claude reports per turn, pi per prompt (rpc mode), Codex only at process exit. Claude and pi stay alive on stdin between turns; Codex waits for work after a natural exit and resumes the same thread. Today a steer to an idle Codex queue still fires the interrupt sweep (codexSteerQueue.enqueue checks only that the thread id is known); that is a feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 defect being fixed there, and this issue depends on the fix.
Not shipped: anything on the validation path. Output extraction, transcript extraction and the validation script still run after rt.Run returns, once per iteration (run.go steps 9b–9e), and ClearIterationArtifacts still deletes pi's --session-dir and Codex's $CODEX_HOME/sessions. For Claude it runs rm -rf <config>/*.jsonl (top level only) while the session lives under <config>/projects/<cwd>/<id>.jsonl, so if Claude stores the session under projects/ (inferred from our clear and extraction code, not yet confirmed from a captured run) it survives by accident and iteration 2's transcript extraction (a recursive find) also picks up iteration 1's file.
Proposed change
Deliver validation feedback as the next turn of the live session instead of killing the agent and starting a new one.
Runner loop. When the harness has validation_loop, run the agent Steerable and, at each turn end: download the output, run the validation script on the host, then either Steer the failure text as the next user message (feedback_mode: append semantics) or Settle on pass. Clear only output/ between turns, never the session store. max_iterations remains the cap on turns.
Per runtime.
Claude Code and pi: the live session takes the next message on the existing mailbox path; no process exit, no kill.
Codex: no live channel, so the runner's steer after the natural exit becomes codex exec <flags> resume <thread_id> - on the same thread (0.152.1; flags before resume). Needs the idle-aware queue fix in feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 so the steer does not fire the interrupt sweep.
Any runtime without Steerer (the type assertion already exists): today's fresh-process retry with the feedback in the prompt, and a line in the run summary saying so.
Interaction with the steer watcher (#6959), so the two paths do not collide.
Steerable is currently set only when the harness enables steer:; the validation path needs its own switch that also sets it, so a harness with validation_loop and steer off still gets the live retry.
A validation-feedback SteerMessage carries FollowUpRunID 0 ("not from a run"); it must not enter the consumed-run marker and must not count against steer.max_steers, which budgets follow-up runs. max_iterations is its cap.
A remaining-time floor like the watcher's MinRemaining is needed here too: today that floor lives only inside the follow-up-run watcher, which is not created when steer: is off, so the validation path must carry its own check. A retry with less than the floor left ends the run instead of starting a turn the exec timeout would cut off.
Feedback text must never reach the agent through argv (it is the most attacker-influenced string in the run). Today the mailbox seed and the Codex prompt are written with printf '%s' <quoted> inside the sh -c string the runner execs, so the text is in that intermediate shell's argv and in OpenShell's host-side command preview; the agent CLI itself reads stdin. Fixing that (plumb the exec request's stdin field through sandbox.ExecContext, or upload-then-append) is a feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 follow-up this issue inherits. Note the field exists on the gRPC request (ExecSandboxRequest.stdin is a one-shot bytes payload) but the openshell sandbox exec CLI fullsend shells out to does not expose it, so the fix means the gRPC path or the upload path, not a new CLI flag.
Metrics: per-turn usage as a steered session reports it today.
Transcripts. Iteration 1's session file is attributed to iteration 1 once, not re-extracted on the next turn.
Out of scope: the stray-process sweep itself. With no kill between validation turns it leaves the validation loop; it remains for the Codex mid-turn steer interrupt. NVIDIA/OpenShell#3159 was closed (OpenShell will not add per-exec termination), so any replacement is fullsend-side and is discussed on #6961. Two source facts constrain that replacement (see research/openshell-sandbox-process-model.md in the workspace): an exec'd command is a sibling of the sandbox main process under the PID 1 supervisor, and on the --no-tty path it inherits the supervisor's process group, so a group kill from inside the sandbox is only safe if the launched shell first creates its own session.
Sequencing. The pi launch command builder in internal/runtime/pi_run.go is rewritten by both #6754 (plugins under one key: JITI_* env pinning, -e extension list) and #6959 (--mode rpc --session-id under Steerable), and those two conflict with each other on that file today. This issue lands after both, on top of whichever merges second. #6754 also documents its extension tree-hash and hook-digest guards in terms of "between iterations" (docs/contributing/runtime-implementation.md, and the refusal message in pi_run.go): under a live-session retry pi is not restarted between validation turns, so those per-process guards run once per session rather than once per iteration and the "previous iteration's process rewrites the tree before the next import" window disappears for pi (there is no second import); the wording there needs updating when this lands, and the Codex resume path, which is a new process per turn, keeps the per-process guards as they are. No MCP server, remote or sidecar (#6961 floated one): it costs context per turn, VM resources and a second control surface next to the harness. The agent's early signal is the existing in-sandbox tool fullsend-check-output (#2777 for the code agent); the turn boundary is the contract the runner relies on, and the host-side validation script and post-script keep the schema and outcome checks.
Validation criteria
A two-iteration code or fix run on each runtime shows the second turn continuing the first session: same RunMetrics.SessionID, the agent's next message references the validation failure text, and the iteration-1 transcript is extracted exactly once.
No process kill and no stray-process sweep between validation turns on any runtime (the sweep log line is absent); pi and Codex session stores survive, output/ does not.
With steer.enabled: false and validation_loop set, the live retry still happens; with both on, a validation turn does not consume a follow-up run or decrement max_steers.
Forcing the fallback (runtime without Steerer, or session file removed) produces the fresh-process retry with the feedback in the prompt and a visible "live retry unavailable" line.
What happened
A
validation_loopretry re-runs the agent as a fresh process in the same sandbox:ClearIterationArtifactssweeps stray processes, deletes the iteration's output and the runtime's session store, and iteration 2 starts a new session with the previous validation failure injected into the prompt (#1050, #6494). The agent keeps the working tree but loses everything else it knew: what it tried, why, and what it already ruled out. The process kill between iterations is also what forces the stray-process sweep from #6753 into the validation loop, which #6961 objects to.#6961 proposes the alternative: do not terminate the agent process; validate when the agent says it is done. #6959 already built the machinery for that shape, for work-item updates: one sandbox, and one session per steerable
rt.Run, with the runner talking to the live session at result boundaries.What #6959 shipped and what it did not:
RunMetrics.SessionID(Claudesystem/initsession_id, Codexthread.startedthread_id, pi session id). UnderSteerable, the runner is notified at the runtime's result boundary before deciding toSteerorSettle(steerTurnEndHandlerininternal/cli/steer.go; coalescing channel, depth 16): Claude reports per turn, pi per prompt (rpc mode), Codex only at process exit. Claude and pi stay alive on stdin between turns; Codex waits for work after a natural exit and resumes the same thread. Today a steer to an idle Codex queue still fires the interrupt sweep (codexSteerQueue.enqueuechecks only that the thread id is known); that is a feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 defect being fixed there, and this issue depends on the fix.rt.Runreturns, once per iteration (run.go steps 9b–9e), andClearIterationArtifactsstill deletes pi's--session-dirand Codex's$CODEX_HOME/sessions. For Claude it runsrm -rf <config>/*.jsonl(top level only) while the session lives under<config>/projects/<cwd>/<id>.jsonl, so if Claude stores the session underprojects/(inferred from our clear and extraction code, not yet confirmed from a captured run) it survives by accident and iteration 2's transcript extraction (a recursive find) also picks up iteration 1's file.Proposed change
Deliver validation feedback as the next turn of the live session instead of killing the agent and starting a new one.
Runner loop. When the harness has
validation_loop, run the agentSteerableand, at each turn end: download the output, run the validation script on the host, then eitherSteerthe failure text as the next user message (feedback_mode: appendsemantics) orSettleon pass. Clear onlyoutput/between turns, never the session store.max_iterationsremains the cap on turns.Per runtime.
codex exec <flags> resume <thread_id> -on the same thread (0.152.1; flags beforeresume). Needs the idle-aware queue fix in feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 so the steer does not fire the interrupt sweep.Steerer(the type assertion already exists): today's fresh-process retry with the feedback in the prompt, and a line in the run summary saying so.Interaction with the steer watcher (#6959), so the two paths do not collide.
Steerableis currently set only when the harness enablessteer:; the validation path needs its own switch that also sets it, so a harness withvalidation_loopand steer off still gets the live retry.SteerMessagecarriesFollowUpRunID0 ("not from a run"); it must not enter the consumed-run marker and must not count againststeer.max_steers, which budgets follow-up runs.max_iterationsis its cap.MinRemainingis needed here too: today that floor lives only inside the follow-up-run watcher, which is not created whensteer:is off, so the validation path must carry its own check. A retry with less than the floor left ends the run instead of starting a turn the exec timeout would cut off.printf '%s' <quoted>inside thesh -cstring the runner execs, so the text is in that intermediate shell's argv and in OpenShell's host-side command preview; the agent CLI itself reads stdin. Fixing that (plumb the exec request's stdin field throughsandbox.ExecContext, or upload-then-append) is a feat(dispatch)!: steer the agent run already in flight on work-item updates #6959 follow-up this issue inherits. Note the field exists on the gRPC request (ExecSandboxRequest.stdinis a one-shotbytespayload) but theopenshell sandbox execCLI fullsend shells out to does not expose it, so the fix means the gRPC path or the upload path, not a new CLI flag.Transcripts. Iteration 1's session file is attributed to iteration 1 once, not re-extracted on the next turn.
Out of scope: the stray-process sweep itself. With no kill between validation turns it leaves the validation loop; it remains for the Codex mid-turn steer interrupt. NVIDIA/OpenShell#3159 was closed (OpenShell will not add per-exec termination), so any replacement is fullsend-side and is discussed on #6961. Two source facts constrain that replacement (see
research/openshell-sandbox-process-model.mdin the workspace): an exec'd command is a sibling of the sandbox main process under the PID 1 supervisor, and on the--no-ttypath it inherits the supervisor's process group, so a group kill from inside the sandbox is only safe if the launched shell first creates its own session.Sequencing. The pi launch command builder in
internal/runtime/pi_run.gois rewritten by both #6754 (plugins under one key:JITI_*env pinning,-eextension list) and #6959 (--mode rpc --session-idunderSteerable), and those two conflict with each other on that file today. This issue lands after both, on top of whichever merges second. #6754 also documents its extension tree-hash and hook-digest guards in terms of "between iterations" (docs/contributing/runtime-implementation.md, and the refusal message inpi_run.go): under a live-session retry pi is not restarted between validation turns, so those per-process guards run once per session rather than once per iteration and the "previous iteration's process rewrites the tree before the next import" window disappears for pi (there is no second import); the wording there needs updating when this lands, and the Codex resume path, which is a new process per turn, keeps the per-process guards as they are. No MCP server, remote or sidecar (#6961 floated one): it costs context per turn, VM resources and a second control surface next to the harness. The agent's early signal is the existing in-sandbox toolfullsend-check-output(#2777 for the code agent); the turn boundary is the contract the runner relies on, and the host-side validation script and post-script keep the schema and outcome checks.Validation criteria
codeorfixrun on each runtime shows the second turn continuing the first session: sameRunMetrics.SessionID, the agent's next message references the validation failure text, and the iteration-1 transcript is extracted exactly once.output/does not.steer.enabled: falseandvalidation_loopset, the live retry still happens; with both on, a validation turn does not consume a follow-up run or decrementmax_steers.Steerer, or session file removed) produces the fresh-process retry with the feedback in the prompt and a visible "live retry unavailable" line.Related
sandbox execcommand and everything it spawned NVIDIA/OpenShell#3159 asks for the primitive that would replace its remaining use.