Skip to content

SLM-418: render replay preference rows into trainable pairs (DSH5-10) - #1124

Closed
Tyler-R-Kendrick wants to merge 3 commits into
mainfrom
claude/great-dirac-ni43oh
Closed

SLM-418: render replay preference rows into trainable pairs (DSH5-10)#1124
Tyler-R-Kendrick wants to merge 3 commits into
mainfrom
claude/great-dirac-ni43oh

Conversation

@Tyler-R-Kendrick

@Tyler-R-Kendrick Tyler-R-Kendrick commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Sixth slice of SLM-418's replay-grounded preference extraction (DSH5-10). Pattern extraction reached 7/7 in the fifth slice (SLM-418: add pronoun-focus-followup replay preference pattern (DSH5-10) #1119), but nothing in the repo converted a row into the PreferencePair shape scripts/train_preference.py's build-pairs/train path actually consumes — confirmed by grepping the whole tree for ReplayPreferenceRelation/extract_replay_preference_rows/extract_merge_preference_row before this change: only the operators module, its own test file, the version registry, and the disposition doc referenced them.
  • Adds src/slm_training/harnesses/preference/replay_pairs.py: render_replay_preference_pair renders one OperatorReplayPreferenceRowV1 into a PreferencePair without ever fabricating a state — undo/redo:<id>/checkout:<id> resolve via direct node lookup (those targets are always already-materialized trace nodes, no computation needed), and the one case that needs a genuinely new state (a pronoun_focus_followup sibling operator action) re-derives it by recomputing the exact legal set at the input state (enumerate_operator_legal_set, the same call extract_replay_preference_rows itself makes) and actually applying the matching action through the pack-authorized OperatorLibraryV1.apply. A rejected merge:<pair> (unreachable under today's single-candidate extraction, but defensively handled) is left honestly unrendered rather than guessed — see test_rejected_merge_action_is_never_rendered.
  • MERGE_SUCCESS rows never live on a shared ConversationTraceV1 (extract_merge_preference_row operates on independent BranchEditV1 edges plus a fresh merged node), so merge_node_resolver(left, right, decision) builds the equivalent per-call node resolver for that case.
  • Documents an open, explicitly-flagged modeling choice: the pair's prompt is the input state's own DSL source — a first cut for compatibility with the existing generic TwoTower pair format, not a validated training-objective decision. The disposition doc's own remaining-scope note names the DSH3-selected policy head (typed_operator_policy.py) as the actual training target, so this is called out as open for whoever wires a real training run.
  • Still wiring only. No pairs corpus is built from real conversation traces, no training run happens, and no held-out benefit is claimed. harness.preference.replay_pairs registered fresh (v1, initial registration) in versions.json; dsl.operators.replay_preference is unchanged (only consumed, not modified — still v6).
  • Honesty tier: wiring, not a ship or training claim. Full disposition in docs/design/dsh5-10-replay-preference-rows.md's new "Sixth slice" section.

Test plan

  • env -u NODE_OPTIONS pytest -q tests/test_harnesses/preference/test_replay_pairs.py tests/test_dsl/test_replay_preference.py tests/test_dsl/test_operator_merge.py tests/test_dsl/test_operator_conversation.py tests/test_evals/test_advanced_operator_disposition.py tests/test_scripts/test_validate_advanced_operator_disposition.py69 passed (8 new + 61 pre-existing, all green) in a fresh .venv, Python 3.12, pip install -e ".[dev,grammar]" + npm ci in src/apps/openui_bridge for the G2/G8 schema-oracle gate
  • ruff check clean on both new files
  • python -m scripts.verify_version_stamps --check --base origin/mainok (1 component(s) touched)
  • python -m scripts.repo_policyok
  • python -m scripts.verify_decode_invariants — clean
  • New tests directly exercise every rendering branch, including two adversarial/defensive cases: a degenerate chosen==rejected collapse (test_undo_then_redo_row_declines_a_degenerate_collapse) and the unreachable-but-guarded merge:<pair>-as-rejected case (test_rejected_merge_action_is_never_rendered)

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added conversion of replay-based preference records into trainable preference examples.
    • Supports undo, redo, checkout, follow-up, and merge scenarios while preserving grounded conversation states.
    • Excludes invalid, duplicate, or unsupported alternatives instead of generating misleading examples.
  • Documentation

    • Updated design documentation to reflect the sixth implementation slice and revised reproducibility results.
  • Tests

    • Added comprehensive coverage for conversion, state resolution, merge handling, deduplication, and batch processing.

Sixth slice of SLM-418's replay-grounded preference extraction. Pattern
extraction reached 7/7 in the fifth slice (#1119), but nothing in the repo
converted a row into the PreferencePair shape scripts/train_preference.py
actually consumes -- confirmed by grepping the whole tree for
ReplayPreferenceRelation/extract_replay_preference_rows/
extract_merge_preference_row before this change.

Adds src/slm_training/harnesses/preference/replay_pairs.py:
render_replay_preference_pair renders one OperatorReplayPreferenceRowV1 into
a PreferencePair without ever fabricating a state -- undo/redo/checkout
resolve via direct node lookup (no computation needed, since those targets
are always already-materialized trace nodes), and the one case that needs a
genuinely new state (a pronoun-focus-followup sibling operator action)
re-derives it by recomputing the exact legal set at the input state and
actually applying the matching action through the same pack-authorized
OperatorLibraryV1.apply every other application in this module goes
through. merge:<pair> as a rejected action (unreachable today, but
defensively handled) is left honestly unrendered rather than guessed.

MERGE_SUCCESS rows never live on a shared ConversationTraceV1 (BranchEditV1
edges + a fresh merged node instead), so merge_node_resolver(left, right,
decision) builds the equivalent per-call node resolver for that case.

Also documents an open, explicitly-flagged modeling choice: the pair's
prompt is the input state's own source, a first cut for compatibility with
the existing TwoTower pair format -- not a validated training-objective
decision, since the disposition doc's own remaining-scope note names the
DSH3 policy head as the actual training target.

Still wiring only: no pairs corpus built from real traces, no training run,
no held-out claim. harness.preference.replay_pairs registered fresh (v1);
dsl.operators.replay_preference is unchanged (only consumed, not modified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slm-training Ready Ready Preview, Comment Jul 27, 2026 2:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Tyler-R-Kendrick, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ec45edf-ee0d-45ba-8651-419c1b54a2dc

📥 Commits

Reviewing files that changed from the base of the PR and between df5fa56 and cfc9a3c.

📒 Files selected for processing (6)
  • docs/design/dsh5-10-replay-preference-rows.md
  • scripts/build_replay_preference_pairs.py
  • src/slm_training/harnesses/experiments/argument_preference.py
  • src/slm_training/resources/versions.json
  • tests/test_harnesses/experiments/test_argument_preference.py
  • tests/test_scripts/test_build_replay_preference_pairs.py
📝 Walkthrough

Walkthrough

Changes

Replay preference pair conversion

Layer / File(s) Summary
State resolution contracts
src/slm_training/harnesses/preference/replay_pairs.py
Adds NodeResolver and a merge resolver that exposes merge inputs, outputs, and continuation nodes.
Preference pair rendering
src/slm_training/harnesses/preference/replay_pairs.py, tests/test_harnesses/preference/test_replay_pairs.py, src/slm_training/resources/versions.json, docs/design/dsh5-10-replay-preference-rows.md
Converts replay rows into PreferencePair objects, re-derives supported rejected actions, drops unresolved or degenerate alternatives, tests semantic relations, registers harness version v1, and documents the sixth slice and reproducibility results.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReplayRow as OperatorReplayPreferenceRowV1
  participant Renderer as render_replay_preference_pair
  participant Resolver as NodeResolver
  participant Library as OperatorLibraryV1
  participant Pair as PreferencePair
  ReplayRow->>Renderer: provide input, chosen, and rejected actions
  Renderer->>Resolver: resolve input and chosen state IDs
  Renderer->>Library: re-apply derivable rejected action
  Library-->>Renderer: rejected output state
  Renderer-->>Pair: construct prompt, chosen, rejected, and metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: rendering replay preference rows into trainable pairs for DSH5-10.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/great-dirac-ni43oh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/slm_training/harnesses/preference/replay_pairs.py`:
- Around line 94-172: Validate the replay row’s legal-set fingerprint in
render_replay_preference_pair before re-deriving the rejected operator action.
Recompute the legal set using the same ordinary_nonoperator_actions
configuration as extraction, return None when its fingerprint differs from
row.legal_set_fingerprint, and only then locate the action and call
library.apply.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d6c0dce0-6f6d-45e9-952a-882a50ce08e0

📥 Commits

Reviewing files that changed from the base of the PR and between 5f94b92 and df5fa56.

📒 Files selected for processing (4)
  • docs/design/dsh5-10-replay-preference-rows.md
  • src/slm_training/harnesses/preference/replay_pairs.py
  • src/slm_training/resources/versions.json
  • tests/test_harnesses/preference/test_replay_pairs.py

Comment on lines +94 to +172
def render_replay_preference_pair(
row: OperatorReplayPreferenceRowV1,
*,
resolve_node: NodeResolver,
pack: DslPack,
library: OperatorLibraryV1,
provenance_for: ProvenanceFactory,
) -> PreferencePair | None:
"""Render one replay-grounded row into a ``PreferencePair``, or ``None``.

Returns ``None`` rather than fabricating a rejected state whenever the
rejected action is ``merge:<pair>`` (out of scope; see module
docstring), its target legal action cannot be re-derived from the exact
legal set at ``row.input_state_id`` (would indicate a stale/inconsistent
row), or the re-applied rejected state is identical to the chosen state
(degenerate; not a real preference).
"""
input_node = resolve_node(row.input_state_id)
chosen_text = resolve_node(row.chosen_output_state_id).state.source

action = row.rejected_action
if action == "undo":
if input_node.parent_state_id is None:
return None
rejected_text = resolve_node(input_node.parent_state_id).state.source
elif action.startswith("redo:") or action.startswith("checkout:"):
target_state_id = action.split(":", 1)[1]
rejected_text = resolve_node(target_state_id).state.source
elif action.startswith("merge:"):
return None
else:
legal_set = enumerate_operator_legal_set(
pack=pack,
library=library,
state=input_node.state,
reference_table=input_node.reference_table,
provenance=provenance_for(input_node.state),
ordinary_nonoperator_actions=(),
)
match = next(
(
candidate
for candidate in legal_set.operator_actions
if candidate.serialized == action
),
None,
)
if match is None:
return None
result = library.apply(
pack,
input_node.state,
match.operator_id,
match.arguments,
provenance_for(input_node.state),
)
if not result.succeeded or result.state is None:
return None
rejected_text = result.state.source

if rejected_text == chosen_text:
return None

return PreferencePair(
prompt=input_node.state.source,
chosen=chosen_text,
rejected=rejected_text,
meta={
"pair_corpus": "replay_preference",
"schema": "replay_preference_pair/v1",
"semantic_relation": row.semantic_relation.value,
"correction_reason": row.correction_reason,
"input_state_id": row.input_state_id,
"chosen_action": row.chosen_action,
"rejected_action": row.rejected_action,
"legal_set_fingerprint": row.legal_set_fingerprint,
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect OperatorLegalSetV1 / enumerate_operator_legal_set for a fingerprint field
# comparable to OperatorReplayPreferenceRowV1.legal_set_fingerprint.
ast-grep outline src/slm_training/dsl/operators/legal_set.py --items all
rg -n 'fingerprint' src/slm_training/dsl/operators/legal_set.py src/slm_training/dsl/operators/replay_preference.py

Repository: Tyler-R-Kendrick/slm-training

Length of output: 7040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== preference module outline =="
ast-grep outline src/slm_training/harnesses/preference/replay_pairs.py --items all || true

echo "== relevant preference module lines =="
sed -n '1,220p' src/slm_training/harnesses/preference/replay_pairs.py

echo "== legal_set relevant exports and function =="
sed -n '420,555p' src/slm_training/dsl/operators/legal_set.py

echo "== replay_preference fingerprint occurrences around legal sets =="
sed -n '250,550p' src/slm_training/dsl/operators/replay_preference.py

Repository: Tyler-R-Kendrick/slm-training

Length of output: 28183


Validate legal_set_fingerprint before rendering replay pairs.

OperatorLegalSetV1 exposes .fingerprint, and rows are grounded with the legal-set fingerprint at extraction time. In the operator-action re-derivation path, recompute the legal set (with the same ordinary_nonoperator_actions used by the row’s extraction path) and return None unless the fresh fingerprint matches row.legal_set_fingerprint; otherwise library.apply can still render a stale row as long as that one action string is still legal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/slm_training/harnesses/preference/replay_pairs.py` around lines 94 - 172,
Validate the replay row’s legal-set fingerprint in render_replay_preference_pair
before re-deriving the rejected operator action. Recompute the legal set using
the same ordinary_nonoperator_actions configuration as extraction, return None
when its fingerprint differs from row.legal_set_fingerprint, and only then
locate the action and call library.apply.

…0) (#1125)

* SLM-418: build a demo replay-preference pairs corpus + run it (DSH5-10)

Seventh slice, stacked on #1124 (the sixth slice's row->PreferencePair
renderer). Adds scripts/build_replay_preference_pairs.py: builds one small,
deterministic, honestly-labeled fixture_or_scratch conversation exercising
three of the seven named patterns (edit-then-undo, undo-then-redo,
checkout-another-state), extracts rows, renders them, and writes a real
pairs.jsonl -- the first real, on-disk pairs corpus this feature line has
ever produced.

2 of 3 rows render; undo_then_redo never does, for a structural reason, not
a bug: for any deterministic zero-argument operator, redo and reapplying
the same operator at the same input state are, by construction, identical
text, so the renderer's own dedup guard correctly declines rather than
emitting a self-contradictory pair. Confirmed (again) while scoping this
slice: no real captured ConversationTraceV1 corpus exists anywhere in this
repo, and no harness ingests one -- build_symbolic_operator_corpus
synthesizes traces combinatorially from gold DSL records, it doesn't read
captured usage.

Also ran the first real end-to-end chain using DSH5-10 rows: a scratch SFT
checkpoint (wf_smoke_v2, seed 0, 8 steps -- last_loss identical to the
16+ prior independently-verified rows in the smoke-loop ledger, confirming
this is the same deterministic artifact those rows already verified), the
new demo pairs corpus, and one bounded scripts/train_preference.py train
call against it (6 steps, 2 pairs, last_loss=1.077). Both well under
MAX_RUN_MINUTES=3. Still not a training or held-out-benefit claim: n_pairs=2
on a scratch fixture says the pipeline runs end to end for real, nothing
about whether the signal helps the model or should train the DSH3-selected
policy head the issue actually asks about.

harness.preference.replay_pairs bumped v1 -> v2 (adds the new script+test
to its watched paths).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR

* SLM-418: add merge_success to the demo pairs corpus + rerun (DSH5-10) (#1126)

Eighth slice, stacked on #1125 (the seventh slice's demo trace + first real
training run). Adds build_demo_merge_scenario to
scripts/build_replay_preference_pairs.py: a second scratch fixture (two
branches forked from a shared base, editing disjoint node refs), mirroring
the exact disjoint-target shape tests/test_dsl/test_operator_merge.py
already verifies merges cleanly, replayably, and order-invariantly. Reaches
merge_success, the one named pattern the seventh slice's single-trace
corpus structurally cannot (extract_merge_preference_row never operates on
a shared ConversationTraceV1).

main() now combines both sources into one report and one pairs.jsonl: 4
rows (3 trace-scan + 1 merge_success), 3 render -- undo_then_redo is still
the only drop, for the same structural reason the seventh slice documented.

Reran the full training chain against the richer corpus: same deterministic
SFT checkpoint (last_loss=32.610084533691406, matching every prior
wf_smoke_v2/seed-0/8-step row), preference training now over 3 pairs
instead of 2 (9 steps, last_loss=0.531). Still not a training or
held-out-benefit claim -- a larger, more structurally diverse scratch
corpus, not evidence the signal helps the model.

harness.preference.replay_pairs bumped v2 -> v3.


Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
* SLM-418: build a demo replay-preference pairs corpus + run it (DSH5-10)

Seventh slice, stacked on #1124 (the sixth slice's row->PreferencePair
renderer). Adds scripts/build_replay_preference_pairs.py: builds one small,
deterministic, honestly-labeled fixture_or_scratch conversation exercising
three of the seven named patterns (edit-then-undo, undo-then-redo,
checkout-another-state), extracts rows, renders them, and writes a real
pairs.jsonl -- the first real, on-disk pairs corpus this feature line has
ever produced.

2 of 3 rows render; undo_then_redo never does, for a structural reason, not
a bug: for any deterministic zero-argument operator, redo and reapplying
the same operator at the same input state are, by construction, identical
text, so the renderer's own dedup guard correctly declines rather than
emitting a self-contradictory pair. Confirmed (again) while scoping this
slice: no real captured ConversationTraceV1 corpus exists anywhere in this
repo, and no harness ingests one -- build_symbolic_operator_corpus
synthesizes traces combinatorially from gold DSL records, it doesn't read
captured usage.

Also ran the first real end-to-end chain using DSH5-10 rows: a scratch SFT
checkpoint (wf_smoke_v2, seed 0, 8 steps -- last_loss identical to the
16+ prior independently-verified rows in the smoke-loop ledger, confirming
this is the same deterministic artifact those rows already verified), the
new demo pairs corpus, and one bounded scripts/train_preference.py train
call against it (6 steps, 2 pairs, last_loss=1.077). Both well under
MAX_RUN_MINUTES=3. Still not a training or held-out-benefit claim: n_pairs=2
on a scratch fixture says the pipeline runs end to end for real, nothing
about whether the signal helps the model or should train the DSH3-selected
policy head the issue actually asks about.

harness.preference.replay_pairs bumped v1 -> v2 (adds the new script+test
to its watched paths).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR

* SLM-418: add merge_success to the demo pairs corpus + rerun (DSH5-10)

Eighth slice, stacked on #1125 (the seventh slice's demo trace + first real
training run). Adds build_demo_merge_scenario to
scripts/build_replay_preference_pairs.py: a second scratch fixture (two
branches forked from a shared base, editing disjoint node refs), mirroring
the exact disjoint-target shape tests/test_dsl/test_operator_merge.py
already verifies merges cleanly, replayably, and order-invariantly. Reaches
merge_success, the one named pattern the seventh slice's single-trace
corpus structurally cannot (extract_merge_preference_row never operates on
a shared ConversationTraceV1).

