Skip to content

feat(replay): replay-buffer env — continue/recheck/judge tasks from saved rollouts#2941

Draft
faresobeid wants to merge 13 commits into
mainfrom
feat/replay-buffer
Draft

feat(replay): replay-buffer env — continue/recheck/judge tasks from saved rollouts#2941
faresobeid wants to merge 13 commits into
mainfrom
feat/replay-buffer

Conversation

@faresobeid

@faresobeid faresobeid commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Turns saved rollouts into new training tasks. Every run already writes each rollout to <output_dir>/rollouts/step_N/train_rollouts.jsonl; replay envs index those files as a buffer and serve derived tasks from them, mixed into training as normal [[orchestrator.train.env]] entries with a ratio.

Depends on PrimeIntellect-ai/verifiers#1919 (submodule pinned to its tip), which now hosts the shared machinery as verifiers.v1.tasksets.replay — a base taskset in the harbor/textarena mold. This PR contributes the derivation packages, the orchestrator/persistence glue, docs, and configs.

The derivations (thin packages over the verifiers base)

Taskset id Task
replay-continue-v1 Resume a source rollout mid-way: from a context-compaction point (anchor = "compaction"; one task per compaction) or right after a tool result (anchor = "tool-call"; one deterministically sampled resumable point per source — only points where every issued call has its result).
replay-recheck-v1 The finished conversation + an appended check-your-work turn.

Each is ~30-45 lines: a flat config subclass + two hooks (record_anchors, build_prompt). All derivations are scored by the required inner (original) taskset — inner rewards land with their original names and weights via a task-swapped trace view; tools/setup/finalize delegate too. Each drawn source spawns a fresh GRPO group. Adding a new derivation = a new small package (see docs/replay.md "Writing a New Derivation").

