Repository navigation
feat(opencode): support multiple activations per session - #4747
Conversation
There was a problem hiding this comment.
Thanks for this, @lafrancef. The ledger and per-activation rearm design is clear, and the README and fern updates make the protocol easy to follow. We ran the PR's tests locally (test_agent_activations: 12 passed; opencode_agent tests: 405 passed, 42 skipped on macOS) and reproduced the inline items with scratch probes against both base and head.
The main item is in opencode_agent/sandbox.py: the single-turn path's failure-time evidence changed, which the description says is preserved. The rest are smaller edges in the new continuation path, test coverage for the shared SandboxSession changes, and docs and contract clarity.
PR hygiene:
- Could you link the tracking issue, or say why none is needed (AGENTS.md "Pull Requests")? Naming the intended consumer of the continuation fields would also help, since nothing in the repo sends
continuationyet. - Could you list the exact local checks you ran (pytest targets,
pre-commit run --all-files) next to the live-smoke description? - CI still needs a vetter's
/ok to test.
|
Not blocking for this PR, but a suggestion to make the next harness cheaper to add (codex, hermes, openclaw and pi all use Only three things look OpenCode-specific: the resume flag, where native state lives, and how to find new messages in the export. The rest is generic but lives in OpenCode behind ~20
Could we split session state from per-activation state and move the generic part into @dataclass
class Activation: # replaced each activation, no field-by-field reset
id: int
log: str = ""
observations: AgentObservationBundle | None = None
trajectory: TrajectoryRecord | None = None
runtime_info: HarnessProcessInfo | None = None
@dataclass
class ContinuationState: # retained across activations
home_dir: str
native_id: str | None = None
seen_message_ids: set[str] = field(default_factory=set)
evidence: EvidenceAccumulator = field(default_factory=EvidenceAccumulator) # merge + ID scopingTwo related ideas:
A follow-up PR is fine too. |
|
Ack, agreed splitting off the generic part makes sense, will do once we add multiple-activation support to a second harness |
|
/ok to test 9d4caf5 |
|
🌿 Preview your docs: https://nvidia-preview-pr-4747-b3cc21c46cd4.docs.buildwithfern.com/nemo/gym |
|
/ok to test 2d711fd |
|
/ok to test 221c064 |
… root Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
|
/ok to test ea17c8b |
…bump Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
|
/ok to test b3cc21c |
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39649856807 |
NVIDIA-NeMo#4747's multi-activation tests expected process_supervisor.py, while this branch uploads the supervisor without the suffix. Both now use SUPERVISOR_FILE, so the OpenCode upload-delay test injects its delay again. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
NVIDIA-NeMo#4805) * fix(agents): hide the sandbox supervisor from pkill python Models clean up their own work with `pkill -f python3`, `pkill python3`, `killall python3` and similar. In the task sandbox those matched Gym's supervisor (`python3 -I .../process_supervisor.py`) and the harness runner under it, so the supervisor stopped the agent CLI and the episode ended as "Agent response status: failed" with reward 0. On long Terminal-Bench-Science runs this ended Codex trials partway through, while under a harness whose agent CLI is node or a native binary the same commands are harmless. The launch shell now links `<session>/bin/gym-supervisor` to the binary behind the adapter's interpreter (`/proc/self/exe`, past venv links and shims) and execs the supervisor through it, from `process_supervisor` without a `.py` suffix. A runner that opts in with `SandboxCommand.hide_runner` runs the same way as `bin/gym-runner`. Codex and Pi opt in and upload their runner as `sandbox_runner`. Neither process name nor command line mentions python, so none of `pkill -f python3|python|"python3 -"|"\.py"`, `pkill python3|python` or `killall python3` matches. Hermes keeps its venv interpreter for its runner (the runner is the agent); its supervisor is hidden. OpenCode and OpenClaw get the hidden supervisor too; their test fakes now match the new file name. If the link cannot be made or cannot load the supervisor, both processes keep the given interpreter. Gym's stop path signals the recorded PID and is unchanged. Tests: the shared supervisor tests add a pgrep-based identity test that runs the patterns as pkill/killall would match them, a link fallback case and a check that a venv runner is left alone. Codex, Pi, Hermes, OpenCode and OpenClaw tests follow the new file names. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * fix(agents): hide the OpenCode and OpenClaw runners from pkill python The OpenCode and OpenClaw adapters launch the same shared supervisor and a standard-library runner (`python3 -I <session>/sandbox_runner.py`) above their CLI, so a task agent's `pkill -f python3` or `killall python3` stops the runner and ends the episode just as it did for Codex and Pi. The supervisor is already hidden; opt the runners in too: upload them as `sandbox_runner` and set `SandboxCommand.hide_runner`, so they run as `bin/gym-runner`. OpenCode's post-cleanup transcript snapshot uses the renamed file. Tests: each adapter's exec-transport suite gains a test that launches the real supervisor and runner and checks that neither process name nor command line mentions python; the session fakes expect the new runner name. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * fix(agents): don't recreate a retired session directory from a late launch A launch delayed before it links the interpreter used mkdir -p for the session's bin directory, so it recreated the session directory after close had removed it. Create only bin, so the link fails once the session is gone. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * docs(agents): scope the pkill claim to runners that opt in A harness argv that keeps its own python, such as Hermes's or Stirrup's, still appears in the supervisor's command line, so pkill -f python can match the supervisor there. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * test(agents): run the launch quoting test off Linux again Only the interpreter links depend on /proc. Elsewhere the launch falls back to the given interpreter, so expect that instead of skipping the test. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * refactor(agents): rename hide_runner to stdlib_runner and reject an argv it can't hide The opt-in is a claim about the runner: it must need nothing beyond the standard library, because the link runs the base interpreter outside any virtual environment. Name it after that contract. Setting it for an argv that doesn't start with the adapter's python used to do nothing silently; SandboxCommand and supervised_launch_command now raise ValueError. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * test(kilocode): match the supervisor uploaded without a .py suffix Kilo landed on main after this branch renamed the uploaded supervisor to process_supervisor; its fake sandboxes still looked for process_supervisor.py. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * docs(agents): describe the supervisor links and stdlib_runner in the harness guide Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * test(agents): match the uploaded supervisor name in the activation tests NVIDIA-NeMo#4747's multi-activation tests expected process_supervisor.py, while this branch uploads the supervisor without the suffix. Both now use SUPERVISOR_FILE, so the OpenCode upload-delay test injects its delay again. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> * test(nooa): match the supervisor upload without the .py suffix NOOA's sandbox runner landed on main after this branch renamed the uploaded supervisor to process_supervisor, so its tests still looked for process_supervisor.py and never routed the launch. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> --------- Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com> Co-authored-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Add multi-turn OpenCode continuation for SWE-Together's interactive coding episodes. Seed with
continuation: {mode: resume_harness_session}, then append user input through ordinary/v1/responsesrequests using the session cookie and existing 1-basedx-ng-agent-activationheader. Usemetadata.agent_timeout_secfor each activation's budget. The adapter retains its saved session and HOME between calls.Each activation has its own invocation ID and status, with
harness_session_idrecorded as context. Trajectory turns carry the activation ID, and the plugin forwards activation-scopedX-Session-Idvalues for root and child invocations. Responses expose new output, usage and observations, withharness_session_idalso in metadata; close returns the accumulated evidence. Requested in-harness tool observations useharness_tool_calls.The shared ledger joins identical retries, rejects conflicting or out-of-order inputs, and fences execution during close. A final negative termination receipt fails the activation and skips verification; close releases the borrowed connection and returns
cleanup_confirmed: false. Connection release failures remain retryable. Validation runs before consuming an index. Deadlines cover preparation, queueing and staging; expiry before the first launch allows a later activation to start. Retry guarantees remain process-local.Continuation timeouts fail on explicit OpenCode error events, including after partial output. When close recovers delayed artifacts, it replaces that activation's fallback evidence while preserving its original outcome and earlier activations.
Existing single-turn response metadata, model headers and close behavior are preserved. Unknown cleanup confirmation is omitted. SandboxSession keeps finalization private and supports explicit rearming after confirmed cleanup with fresh launch files. The adapter README and integration guides document the protocol and directory ownership.
Live validation used OpenCode 1.17.11 and gpt-4.1-mini on borrowed OpenSandbox CPU sandboxes. Single-turn and two-activation file-repair smokes passed their external verifiers, identical replay checks, existing observability conformance checks and rollout health checks. The second activation resumed the same OpenCode session and recalled a label from the first. Agent cleanup disconnected the borrowed connection, leaving task files available for verification; the sandbox owner then destroyed the allocations.
No separate tracking issue is needed: this focused change supplies multi-turn continuation for the SWE-Together harness integration.
The terminology-only follow-up renames the protocol, OpenCode helpers, docs and tests consistently. Its focused checks passed (155 tests):
Whole-tree pre-commit passed; its guardwords check skipped because the private catalog was unavailable. Full local CI and live smokes were not repeated for this renaming; GitHub CI is requested for the final commit.
Validation before the terminology-only follow-up (
02aeba2b):The focused regression/checker selection passed: 993 tests, 42 platform skips. Whole-tree pre-commit passed. Fresh single-turn and two-activation live smokes passed verification, replay, observability conformance, rollout health and cleanup.
Linux core passed 8877 tests (28 skips), followed by 921 sandbox tests (5 skips). Combined coverage passed the configured 95% gate at 95.09%.
The full server run selected all 233 servers: 210 passed; 23 were blocked by GitHub HTTP 503 responses or PyTorch download connection resets. Both changed servers passed their full isolated Linux suites: OpenCode 472 passed/6 skips; NOOA 226 passed. There were no data-validation failures. Scripted OpenCode conformance statuses match the previous revision, including its existing sandbox.identity and model_error failures.