Repository navigation
feat(checkpoint): harness driver for sandboxed agents that pauses them at checkpoints, with OpenCode on it - #4274
Conversation
fb7eda7 to
df0aacd
Compare
df0aacd to
34c1c9c
Compare
34c1c9c to
97c4044
Compare
|
Rewritten in place (2026-10-09): an explicit snapshot taken at commit is now the only restore point of a checkpointed sandbox. Pause is no longer a restore point: the lazy thaw, resume hooks, 🤖 Generated with Claude Code |
97c4044 to
d2fb533
Compare
2895d2b to
9cd8c0e
Compare
86b5c2a to
5c0c532
Compare
5c0c532 to
8646143
Compare
8646143 to
eff5f61
Compare
…t a failed session database snapshot in full - README: a "Checkpoint resume" section. A checkpoint pauses OpenCode, and OpenCode is relaunched on its saved session only after a crash restore or when a pause cannot be confirmed. The relaunch re-enters the saved user message through resume_opencode.py without a new message. The section also gives the sandbox requirements and how to run the real-runtime resume regression. - The error for a failed `opencode db VACUUM INTO` names the destination, return code, error type and the tail of stdout and stderr. A background execution returns both streams in stdout, so stderr alone often held only a status. From #4553 (b53e39f, 4d077ea), whose resume helper and the rest of whose changes are now in #4274 and #4778. Signed-off-by: Anish Mahishi <amahishi@nvidia.com> Co-authored-by: Pranav Prashant Thombre <pthombre@nvidia.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
07070d8 to
f99ad45
Compare
…t a failed session database snapshot in full - README: a "Checkpoint resume" section. A checkpoint pauses OpenCode, and OpenCode is relaunched on its saved session only after a crash restore or when a pause cannot be confirmed. The relaunch re-enters the saved user message through resume_opencode.py without a new message. The section also gives the sandbox requirements and how to run the real-runtime resume regression. - The error for a failed `opencode db VACUUM INTO` names the destination, return code, error type and the tail of stdout and stderr. A background execution returns both streams in stdout, so stderr alone often held only a status. From #4553 (b53e39f, 4d077ea), whose resume helper and the rest of whose changes are now in #4274 and #4778. Signed-off-by: Anish Mahishi <amahishi@nvidia.com> Co-authored-by: Pranav Prashant Thombre <pthombre@nvidia.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
After a restore the run continues in the fork the resources server names. Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the checkpoint can With checkpointing on, the run asks the resources server for the current sandbox before every launch: a boundary recorded before the first launch names a sandbox a restore may have replaced too. A relaunch continues OpenCode's session only when its database holds one that starts with the task; otherwise it starts over with the task in a fresh data folder, since --continue would open an empty session. OpenCode is not started while a checkpoint asks the run to park. A seed reply that says the resources server cannot capture its session, or that lends no sandbox, makes the run a restart: it is never interrupted, and after a crash it starts over. A /sandbox_access failure other than 404 fails the attempt instead of continuing in the known sandbox. The "return" boundary keeps the trajectory and observations a replacement returns with the stored result. The resume test now checks that the relaunch ran; each change has a regression test, and the real agent runs against the real SWE-bench server through a checkpoint, a crash and a restore. Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and continue saved sessions Stopping OpenCode for a checkpoint now goes through nemo_gym/_checkpoint/harness.py, which any black-box harness can use. The launch runs with NEMO_GYM_HARNESS_LAUNCH=<id>, which every process it starts inherits wherever it is re-parented. A stop signals the tagged processes, the processes an earlier pass found (matched on start time, so a reused PID is never hit) and all their descendants: SIGINT, then SIGKILL after 10 s, then SIGKILL again for anything still found. A process that outlives SIGKILL is finishing an uninterruptible system call with the kill pending, so the run parks and the stop reports it. A stop file keeps a launch that the stop beat from starting anything. The agent prints how each stop went. The previous stop matched `opencode run` in command lines. During the install it reached only the wrapper shell, which went on to start OpenCode unsignalled, so only the SIGKILL 45 s later stopped it; and it never reached the commands OpenCode runs in sessions of their own, which kept running while the run was parked and snapshotted. The saved-session check never found a session: OpenSandbox's background executions return stderr inside stdout, and `opencode export` prints "Exporting session: ..." there, so the export never parsed and the parse error read as "no session". Its commands now discard stderr, and every relaunch logs whether it continues and why. Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…HarnessAgent
The OpenCode agent and the e2e fake harness agent each carried their own copy of the checkpointed stage machine.
nemo_gym/_checkpoint/harness_run.py writes it once: SandboxedHarnessAgent.run_harness seeds, runs the harness as
one long wait step until it finishes, and verifies, recording a bookmark before each step. When a checkpoint asks
the run to park, the launch is stopped and the bookmark continues the harness step; after the resume, or in the
replacement attempt after a crash, the run asks the owner's /sandbox_access for the current sandbox and continues
the harness's saved session there.
The harness-specific parts are a HarnessAdapter: where the harness keeps its session (new_state), how to connect
to the lent sandbox, whether a saved session can be continued (resume_decision), how to launch, and how to verify.
OpenCode becomes OpenCodeAdapter. _launch_opencode takes the sandbox, the data folder and the continue flag
explicitly instead of through request state, the sandbox and result maps, and a rewritten request cookie jar;
responses() keeps working for callers that manage the sandbox themselves, such as the ViBench agent.
The bookmark is versioned ("v": 1) and validated before a continuation runs: seed, harness (cookies, verify mode,
the sandbox hint, the adapter's state, resume, and a record of every launch), verify (the outcome), return (the
result). Tests change in bookmark keys, and where they stubbed responses() to fake a launch they stub
_launch_opencode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
…ave a checkpointed sandbox to its owner - A launch that ended before the park request keeps its result. OpenSandbox can report a finished execution up to about 30 s late, and the run used to treat such a launch as interrupted and relaunch OpenCode on a finished session. The launch now records its root process when it starts and replaces the record with "ended" when its command returns; the first stop pass reports an ended launch instead of signalling it, and the run waits for its result. A launch that never started has no record and is still an interrupt. - A cancelled launch is killed: a shielded, time-limited SIGKILL pass over the launch and everything it started, before the cancellation goes on. A checkpointed run no longer stops its sandbox, so nothing else would. - A checkpointed run (checkpointing on, not a restart) never stops the sandbox: the owner may still export the session, and stopping it under the owner failed the next commit. Parked at a bookmark it leaves everything to the checkpoint; verified, the owner has ended the session; leaving without a verdict, it closes the session with an empty-body /close_session. Every other run (a restart, checkpointing off, a lender that cannot capture the session, the agent's own sandbox) stops the sandbox as before. This also drops the second stop after SWE-bench's verify, which logged "Failed to stop sandbox". - With checkpointing on, the OpenCode response carries ng_harness_launches: every launch of the rollout, whether it continued a saved session and why, how long it ran, and how it ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
…ts tool call test_an_episode_the_environment_reports_as_a_restart_does_not_hold_up_the_agent_or_resources_stage waited for the resources server to have one request in flight, which the seed also satisfies. A prepare that ran during the seed found no stage that knew the episode was a restart yet (the restart header comes with the seed reply), and the test failed on prepared.restarts() == set() about one run in three under xdist. It now waits for the tool call itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
…ency slot OpenCode took its per-worker concurrency slot before run_harness registered the run with the agent participant. Parked runs keep their slots, so a /run queued behind them was not a session: a checkpoint neither exported it, nor waited for it, nor reported it as a restart, while the controller had dispatched it and kept it in scope. The controller then waited out its drain deadline for replies that cannot arrive while the deployment is prepared. In a 32-node SWE-bench run, 33 of 110 in-flight rollouts were queued this way at one checkpoint. run_harness now takes the slot itself. With checkpointing on it registers the run, records the bookmark it continues from (the seed bookmark for a new run), waits for the slot inside a replay step, and records the bookmark again, so a slot that comes free while admission is closed parks the run before any task work. A checkpoint exports a queued run at that bookmark and does not wait for it. OpenCode passes its semaphore as the slot instead of wrapping the call in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
…t adding a user turn A relaunch after a checkpoint ran `opencode run --continue -- "Your previous session was interrupted. Continue working on the task from where you left off."`. Every checkpoint stops OpenCode, so every rollout running through a checkpoint got that message in its trajectory: in a 32-node SWE-bench run, all 641 calls that spanned a checkpoint grew the conversation by one extra message, in 56% of rollouts. The policy is then trained on turns conditioned on a message the task never had, only because the run was checkpointed. The relaunch now runs resume_opencode.py in the sandbox (from #4553's d431b1fb): it starts an authenticated loopback `opencode serve`, posts the session's last user message back with empty parts, which OpenCode 1.17.11 upserts and continues without a new message, checks the saved user turns are unchanged, and stops the server. resume_decision still decides whether there is a session to continue; the launch tag that the stop signals is inherited by the helper and its server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Anish Mahishi <amahishi@nvidia.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
…no model response yet Re-entering the saved user turn of a session that a checkpoint stopped before OpenCode's first model response ends the rollout without a single model call (rollout_failed:no_records): 14 rollouts in a 32-node SWE-bench run, 2 of which reached training as invalid rows. Such a session holds only the task, so nothing is lost by starting over: resume_decision now continues only a session that holds a completed assistant message, and otherwise the relaunch runs the task fresh in a new data folder, as for a session OpenCode never saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
… and relaunching it A checkpoint stopped OpenCode (SIGINT, then SIGKILL) and relaunched it on its saved session once the checkpoint opened, at every checkpoint. The relaunch costs time, drops the reply the model server held, and is where restored rollouts were seen to end without another model call. A run whose adapter pauses at checkpoints now sends SIGSTOP to every process of the launch, confirms each is stopped, records its bookmark and parks; once the checkpoint opens it sends SIGCONT and keeps waiting on the same launch. Nothing in the sandbox changes while it is paused, so the snapshot holds that moment, and a restore after a crash relaunches from the bookmark as before. A pause that cannot be confirmed, or a launch that cannot be continued, falls back to the stop; a run cancelled while paused kills the launch. - harness.py: HarnessLaunch.pause_command/continue_command and a pause file the launch waits on before it starts; run_until_park_requested takes a park callback and pauses, parks and continues. - harness_run.py: LaunchContext.park, the optional PausesAtCheckpoints adapter protocol, and LaunchRecord.pauses. - opencode_sandboxed_agent: pause_at_checkpoints (default true; false keeps the stop and relaunch). Validated in OpenSandbox with real OpenCode 1.17.11 (job 8077144): four pauses (mid-tool, waiting on a held reply, 120 s, mid-stream) left files, the session database and process CPU time unchanged; the same OpenCode process finished with no repeated model call; a snapshot taken while paused forked byte-identical and resumed through resume_opencode.py. Paused time counts toward OpenCode's 120 s bash timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
… interpreter After a crash restore, the relaunch ran `python3 resume_opencode.py`. In SWE-bench sandboxes the task's testbed environment comes first on PATH, and for old Django (1.11-3.2), scikit-learn (0.20-0.22) and astropy (1.3) it is Python 3.5 or 3.6. The helper's `from __future__ import annotations` is a SyntaxError there, so the relaunch exited after about two seconds without touching the session, and the rollout was graded as if the model had stopped at the checkpoint. The relaunch now selects the first interpreter that is 3.8 or newer (python3, /usr/bin/python3, /opt/miniconda3/bin/python3, /usr/local/bin/python3) and fails with a clear message when there is none. OpenCode and the commands the model runs keep the task's environment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Prashant Thombre <pthombre@nvidia.com>
f99ad45 to
b5d68b0
Compare
…ed nothing, and say why one failed
At checkpoints of a 16-node full-SWE run, 26 pauses of the OpenCode launch failed with empty output
("Could not pause harness launch X ()"), 19 of them at one checkpoint. Each fell back to stopping and relaunching
OpenCode, and the 5 rollouts poisoned in that run all started right after such a failure. The failures repeat
for the same tasks across runs, and the pause and continue scripts scan every process of the sandbox on each
pass, so a sandbox running many processes can use up the 30 s command budget before anything is printed. The
warning gave no exit code, error, duration, or rollout, so the logs could not tell.
- Pause and continue commands get 120 s instead of 30 s.
- A pause that printed nothing although it ended within its budget is run once more before the stop: the
script always prints its outcome. One that used up its budget is not retried.
- The pause failure warning names the rollout and gives the exit code, error type, stderr, duration, and
attempt. run_until_park_requested and LaunchContext take an optional label for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
… a pause finds its processes in a tree walk
A pause, continue, or stop found the launch's processes by reading the environment of every process in the
sandbox and walking parent links one /proc scan per tree level, on every check. Launches whose commands start
thousands of processes made that take minutes: in 16-node full-SWE runs 14 pauses were killed at their 120 s
budget ("exit -1, stderr 'signal: killed'"), all on a few tasks, each fell back to stopping and relaunching
OpenCode, and the poisoned rollouts of those runs all started there.
A checkpointed launch now uploads nemo_gym/agent_utils/process_supervisor.py and, where the sandbox has
Python 3.8+, runs under it: the supervisor gives the harness a process group of its own and adopts what it
orphans (PR_SET_CHILD_SUBREAPER), so every process of the launch is in its tree while it runs. The launch records
the supervisor's pid and start time, and the commands walk /proc/<pid>/task/*/children from it: one read per
process of the launch. Pause, continue, and stop keep their logic; only the search changes. Without such a
Python, when the upload fails or the supervisor cannot launch, and once the supervisor has exited, the tag scan
is used as before. The launch exits with the harness's own exit code from the supervisor's receipt.
Measured in real OpenSandbox sandboxes (swemer_v1 and swe_rebench images): 1,000 processes paused in 0.15 s and
2,400 nested ones in 0.24 s, detached and other-group processes included, against 26 s for 1,000 with the scan on
a compute node. The stop scenarios run both supervised and unsupervised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
…say when a launch is unsupervised In a 16-node run with supervised launches, the 3 pauses that still failed each found one process in an uninterruptible system call (PENDING <pid>:D) after the 5 s of checks; such a process takes the pending SIGSTOP when the call returns. While the only processes left are in state D, a pause now keeps checking for up to 30 s. A launch that should run under the process supervisor but does not (no Python 3.8+ in the sandbox, or it could not start) falls back to the /proc scan silently. Its pause now prints UNSUPERVISED, and the driver logs a warning naming the rollout, so a run shows how often the fallback is used. The pause and stop record the found processes with builtins only, and the pause only on its first check: a subshell per process on every check was slow for a launch of thousands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pranav Thombre <pthombre@nvidia.com>
What changed and why
An agent harness such as OpenCode runs a whole rollout as one long command in a sandbox that its resources server lends. This PR makes such an agent checkpointable through a reusable driver, and puts OpenCode on it. At a checkpoint the harness is paused in place, and the same process continues once the checkpoint opens. After a crash, the harness is relaunched in the restored sandbox and continues its saved session.
What this PR adds:
SandboxedHarnessAgentandHarnessAdapter(nemo_gym/_checkpoint/harness_run.py). The driver owns the checkpointed stage machine of a legacy/run: seed, the harness as one longwaitstep, verify, and return./sandbox_accesswhich sandbox is current. After a restore, that is a fork under a new ID.new_state);resume_decision);Pausing a harness at a checkpoint (
nemo_gym/_checkpoint/harness.py, optional adapter protocolPausesAtCheckpoints). When a checkpoint asks the run to park (LegacyRun.wait_park_requested(), feat(checkpoint): ask a long-running agent harness to park when a checkpoint opens #4222):SIGSTOPgoes to every process of the launch, and the pause is confirmed once each one is stopped (stateT) or dead;SIGCONTgoes to the same processes, and the run keeps waiting on the same launch.Nothing in the sandbox changes while it is paused, so the owner's snapshot (feat(sandbox): sandbox owner base for resources servers, with SWE-bench on it #4576) holds that moment. A held model reply is delivered when the checkpoint opens and read on continue. OpenCode opts in with
pause_at_checkpoints: true, the default.A pause or continue command gets 120 s. A pause checks for 5 s that every process stopped, and for up to 30 s while the only ones left are in an uninterruptible system call (state
D, such as a write to a slow disk), which take the pendingSIGSTOPwhen the call returns. A pause that printed nothing although it ended within its budget is run once more. A pause that fails is logged with the rollout, the exit code, the error, how long it took and the attempt.Stopping a harness, the fallback and the path for adapters that do not pause. A stop sends SIGINT, then SIGKILL after 10 s, then SIGKILL again to anything still found; the run relaunches after the resume. The run falls back to the stop when:
Finding every process of a launch. A checkpointed launch uploads
nemo_gym/agent_utils/process_supervisor.pyand, where the sandbox has Python 3.8+ (chosen like the resume helper's interpreter), runs under it.PR_SET_CHILD_SUBREAPER). While it runs, every process of the launch is in its tree: the installer, OpenCode, the commands the model runs in sessions of their own, and background processes they detach./proc/<pid>/task/*/childrenfrom it: one read per process of the launch, never every process of the sandbox. Launches whose commands start thousands of processes made the full scan take minutes, past the command budget (see Rollout evidence).NEMO_GYM_HARNESS_LAUNCH=<id>in its environment, which every process it starts inherits wherever it is re-parented. A pause or stop then acts on the tagged processes, on those an earlier pass found (by PID and start time), and on all their descendants. A pause of a launch that should be supervised but is not logs a warning naming the rollout.A finished launch keeps its result. OpenSandbox can report a finished execution up to about 30 s late. The launch writes
endedinto its record when its command returns, so a park request in that window keeps the result instead of pausing or relaunching.A cancelled launch is killed, shielded and time-limited, before the cancellation goes on. A paused one is killed too; SIGKILL ends a stopped process.
OpenCode becomes
OpenCodeAdapter. OpenCode keeps its session in SQLite under a per-rolloutXDG_DATA_HOMEon the sandbox's root filesystem, so the owner's snapshot carries it into a fork.A relaunch re-enters the saved user turn without adding a new one (from fix(checkpoint): wake sandbox command polls while a harness launch is stopped #4553, co-authored by Anish Mahishi).
resume_opencode.pyruns in the sandbox:opencode serve;A checkpoint therefore never adds a message the task did not have.
The helper runs on a Python 3.8+ interpreter, chosen from
python3,/usr/bin/python3,/opt/miniconda3/bin/python3and/usr/local/bin/python3; the relaunch fails with a clear message if none exists. A task image's own environment can come first onPATHand be older: SWE-bench's testbed is Python 3.5 or 3.6 for old Django, scikit-learn and astropy, where the helper'sfrom __future__ import annotationsis a SyntaxError. OpenCode and the commands the model runs keep the task's environment.A relaunch continues only a session that can be continued. The session must start with the task and hold a completed model response. Otherwise the relaunch starts over with the task in a fresh data folder.
_launch_opencodetakes the sandbox, the data folder and the resume flag explicitly.responses()keeps working for callers that manage the sandbox themselves, such as the ViBench agent.The run is registered before it waits for a concurrency slot. Parked runs keep their slots. A
/runqueued behind them is now registered at its seed bookmark, so a checkpoint exports it instead of waiting for it. Previously a 32-node run waited out a 1200 s drain on queued runs.The owner's sandbox is the owner's. A checkpointed run (checkpointing on, not a restart) never stops the sandbox, because the owner may still export the session.
POST /close_sessionwith an empty body, feat(sandbox): sandbox owner base for resources servers, with SWE-bench on it #4576).Runs that nothing can save are restarts. When the seed reply says the resources server cannot capture the session (
x-ng-checkpoint-restart), or no sandbox is lent, the run is marked a restart: it is never parked, and it starts over from its input after a crash.A versioned bookmark. The format is
{"v": 1, "next": "seed" | "harness" | "verify" | "return", ...}, validated before a continuation runs.resume, and a record of every launch.verifygrades again without launching; one atreturnreturns the stored result.A launch log. With checkpointing on, the OpenCode response carries
ng_harness_launches: every launch of the rollout, whether it continued a saved session and why, how long it ran, how it ended (finished,interrupted,failed), and how many checkpoints paused it (pauses).The second user. The e2e
fake_harness_agent(feat(checkpoint): ask a long-running agent harness to park when a checkpoint opens #4222) runs on the driver with a fake adapter, so the process-level harness scenarios exercise the code OpenCode runs on.SIGSTOP, confirmed stoppedSIGCONT: the same process continuesresume_decision, thenresume_opencode.pyHow it works
Where this PR sits in the overall flow
The highlighted parts are what this PR adds.
flowchart LR C["Controller<br/>NeMo RL"] CO["Coordination<br/>prepare, commit, restore, resume"] subgraph G["One participant per Gym server"] A["SandboxedHarnessAgent<br/>bookmark, park, pause or relaunch,<br/>cleanup rule"] AD["HarnessAdapter<br/>OpenCodeAdapter"] R["Sandbox owner<br/>SWE-bench (#4576)"] end H["Harness in the lent sandbox<br/>supervised, tagged launch"] C --> CO --> G A --> AD AD -- "launch; pause and continue,<br/>or stop and relaunch" --> H A -- "seed, /sandbox_access, verify, close" --> R R -- "snapshot at commit, fork at restore" --> H classDef this fill:#fde68a,stroke:#b45309,stroke-width:2px,color:#1f2937 class A,AD thisThe run's stages
stateDiagram-v2 [*] --> seed seed --> harness: lent sandbox, cookies, verify mode<br/>(restart if the owner cannot capture it) harness --> harness: park requested: pause, bookmark (resume := true),<br/>continue on open (or stop and relaunch) harness --> verify: launch finished (or had ended before the park) verify --> return: verify reply recorded return --> [*] note right of harness Before each launch: bookmark (parks here during a checkpoint), /sandbox_access, resume_decision when relaunching end noteA checkpoint pauses the launch, and a crash relaunches it
sequenceDiagram participant C as Checkpoint participant D as Driver participant O as Owner (SWE-bench) participant H as OpenCode in the sandbox D->>D: bookmark {next: harness, resume: false, state: {data_home}} D->>O: /sandbox_access O-->>D: sandbox S1, workdir D->>H: upload process_supervisor.py D->>H: env NEMO_GYM_HARNESS_LAUNCH=id bash -c 'python3 process_supervisor.py ... -- bash -c "... opencode run -- task"' C->>D: park requested D->>H: SIGSTOP every process in the supervisor's tree, until all are stopped D->>D: bookmark {resume: true, launches: [interrupted, pauses: 1]} (parks) C->>O: commit: snapshot S1 (nothing in it changes while paused) C->>D: resume D->>H: SIGCONT: the same OpenCode continues, and reads the held model reply Note over C,O: later: crash, and the restore forks the snapshot into S2 D->>O: /sandbox_access (replacement attempt) O-->>D: S2 D->>H: resume_decision: the session starts with the task and has a model response D->>H: resume_opencode.py re-enters the saved user turn (no new message) H-->>D: finished D->>O: /verify (the owner ends the session; the agent never stops S2)Leaving the sandbox
POST /close_session {}; release the clientWhere this sits in the stack
Stack #4578, on
ananthsub/partial-ckpt-fault-tests:release, the snapshot index, the OpenSandbox leaseIssue
Part of #3284 (sandboxed blackbox agents) and #3269 (OpenCode). Later harnesses (Claude Code through
harness_agent, Codex, Pi) implement only an adapter, and opt in to pausing withpauses_at_checkpoints().Validation
On this head (
e832faaf5): this PR now ends with three commits that run the launch under the process supervisor (ecb45e063,b7be6aeb5,e832faaf5). The stack above it was replayed onto them; its top's tree is unchanged from the tested state. In therl-gym.nightlycontainer on Slurm (job 8281677), on every level of the stack:ruff checkandruff format --checkclean on the changed files at every level.test_checkpoint_harness.pynow has 21 test functions. The stop scenarios (installing, a command in its own session, a detached process, an untagged child, a harness that ignores SIGINT) run both supervised and unsupervised. New tests: the supervised launch returns the command's output and exit code; a supervised pause stops every process, including a detached daemon and one in another session, and continue resumes them; a launch without an upload runs unsupervised; a launch that cannot run the supervisor still pauses and logs that it is unsupervised; pause and continue get the 120 s budget; an empty pause is retried once; a pause that keeps printing nothing reports why and stops the launch.Earlier heads (
f99ad450e, the fix commit rebased with the stack ontoananthsub/partial-ckpt-fault-testsat02030c3f8; tests below ran on the pre-rebase head07070d83fand again on the rebased stack, in therl-gym.nightlycontainer on Slurm) (f99ad450e, the fix commit rebased with the stack ontoananthsub/partial-ckpt-fault-testsat02030c3f8; tests below ran on the pre-rebase head07070d83fand again on the rebased stack, in therl-gym.nightlycontainer on Slurm):test_the_resume_python_selection_skips_a_task_python_older_than_3_8, and the relaunch test checks that the helper is started through the selection.c67f810d5, job 8085408) passed 2297 tests acrossresources_servers/swebench/tests,responses_api_agents/opencode_sandboxed_agent/tests, and everytests/unit_testsfile for checkpoint, sandbox, OpenSandbox and token capture. The new commit changes only the relaunch command.Signed-off-by.tests/unit_tests/test_checkpoint_harness.py, 14 test functions with real bash,/procand signals:tests/unit_tests/test_checkpoint_harness_run.py, 20 test functions through the real agent participant:OpenCode:
test_resume_opencode.pyand the OpenCode checkpoint tests cover:Live, in OpenSandbox (job 8077144): real OpenCode 1.17.11 on a SWE-bench image, launched as Gym launches it, paused 4 times: mid-tool, waiting on a held reply, 120 s, and mid-stream.
opencode.db,-wal,-shmand every process's CPU time were unchanged while paused.resume_opencode.pycontinued from it.Rollout evidence
Pausing with the supervised launch: 16-node full-SWE runs (4 training and 12 generation nodes, 2,048 rollouts in flight on 5 SWE task routes, a checkpoint attempt every 300 s, 2 h each):
/procscan/procscan, 120 s budgetexit -1, stderr 'signal: killed', about 150 s), all on a few tasksPENDING <pid>:D) after 6.5–9 se832faaf5) lets a pause wait up to 30 s for processes in disk I/O, the cause of its 3 failures, and logs a launch that falls back to the scan.32-node GRPO runs: nano-3.5 SWE policy, SWE-bench, 64 prompts × 2, 5 steps.
c670de9cc, nothing else):run_complete=True;Two earlier runs used this PR plus a cut-epoch change in the model gate and checkpoint-timing logs, neither of which is in the stack:
run_complete=True;run_complete=True. Of the 59 OpenCode runs paused in the snapshot:Relaunch after a crash restore, fixed in this PR's last commit. Before it, some relaunched OpenCode runs exited about 2 s after starting and made no model call: 18 of 55 in the restore above. All 18 were Python 3.5/3.6 SWE-bench tasks, and all 37 that continued had newer Pythons. The relaunch ran
python3 resume_opencode.py,python3was the task's testbed interpreter, and the helper failed withSyntaxError: future feature annotations is not definedbefore touching the session. The rollout was then graded as if the model had stopped at the checkpoint.django__django-10914, testbed Python 3.6.13). Withpython3the helper exited 1 after 2.2 s with that SyntaxError and made 0 model requests. With the fix it exited 0 after 28.6 s, made 2 model requests, and the session ran tostop.run_complete=True;Compatibility
pause_at_checkpoints: falserestores stop and relaunch.ng_harness_launches, whose records now includepauses./tmp/nemo-gym-process-supervisor.pyand runs under it when the sandbox has Python 3.8+. When the harness exits, the supervisor ends what the harness left running in its own process group. Detached processes in other sessions stay until the sandbox is stopped, as before."v": 1). The earlier OpenCode boundary keys (next: opencode,sandbox_handle,data_home,continue) are not read; nothing that wrote them has merged.nemo_gym._checkpoint.harness_run:SandboxedHarnessAgent,HarnessAdapter,PausesAtCheckpoints,LaunchContext(withpark),LentSandbox,LaunchRecord;nemo_gym._checkpoint.harness, whoserun_until_park_requestedandLaunchContexttake an optionallabel(the rollout ID) for pause diagnostics.output_token_policy: remaining_context, with a context window equal to the served model's max length.🤖 Generated with Claude Code