Buffers are offline (a prior run's rollouts, indexed once, deterministic per task index) or online (buffer_dir = "self", inferred online: rescans barrier-complete steps, samples fresh per request, forces whole-group dispatch so the group shares one source). source_envs selects sources by the new info.prime_rl.env_name stamp; by default replay-derived records (structural lineage check) are skipped, and explicitly naming a replay env is the opt-in for chained derivations (recheck-a-recheck as env topology, scoring always unwrapping to the innermost original task).

Core changes outside the packages

  • Rollout.to_record() stamps {kind, env_name, group_id, policy_version, is_filtered} into info["prime_rl"] so saved rollouts stay attributable (round-trips through WireTrace.model_validate).
  • The orchestrator resolves the buffer_dir = "self" sentinel to <output_dir>/rollouts before env servers spawn (train + eval envs).
  • v1 train envs' max_total_tokens now defaults to seq_len at config-resolve time (v1 parity with the legacy max_seq_len injection) — long-prompt envs like replay no longer need hand-set token budgets; explicit values are respected.
  • Submodule bump for the verifiers side (replay base, lazy resolve_task, forced group dispatch, large-Messages-prompt file handoff).

Design notes

  • The wire protocol carries only a task index, so replay must live env-side; task lists freeze at server startup, so tasks bind lazily from (file, offset) handles (~1MB lines are never held in memory). Online buffers advertise a single virtual task (sampling ignores the index), so replay envs need explicit ratios — the all-or-none rule makes that loud.
  • Run replay envs under the source env's harness (message-prompt seeding required). Container sources are gated off (allow_container) until sandbox snapshotting lands.
  • Config surface: flat per env — base fields (buffer_dir, required inner, source_envs, online-usually-inferred, allow_container) + the package's own knob.

Verification

  • 16 base tests in verifiers (tests/v1/test_replay.py: surgery on synthetic forests, buffer scan/barrier/eviction/pick determinism, lineage skipping) + 8 package tests here (loader resolution, config validation, deterministic tool-call anchor sampling).
  • Empirical validation of compaction detection + seed builders against a real 104MB production rollout file; end-to-end resolution sanity for all three flows (compaction, tool-call, recheck) through the real loader path, plus prime-rl config narrowing round-trip (the env-server spawn path).
  • Both debug configs (configs/debug/replay/) pass rl --dry-run.
  • Reviewed twice earlier in development: an adversarial correctness pass (13 confirmed findings fixed — incl. E2BIG on recheck seeds, a self-replay feedback loop, a step-0 permit-hoarding deadlock) and a four-angle quality pass (16 cleanups applied).
  • Live shakedown passed (single node, configs/debug/replay/online_recheck.toml, 20 steps): env ready with 1 virtual task + forced group dispatch; empty-buffer fail-fast freed permits with no deadlock (its error markers saved to step_0 as designed); 176 recheck rollouts across steps 6-15, zero errored, scored by the inner verifier (lcs, real reward distribution) with replay/source_reward/source_step metrics; seeded prefixes fully masked, only the sampled revision trainable; no recheck-of-recheck (lineage exclusion held); trainer completed 20/20 clean.

🤖 Generated with Claude Code

faresoPrime and others added 11 commits July 2, 2026 04:56
…aved rollouts

Turn saved rollouts (<run>/rollouts/step_*/train_rollouts.jsonl) into new
training tasks served by a new v1 taskset plugin, environments/replay_v1
(id "replay-v1"), mixed into training as a normal [[orchestrator.train.env]]
entry with a ratio:

- continue: resume from a context-compaction point — the seed is the
  post-compaction prompt recovered from the trace's message graph; scored
  by the original (inner) taskset.
- recheck: the finished conversation plus an appended check-your-work turn;
  scored by the original taskset via a task-swapped trace view.
- judge: a rendered (truncated, label-balanced) transcript and "was this
  correct?"; reward = verdict matches the reward the rollout received.

Buffers index (path, offset) handles and bind tasks lazily through the new
verifiers Taskset.resolve_task hook; offline buffers bind deterministically
per index, online buffers (buffer_dir = "self") rescan barrier-complete
steps and sample fresh per request, forcing whole-group dispatch so GRPO
groups share one source rollout.

Rollout.to_record() now stamps {kind, env_name, group_id, policy_version,
is_filtered} into info["prime_rl"] so saved rollouts stay attributable;
the orchestrator resolves the "self" buffer sentinel before env servers
spawn (children never learn the output dir).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tests/unit/test_replay_v1.py: trace surgery on a synthetic forest,
  verdict parsing, config validation, buffer scan/barrier/balance/pick.
- docs/replay.md (+ mint.json index, configuration.md pointer; also fixes
  the Environments example that set a ratio on only one env, which the
  all-or-none validator rejects).
- skills/configs + skills/training/monitor-run: replay env config surface
  and the info.prime_rl rollout attribution stamp.
- configs/debug/replay/{offline_recheck,online_judge}.toml + README, both
  validated with `rl --dry-run`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Label balancing is now a derived view over the retained index, not a
  destructive rewrite: majority-label candidates dropped from one balanced
  view pair up in later ones (online rescans no longer bleed the judge
  buffer down to the earliest pairs).
- Replay-derived records (task carries kind + source_task) are never
  candidates — an online "self" buffer would otherwise judge its own judge
  rollouts, a compounding feedback loop; the shipped online example also
  pins env_name.
- Empty online buffers fail requests after a bounded wait instead of
  blocking forever — blocked groups were hoarding dispatch permits before
  step 0's barrier existed, which could deadlock the run.
- Index swaps are atomic (rescan threads no longer mutate the list the
  event loop samples from); online sampling mixes the worker pid into the
  rng; vanished source files (a resumed producer run cleaning future
  steps) discard the candidate and redraw.
- buffer_dir="self" resolution also covers eval envs, and the taskset
  errors clearly when the sentinel reaches it unresolved (standalone
  servers).
- parse_verdict ignores lines quoting both verdict options (an echoed
  instruction is not an answer).
- docs: startup fail-and-retry semantics, self-replay exclusion,
  allow_container runtime requirement, shared-toolset failure mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ained derivations

Replace the single env_name filter and the hard never-replay-a-replay rule
with one mechanism: source_envs, an allowlist over the info.prime_rl
env-name stamp. Unset, it replays every env except replay envs (the old
ban, now a default); listing env names replays exactly those — and naming
a replay env is the deliberate opt-in for chained derivations, so depth is
expressed as env topology (recheck-2 sources [recheck-1]) instead of a
counter. Chained records nest their lineage; scoring, tools, and container
provisioning always resolve to the innermost original task
(unwrap_source_task).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Buffer: rescans prune already-scanned and out-of-window steps by dirname
before any file stat (was ~2 stats per historical step every 10s); the
scan early-breaks once a pass gathers max_candidates — newest-first order
makes everything older permanently evictable — so a cold online buffer no
longer reads a resumed run's entire history; no-op rescans skip the
sort/rebalance; the balanced view is derived in one place (_set_index);
timing knobs become module constants; the rescan lock is built eagerly.

Taskset: resolve reads+materializes off the event loop in one thread; the
rebuilt inner task is validated once per resolve and cached on the task
(was revalidated by every per-rollout hook); mode/inner conditions
collapse to `self.inner is None` (config validation ties them); continue
skips the graph walk it never used.

Also: is_replay_derived tightened to the actual lineage shape (an
unrelated taskset with a `kind` field is no longer silently excluded),
and the orchestrator's "self" comment states the real constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ess, not null

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h varying

num_slots (online sampling ignores the index — any size works), seed (each
pool worker seeds from OS entropy; cross-worker determinism never existed),
balance_labels (unbalanced judge reward is gameable; always on, degrades
with a warning on one-sided data), max_message_chars (a rendering detail —
the total transcript budget is the real knob), and max_candidates (a
safety cap on 60-byte handles; now an internal constant). 16 fields -> 11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_steps_back

Restructure the config so knobs exist only where they mean something:

- `mode` + flat per-mode fields become a `derivation` discriminated union
  ({type = "continue" | "recheck" | "judge"}): `inner`/`allow_container`
  live on the delegating derivations, `instruction`/`success_threshold`/
  `max_transcript_chars` on the derivations that use them. The judge-
  forbids-inner and recheck-requires-inner validators disappear — judge's
  config has no `inner` field and the delegating ones require it.
- `stop_conditions` becomes the first entry type of an opt-in `filters`
  list (absent unless you add it) — the landing spot for future selection
  ideas (recency, reward bands, group-relative) without new core fields.
- `online` is auto-set for buffer_dir = "self" (it must be pinned before
  the orchestrator rewrites the sentinel); explicit use remains only for
  watching another still-running run's dir.
- `max_steps_back` is removed: the buffer's capacity (newest-first scan +
  cap) is the eviction policy, as in any replay buffer.
