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-onlyexecutes the prefix and stops before the next model request.--prepare-onlyonly 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.
| Agent | Task | N | Original steps | Continued steps | Original Reward | Continued Reward | Exact A'(N+1) tools | Text similarity |
|---|---|---|---|---|---|---|---|---|
| Claude Code | NodeBB (291) | 1 | 69 | 101 | 1 | 1 | yes | 0.85 |
| Claude Code | Vuls (666) | 28 | 76 | 200 | 0 | 0 | no | 0.45 |
| Claude Code | qutebrowser (667) | 36 | 46 | 46 | 1 | 1 | no | 0.48 |
| OpenHands | NodeBB (291) | 28 | 62 | 68 | 1 | 0 | yes | 1.00 |
| OpenHands | Vuls (666) | 25 | 43 | 43 | 1 | 1 | yes | 1.00 |
| OpenHands | qutebrowser (667) | 17 | 36 | 87 | 1 | 1 | no | 0.13 |
| mini-swe-agent | NodeBB (291) | 48 | 104 | 115 | 1 | 1 | no | 1.00 |
| mini-swe-agent | Vuls (666) | 39 | 94 | 91 | 1 | 1 | yes | 1.00 |
| mini-swe-agent | qutebrowser (667) | 31 | 115 | 65 | 1 | 1 | no | 0.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.