Ordinary capture: every call hangs off the call before it, so a rollout's calls form one tree. RL walks up from a single leaf and builds one row.
This PR
With context compaction (CC), each rewrite starts a new root, so the calls form a forest. RL walks up once per tree and builds one row per tree.
Status
Proposed — open PR, not merged. Both pictures below are real runs of the PR's code, with a stand-in model.
Nine pages on this PR, one question each. This one: what the call lineage looks like, and which leaves RL walks up from.
Background. Gym's ledger records each model call's parent: the earlier call whose tokens this call's prompt starts with.
Drawn out, that is a tree. A root has no parent and stores its whole prompt. A leaf has no children.
When the agent rejects a response and asks again, the retry gets the same parent as the rejected call, so the tree branches.
Dotted words have a short explanation: hover or tap them.
The same conversation was run twice through the PR's code: 7 accepted actions, and the agent rejects one response at action 4 and one at action 6. That makes 9 model calls, c1–c9.
Without compaction every call extends its parent, so the 9 calls form one tree with 3 leaves. RL walks up from just one of them, the response the agent ended on (Gym _terminal_chain).
With compaction every 2 actions, each rewrite starts a new root, giving 4 trees. RL picks the accepted calls from the agent's output, cuts them at each root, and walks up once per tree from that tree's accepted leaf (_plan_selected_calls, then finalize_rollout per tree).
Rejected leaves are never walked in either case.
Same 9 calls, as one tree and as a forest
Which leaves get walked, and why not all of them
Compaction off
Compaction on
Shape of the lineage
1 tree
4 trees, one per stretch between rewrites
Leaves
3 (c4, c7, c9)
6 (c2, c4, c5, c7, c8, c9)
Leaves walked
1: the response the agent ended on (c9)
4: the last accepted call in each tree (c2, c5, c8, c9)
How RL finds them
From the harness's declared final response, or the scored response, matched to a ledger row
From the agent's whole output: every accepted response is matched to its call, then the list is cut at each root
Rows
1 row, 21 tokens
4 rows: 6 + 6 + 6 + 3 tokens
Rejected calls (c4, c7)
Same in both: never walked and never trained, but still read, checked and cleaned out of storage with the rest (rollout_reassembler.py L680-687)
Walking up from every leaf would train the rejected responses c4 and c7, which the agent threw away. So in both cases RL uses the agent's own record of what it kept to pick the leaves. With compaction that record has to cover the whole conversation, not just the end: a tree whose calls are missing from the agent's output is never walked, so it never trains.
So what. The walk itself did not change: inside each tree, RL still goes from one leaf up to the root and joins the stored tokens.
What changed is how many trees there are, and how RL finds the leaf of each: from the agent's full output instead of a single final response.
A rollout with 3 rewrites becomes 4 rows that share one reward and one advantage.