main() now combines both sources into one report and one pairs.jsonl: 4
rows (3 trace-scan + 1 merge_success), 3 render -- undo_then_redo is still
the only drop, for the same structural reason the seventh slice documented.

Reran the full training chain against the richer corpus: same deterministic
SFT checkpoint (last_loss=32.610084533691406, matching every prior
wf_smoke_v2/seed-0/8-step row), preference training now over 3 pairs
instead of 2 (9 steps, last_loss=0.531). Still not a training or
held-out-benefit claim -- a larger, more structurally diverse scratch
corpus, not evidence the signal helps the model.

harness.preference.replay_pairs bumped v2 -> v3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR

* SLM-418: first step onto the DSH3 typed policy target (DSH5-10)

Ninth slice, stacked on #1126. Every prior slice since the sixth has
flagged the same gap: the TwoTower PreferencePair path (#1124-#1126) exists
only for tooling compatibility -- the disposition doc's own remaining-scope
note names typed_operator_policy.py's TypedOperatorPolicyScorer, not
TwoTower, as the issue's actual training target. This slice takes the
first real step onto it, and honestly narrows what's reachable there.

Adds src/slm_training/harnesses/experiments/argument_preference.py:
build_argument_preference_example renders only pronoun_focus_followup rows
into a TypedOperatorArgumentPreferenceExampleV1 -- the one named pattern
whose chosen/rejected actions are the same operator with a differing
argument for the same slot. The other six patterns are history controls
(undo/redo/checkout) or merge, none of which has a row in
OperatorPolicyInputV1.action_rows (built only from legal_set.entries);
wiring them needs a real scope decision this slice leaves open rather than
guesses at. typed_operator_argument_preference_loss is a Bradley-Terry
pairwise margin over CandidateScoringHead logits (surrogate, not textbook
DPO, same honesty class as train_preference.py's own dpo_loss).

Real, structural finding, not a bug: training on the actual
pronoun-focus-followup fixture provably cannot move the loss. Both sibling
refs share every field ReferenceModelViewV1 exposes -- they differ only by
semantic_fingerprint, which is deliberately stripped from every model input
for anti-identity-leakage. Two feature-identical candidates get
byte-identical embeddings through the shared-weight CandidateScoringHead
regardless of any parameter update, so the loss sits at
-log_sigmoid(0)=ln(2) structurally. Proven exact (pytest.approx) and
distinct from a mechanism failure via a second test with a synthetic,
feature-distinguishable pair, where training does reduce the loss.
Consequence for future corpus-building: only pairs whose candidates differ
in an allowed feature carry learnable signal for this scorer -- worth
knowing before anyone builds a real corpus and is puzzled why it plateaus.

harness.experiments.argument_preference registered fresh (v1). No change
to typed_operator_policy.py itself (only consumed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDctCUvBtGrdMvgNd34oHR

* SLM-418: add partial-rollback and fork-choose demo scenarios (DSH5-10) (#1128)

Stacked on #1127. Tenth slice of SLM-418's replay-grounded preference
work: extends scripts/build_replay_preference_pairs.py with two more
standalone scratch traces (build_demo_partial_rollback_scenario,
build_demo_fork_choose_scenario) covering the two named patterns
(partial_rollback, fork_then_choose_one_branch) extraction has supported
since the fifth slice but the demo TwoTower corpus never exercised.
6 of 7 named patterns are now in this corpus; only pronoun_focus_followup
remains absent, deliberately, since the ninth slice already covers it on
the separate typed_operator_policy path. Reran the full SFT -> build-pairs
-> preference-training chain against the richer 6-pair corpus.


Claude-Session: https://claude.ai/code/session_01Hzz1DzercsjW7yBQaddo4Z

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

@Tyler-R-Kendrick Tyler-R-Kendrick left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against current origin/main (57f5bdb). Not merging — this PR is superseded:

  1. Core capability already in main via #1131 (b349623). This PR's sixth slice adds harnesses/preference/replay_pairs.py (render_replay_preference_pair) as 'the first converter from an extracted row to PreferencePair'. Main already has exactly that: dsl/operators/replay_preference.py:550 preference_pair_from_replay_row and :610 preference_pairs_from_trace — 'Materialize one row into this repo's existing PreferencePair shape' / 'Convert every row grounded in trace to a PreferencePair', with 238 lines of tests in tests/test_dsl/test_replay_preference.py. Merging #1124 would create a second, parallel row→pair converter — a duplicate path the repo's harness rules explicitly forbid.

  2. The stacked remainder is also superseded. The branch's #1127 slice (first step onto TypedOperatorPolicyScorer + turn-depth/context-view scope) is covered in main by #1129 (57f5bdb): harnesses/preference/replay_preference_context_view_variants.py, scripts/run_replay_preference_context_view_ablation.py, evals/ambiguous_operator_followups.py, and the doc's own 'Sixth slice (2026-07-27)' section — an alternative, already-reviewed route over the same gap, including an honest no-benefit-at-fixture-scale result.

  3. Diverged doc + registry. Both sides rewrote docs/design/dsh5-10-replay-preference-rows.md's 'sixth slice' narrative differently (main: converter-in-operators v7 + ablation grid; this PR: converter-in-harnesses + demo corpus), and versions.json conflicts (main has dsl.operators.replay_preference v7; this PR registers a fresh harness.preference.replay_pairs v1 and still asserts v6).

The only content unique to this branch is the demo pairs corpus builder (scripts/build_replay_preference_pairs.py) and the fixture-scale train_preference.py smoke run from the #1125 slice. If that demo corpus is still wanted on top of main's converter, it needs a fresh, rebased PR that consumes preference_pairs_from_trace from dsl/operators/replay_preference.py rather than introducing a parallel renderer. Recommend closing this PR as superseded by #1131 + #1129.

@Tyler-R-Kendrick

Copy link
Copy Markdown
Owner Author

Closing as superseded — the converter landed via #1131 and the ablation scope via #1129 (see review comment).

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