Skip to main content

SandboxReplay

SandboxReplay is pVisor's Agent-trajectory replay capability. It normally runs inside a fresh sandbox created by the caller, re-executes the complete tool batches before a selected boundary, rebuilds the Agent-native context with the fresh observations, and then continues the Agent directly after that boundary. It creates a derived Run in the Run and Attempt model.

Replay boundary

A trajectory can be written as:

task -> A1 -> O1 -> ... -> AN -> ON -> A(N+1) -> ...

SandboxReplay executes A1...AN in the fresh sandbox to produce O'1...O'N, retains the Agent-native system prompt, tools, task, and actions, and issues the first continued model request immediately after O'N. No "continue" message is appended. This preserves the replay boundary but does not guarantee that A'(N+1) is byte-identical to A(N+1).

An explicit boundary user prompt can instead make the first live request:

system + tools + task + A1 -> O'1 -> ... -> AN -> O'N
+ boundary_user_prompt -> A'(N+1)

Use --boundary-user-prompt TEXT only when this changed input is intentional. SandboxReplay injects it once, after O'N and before the first live model inference. It never replaces the original task and is not injected in prepare-only or replay-only mode. Without the option, the existing exact boundary behavior is unchanged.

Supported Agent profiles

Replay profiles are version-pinned and fail closed when the installed runtime does not match. The Pi profile supports @earendil-works/pi-coding-agent 0.83.0 and consumes the native RPC event JSONL produced by Pi. A replay step is one complete turn_end tool batch. The profile reconstructs a fresh Pi v3 session with new observations, then continues through Pi's SDK. Its initial tool surface is intentionally limited to Pi's read, bash, edit, and write tools; trajectories containing another tool fail validation.

OpenCode 1.17.7 and Codex CLI 0.149.0 are also supported. OpenCode consumes the native opencode run --format=json event stream and groups step_start/tool_use/step_finish events into complete replay steps. Codex consumes rollout JSONL (session_meta plus response_item messages, reasoning, function calls/custom tool calls, and outputs). Both adapters re-execute the selected command/file tools in the fresh workspace, replace native observations, write a native JSONL prefix, and then invoke the native continuation command. OpenCode uses run --format=json --session; Codex stages the prefix below an isolated CODEX_HOME and uses exec resume --json. Unknown tool shapes fail explicitly instead of being silently skipped.

Run replay

pvisor replay \
--agent claude-code \
--trajectory /input/session.jsonl \
--after-step 30 \
--agent-entrypoint /usr/bin/claude \
--boundary-user-prompt 'Review the fresh observation before continuing.'

The equivalent TOML is:

[replay]
agent = "claude-code"
trajectory = "/input/session.jsonl"
after_step = 30
agent_entrypoint = "/usr/bin/claude"
max_steps = 200
session_id = "task-291-attempt-1"
replay_only = false
disable_thinking = true
boundary_user_prompt = "Review the fresh observation before continuing."

When the Pi runtime is installed at /opt/pi-agent, the CLI form is:

pvisor replay \
--agent pi-agent \
--trajectory /input/pi-agent.events.jsonl \
--after-step 30 \
--agent-entrypoint /opt/pi-agent/bin/pi

OpenCode consumes its native event stream:

pvisor replay \
--agent opencode \
--trajectory /input/opencode.jsonl \
--after-step 30 \
--agent-entrypoint /usr/bin/opencode

Codex consumes its native rollout JSONL. SandboxReplay derives the Codex native session ID from the trajectory's session_meta; the request session_id remains a model-router/run key and cannot override the native Codex identity. If the trajectory has no native session ID, continuation fails closed instead of starting a fresh conversation:

pvisor replay \
--agent codex \
--trajectory /input/rollout.jsonl \
--after-step 30 \
--agent-entrypoint /usr/bin/codex

For Codex CLI versions that require a prompt on exec resume, SandboxReplay generates a per-run transport nonce and starts a loopback-only Responses bridge. The CLI receives the nonce, but the bridge removes it before every upstream request (Codex may resend the full history on later requests) and fails closed on malformed or ambiguous requests. The continued native JSONL is also scrubbed of the nonce and the legacy Continue from the replay boundary. message. The default metadata records prompt_mode = "transport_nonce" and input_condition = "replayed_boundary_only". With an explicit boundary_user_prompt, the user message is retained and metadata records prompt_mode = "explicit_user_prompt" and input_condition = "boundary_user_prompt_appended".

The same TOML surface is used for both; change [replay].agent, trajectory, and agent_entrypoint to the selected runtime.

Execution modes and results

  • The default mode executes the selected prefix and continues with the live Agent.
  • --replay-only executes the prefix and stops before the next model request.
  • --prepare-only only validates and constructs the prefix. It executes no tools, starts no Agent, and does not require an Agent runtime.

--max-steps counts all Agent actions, including the selected prefix. For example, --after-step 30 --max-steps 50 leaves at most 20 live actions. A replay-only budget must cover the prefix; a continuation budget must leave at least one live action.

Results use sandbox-playback.result/v3. The phase is prepared, replayed, or continued; quality is verified or degraded; and agent_status distinguishes not_started, completed, max_steps, and failed. Failures retain available logs and native trajectories. OpenHands controller fatal states are failures even when its process exits with status zero.

Successful result metadata records whether the boundary prompt was requested and injected, plus its character length and SHA-256 digest; replay journals do not store the prompt text. Agent-native prepared or continued trajectories may contain the user message. For Claude Code, the in-memory bridge adds the message only to the first cleaned upstream request and leaves the reconstructed native session unchanged. When it is injected, next-action-comparison.json uses the input condition boundary_user_prompt_appended. Its similarity and tool metrics remain descriptive and must not be interpreted as same-input replay fidelity.

Migration: older non-Claude configurations sometimes used replay_only = true to construct a prefix without executing it. Use prepare_only = true for that behavior. In v3, replay-only always executes the selected prefix and therefore requires an exact-version runtime. Claude observations that cannot be reproduced fresh fail by default; --allow-stale-observations explicitly permits a degraded result.

Runtime isolation is opt-in. Replay only creates an outer managed pvisor run when the caller supplies runtime options such as --executor, --stage, or --overlayfs-path/--overlayfs-compose, or their TOML equivalents. See the pvisor replay reference for the complete surface.

Qwen3.6 evaluation

The evaluation used Qwen3.6-35B-A3B with thinking disabled. Each original run and continuation used a newly created sandbox with 2 CPUs, 7 GiB memory, and 70 GiB storage. Step counts are native tool batches recognized by the Rust parser. Text similarity compares normalized visible text and excludes reasoning. Exact tools require equal counts, order, names, and JSON arguments.

AgentTaskNOriginal stepsContinued stepsOriginal RewardContinued RewardExact A'(N+1) toolsText similarity
Claude CodeNodeBB (291)16910111yes0.85
Claude CodeVuls (666)287620000no0.45
Claude Codequtebrowser (667)36464611no0.48
OpenHandsNodeBB (291)28626810yes1.00
OpenHandsVuls (666)25434311yes1.00
OpenHandsqutebrowser (667)17368711no0.13
mini-swe-agentNodeBB (291)4810411511no1.00
mini-swe-agentVuls (666)39949111yes1.00
mini-swe-agentqutebrowser (667)311156511no0.77

Boundary-response examples

The Chinese localization of this page contains all nine paired A(N+1) and A'(N+1) responses, including visible text, tool names, and arguments. Use the language switcher to open that detailed report. Reasoning is removed, and long file replacements are reduced to their distinguishing targets.

For exact options, use the pvisor replay reference; for execution boundaries, continue to the execution guide.