- Online buffers advertise a single virtual task instead of 1024 — the
  index is ignored by sampling, so 1 is the honest count (replay envs need
  explicit ratios regardless; documented).
- Judge per-message truncation derives from the total budget (1/20).

Core config: buffer_dir, derivation, source_envs, filters, online.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source selection beyond source_envs is not core config: a derivation that
wants it (e.g. recheck only agent_completed sources) owns that choice in
its own code/config when derivations become user-extensible. Core config
is now buffer_dir, derivation, source_envs, online.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…echeck

Drop the judge derivation for now — with it go verdict parsing, transcript
rendering, label balancing, success_threshold, and the balanced-view
machinery (~150 lines). Both remaining derivations delegate to a now-
required `inner`, so the delegation guards simplify too.

Continue gains `anchor = "compaction" | "tool-call"`: tool-call anchoring
resumes right after a tool result, at points where every issued call has
its result (no dangling calls in the seed); tool calls are numerous, so
one resume point per source rollout is sampled deterministically (same
choice across pool workers). The debug online example becomes
online_recheck.toml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e thin packages

Following the harbor/textarena pattern, the buffer, trace surgery, and
delegation machinery move to verifiers.v1.tasksets.replay (with their
tests), and each derivation is now a thin subclass package binding its own
flat config:

- environments/replay_continue_v1 (id "replay-continue-v1"): anchor =
  "compaction" | "tool-call" (~45 lines)
- environments/replay_recheck_v1 (id "replay-recheck-v1"): instruction
  (~30 lines)

The derivation union disappears from config — the derivation IS the
taskset id, `inner` is a required base field, and a new derivation is a
new small package implementing record_anchors + build_prompt. Replay-
derived detection becomes structural (source_task + source_id lineage
keys), so it holds for any future derivation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
faresoPrime and others added 2 commits July 3, 2026 05:37
Legacy envs already get max_seq_len injected at config-resolve time; v1
envs had no equivalent, so any long-prompt env (replayed conversations
especially) needed hand-set token budgets kept in sync with seq_len.
resolve_env_config now defaults an unset max_total_tokens to seq_len for
v1 train envs — rollouts stop at the training budget instead of producing
samples that can't fit. Explicit values (including "None" = unlimited)
are respected; eval envs are untouched.

Drops the budget boilerplate from the replay debug configs and docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Whole-rollout derivations (recheck) now implement a single hook,
build_prompt; only mid-rollout derivations (continue) override
record_anchors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants