Session replay stands in for the model with recorded traffic: it rebuilds each model call from a recorded session log, so the real Oda agent runs its loops with no provider key and the same output every run. Use it for automated scenarios that need real loop behaviour with fixed model output; to choose between the test tiers, see Testing tiers.
Replay mounts in place of a real model route:
The settings a scenario passes:
| Setting | What it sets | Default |
|---|---|---|
file | The primary fixture: session. for a v0 log, or session. for a numbered generation. | The path the snapshot suite supplies for the scenario. |
overrideFile | An optional sidecar for the primary session (see below). | The path the suite supplies when a scenario has one. |
childFiles | Recorded subagent logs for a nested scenario, one per child session. | None; the suite supplies them for nested scenarios. |
providers | An optional replay-only provider and model catalogue for scenarios that exercise model discovery. | None; every model call is intercepted. |
paceMs | A delay before each replayed chunk, for genuinely incremental delivery. | None; chunks yield at once. |
A run with no fixture path at all is refused: a fixture path is required.
a model call arrived from an unrecorded session (#<n>); the scenario recorded only <m> session(s), and it tells you to re-record the scenario.Two failure shapes cannot be rebuilt from a session log alone: a call that threw before producing any chunk, and a call that hung or was cancelled. A scenario that needs them points overrideFile at a replay. sidecar in the scenario folder.
{ "patches": [{ "at", "entry" }] } keeps it and swaps only the calls you name; at counts calls from 0, and at equal to the number of derived calls appends a call, for example the retry after an injected failure.chunks replays its chunks. throw replays its prefix chunks (possibly none), then fails the call with its message and code; a throw with prefix chunks counts as accepted, while a throw with no chunks counts as never accepted unless you set accepted: true. hang streams a short prefix, optionally writes a readyFile marker, then waits for the test to cancel.{ "patches": [{ "at": 0, "entry": { "kind": "throw", "chunks": [], "message": "<error text>", "code": "<code>" } }] }invalid override <file>: <location> <detail>, including an unknown entry kind and a non-boolean accepted. A patch index past the end is override patch index <n> out of range (derived script has <n> call(s); == length appends), and naming an index twice is duplicate override patch index <n>.{{fromRequest:<pattern>}}. At stream time, replay matches the pattern against the live request's text and substitutes the last match's first capture group, or the whole match when the pattern has no group. Use it for values no fixture can hold, such as an id minted during the run that the model must echo back.fromRequest has an invalid pattern <pattern>), a pattern that matches nothing (fromRequest pattern <pattern> matched nothing in the request), or an unterminated placeholder (fromRequest placeholder is unterminated in <text>).{{session:N}} becomes the live id of the nth session that bound; using it before that session binds fails with session token {{session:N}} was used before that recorded session bound, and the full vocabulary is on Recorded-session snapshots.format catalog v<n> does not match Session v<n>.session snapshot line <n> contains invalid JSON, session snapshot must start with a session header, or session snapshot line <n> cannot mix projected and complete body rows.model call <turn>/ <step> ended without a finish chunk (a thrown stream); this scenario needs a replay. override. json sidecar.fixture not found: <file> tells you to run pnpm run test:snapshot:record first, and child fixture not found: <file> tells you to re-record the scenario.script exhausted: a session asked for a call its script does not have. Re-record the scenario.assertConsumed) fails teardown with fixture not fully consumed, naming scripts that never bound to a live session and sessions that consumed only part of their script, so a silent underrun becomes a diagnostic.paceMs must be a non-negative integer, got <value>; a provider catalogue with a bad value is refused at load, for example provider "<id>" model "<id>" systemPromptUpdate must be "in-history" when present.paceMs changes delivery timing, and correctness must never depend on it; cancelling during a pace wait cancels the stream like any other cancel.The tier that records the fixtures replay runs on, with its record, replay and refresh modes, is Recorded-session snapshots.
replay. override. json sidecar?Because two failure shapes cannot be rebuilt from a session log: a call that threw before producing any chunk, and a call that hung or was cancelled. The sidecar supplies them, and the message for a stream that ended without a finish chunk points at it.
By first-call order. The first live session to make a model call takes the parent script, and the next new session takes the next child script, each advancing its own cursor. More live sessions than recorded scripts fails loud rather than guessing.
script exhausted, naming the call the session wanted and the calls the script has. Re-record the scenario so the script covers the run.
The call fails with the unrecorded-session message: more distinct live sessions made calls than the scenario recorded. Re-record the scenario.
Set paceMs to delay each chunk. It exists for genuinely incremental delivery; never let correctness depend on it.
No. Every call is answered from the fixture, and the provider catalogue replay can expose is replay-only, so a keyless run is also a harmless one.
Run the consumption check at teardown. It fails unless every recorded script bound to a live session and every bound session consumed its whole script, so an underrun cannot pass silently.
{{fromRequest:. . . }} placeholder fail?The resolution names its reason: an invalid pattern, a pattern that matched nothing in the request, or an unterminated placeholder. Remember that the last match wins and its first capture group substitutes.
Not deterministically. Binding follows first-call order and assumes delegation ran one session at a time, so a recording whose sibling sessions interleave cannot be bound reliably.
From the scenario's own settings, or from the snapshot suite, which supplies the selected fixture per scenario. With neither, replay refuses to run: a fixture path is required.
No. Replay never rewrites or renames a fixture, and a historical fixture is migrated in memory as it reads it. Recording is the snapshot suite's job.