Skip to content

fix(review): the four deferred #42 findings — causal finding attribution and a member-aware v2 lifecycle - #62

Merged
stranske merged 8 commits into
mainfrom
claude/pr42-deferred-four-rebased
Aug 23, 2026
Merged

fix(review): the four deferred #42 findings — causal finding attribution and a member-aware v2 lifecycle#62
stranske merged 8 commits into
mainfrom
claude/pr42-deferred-four-rebased

Conversation

@stranske

@stranske stranske commented Aug 23, 2026

Copy link
Copy Markdown
Owner

The four findings from the CodeRabbit review of #42 that were deliberately deferred because each one changes learning-loop semantics, where CLAUDE.md §2 says a wrong call corrupts the Brain quietly rather than failing loudly.

They were fixed and pushed to claude/profile-per-agent at 3f43569, but #42 had already merged at 9239e06b twelve minutes earlier, so that commit never reached main. This is the same commit cherry-picked onto af6654d6 — clean, and nothing else on main touched those files after #42 landed. The three inline threads on #42 carry the full per-finding replies; #42 also carries a note that they did not ship with the merge.


1. Durability inferred from an arbitrary run — research_subjects.py (thread)

resolve_round_durability picked an issue's outcome with ORDER BY r.ts DESC LIMIT 1 over every run that had ever targeted it, so a later review, audit or unrelated run silently replaced the delivery outcome and rewrote both per_arm_durability and the accepted influence edge.

The delivering run cannot be known at filing time — the issue is filed before anything implements it — so this is a schema change, not a query tweak:

  • finding_implemented, a new decision in the same research_subject_events log (no new table, so no migration), written by record_finding_implementation(). Brain-validated: the run must exist and its recorded target must be that issue. The arm is inherited from the filing, never taken as an argument, so a binding cannot re-attribute a finding.
  • No heuristic replaced the sort. Absent a binding, resolution requires exactly one scored run; two or more stays unresolved as ambiguous_outcome_runs. Newest / first / "looks like an implement" would each let an unrelated run rewrite an arm's durability, which is the defect.
  • The gap reports its own drain so it cannot latch quietly: unresolved_by_reason splits the causes, drainable_by_binding counts the only one that drains through nothing on its own (no_outcome_run drains when work lands, bound_run_has_no_outcome when the run is scored), and drain names the command.

2. Unvalidated arm attribution — research_subjects.py (thread)

record_finding_issue stored a caller-supplied arm, so a finding could be attributed to another participating agent and resolve_round_durability would then build an accepted edge from that agent's run. Membership now comes from round_arm_evidence() — the round's registered arm set, or a run bound to the round by experiment_id. A round with neither is refused outright.

The rule already existed at a second seam (completion_event_adapter's selected_arm_not_in_subject_set), so it is centralised in one reader rather than hand-rolled a third time — two seams disagreeing about who was in a round is exactly the drift that matters, because the learner treats each arm as independent evidence.

3. Arms registered for agents that never ran — partitioned_review.py (review body, reported outside the diff)

register_review_round recorded every --round-agents entry as an arm while run_plan dispatches every partition to --agent alone, so --agent gemini --round-agents gemini,cursor registered two arms of comparative evidence one of which never ran.

Neither option the reviewer offered was right: restricting the set to the executing agent breaks the real multi-seat case (--round-agents exists so several run invocations land on one subject). So an arm is admitted only on a record — the executing seat, a round-bound run, or a prior registration, meaning the set can only grow. That last clause is load-bearing: finding 2 validates against this set, so a resumed single-seat run that shrank it would start refusing the other seats' real findings. Declared arms with no record are dropped and named in arms_unproven.

4. v2 experiment lifecycle not member-aware — exp_abcd.py, experiment_recovery.py (thread)

followup measured eligibility per AGENT (<agent>.log from meta["agents"]) while prepare_arms writes one log per MEMBER, so no v2 experiment ever reached collect/evaluate and the v2 identity work produced zero evaluations_v2 rows — eaf2f7b half-fixed. Logs now come from experiment_members + exp_log_path, which keeps the legacy shape working because a legacy member's id is its agent (that is what recovered directories write).

collect()'s branch recovery had the matching gap — v2 branches are exp/<exp_id>-<member_id> — so it asked for a branch that cannot exist, read that as "the evidence is gone", and followup stamped followup-skip.json while the commits sat intact in the shared store. arm_diff now takes a keyword-only member_id.


Test gate

Three new tests plus selftest coverage, each with a deliberate-break → revert demonstration actually run:

Break Result
restore newest-run-wins selftest fails: per_arm_durability: {'codex': {'reverted': 1}} — the mis-attribution, reproduced
trust the caller's arm selftest fails: arm outside the round's set accepted: nobody
believe the --round-agents declaration 2 pytest failures
list meta["agents"] logs ['legacy-exp'] == ['legacy-exp', 'v2-exp'] — legacy passes, v2 skipped: the half-fixed state exactly
drop member_id from arm_diff branch recovery misses the member branch
  • test_partitioned_review.test_multi_agent_arm_set_is_admitted_only_on_a_round_bound_attempt — covers all three reviewer cases (dropped / correctly scheduled / never shrinking), then files from both real seats and asserts a third agent gets arm_not_in_subject_set
  • test_experiment_arm_identity.test_followup_collects_and_evaluates_a_v2_manifest — builds the manifest through the production launcher path (research_v2_arms_normalize_arm_members), asserts the member log names really are disjoint from the agent names, drives followup to collect+evaluate both a v2 and a legacy experiment, and checks idempotency
  • test_experiment_arm_identity.test_branch_recovery_resolves_the_v2_member_branch — stubs _git rather than arm_diff, so the real branch name is composed and asserted
  • research_subjects --selftest — both refusals (unknown arm; a real agent that is an arm of a different round), the ambiguity → binding → resolution path, and binding validation (unknown run, run that targeted a different issue, binding with no filing)

The existing round test was asserting the buggy behaviour (arms == ["gemini","cursor","codex"] from a gemini-only dispatch) and now asserts the fix.

Verification

python3 verify.py green, and run twice — once here and once from a mirror-shaped tree, per the rule that cmp-clean is not agreement. The live ~/.codex/orchestrator-mirror was deliberately not synced: ORCH_MIRROR was pointed at a scratch path, so the layout check runs while the dispatcher keeps running merged code and CLAUDE.md §1's manual circuit breaker stays intact.

Floor re-measured on this base rather than carried over from the pre-merge branch. Dedup findings recorded in the capability ledger notes for abcd-experiment and partitioned-review per CLAUDE.md §0 — in both cases the concept already existed and this wired or centralised it rather than building anything new.

🤖 Generated with Claude Code


Scope

Only the four findings from the #42 CodeRabbit review that were deliberately deferred there because each changes learning-loop semantics. Touches research_subjects.py, partitioned_review.py, exp_abcd.py, experiment_recovery.py, their two test files, and .verify-floor.json. No new table, no migration, no new event log, no flag flipped, no capability added.

Tasks

  • Replace the ORDER BY r.ts DESC LIMIT 1 durability inference with a persisted, Brain-validated finding_implemented binding; leave ambiguous targets unresolved and report the drain
  • Validate the filing arm against the round's registered/attempted arm set before writing FINDING_FILED
  • Admit review-round arms only on a record (executing seat, round-bound run, or prior registration); name dropped declarations in arms_unproven
  • Derive followup eligibility from experiment_members + exp_log_path, and pass member_id through branch recovery
  • One test per fix, each with a deliberate-break → revert demonstration actually run
  • Re-measure .verify-floor.json on the merge result (405), union-resolving the three conflicts rather than taking a side

Acceptance criteria

Workflow source: review follow-up to #42. Not a new issue — #42's review is the origin, and its three inline threads carry the per-finding detail.

…ttribution and a member-aware v2 lifecycle

All four change learning-loop semantics, which is why they were deferred: each one
lets the Brain train on a fiction rather than fail loudly.

1. research_subjects.resolve_round_durability inferred an issue's implementation
   outcome with `ORDER BY r.ts DESC LIMIT 1` over every run that had ever targeted
   it, so a later review, audit or unrelated run silently replaced the delivery
   outcome and rewrote both `per_arm_durability` and the accepted influence edge.
   The delivering run cannot be known at filing time (the issue is filed first), so
   this is a schema change, not a query tweak: a new `finding_implemented` decision
   in the SAME event log carries a Brain-validated binding (the run must exist and
   must have targeted that issue), and the arm is INHERITED from the filing so a
   binding can never re-attribute. With no binding, resolution now requires exactly
   one scored run; two or more stays unresolved as `ambiguous_outcome_runs`. The
   report carries the blocking quantity beside its drain — `unresolved_by_reason`,
   `drainable_by_binding` and the `finding-implemented` command that clears it —
   because `no_outcome_run` drains on its own and ambiguity drains through nothing.

2. record_finding_issue stored a caller-supplied `arm`, so a finding could be
   attributed to another participating agent and resolve_round_durability would
   then build an accepted edge from that agent's run. Membership is now read from
   the round's registered arm set or a run bound to the round. The rule already
   existed at a second seam (completion_event_adapter's
   `selected_arm_not_in_subject_set`), so the reader is centralised as
   research_subjects.round_arm_evidence rather than hand-rolled a third time.

3. partitioned_review.register_review_round recorded every `--round-agents` entry
   as an arm while run_plan dispatches every partition to `--agent` alone, so
   `--agent gemini --round-agents gemini,cursor` registered two arms of comparative
   evidence one of which never ran. An arm is now admitted only on a record — the
   executing seat, a round-bound run, or a prior registration (so the set can only
   grow, since #2 validates against it and a shrink would refuse real findings).
   Declared arms with no record are dropped and named in `arms_unproven`.

4. exp_abcd.followup measured eligibility per AGENT (`<agent>.log` from
   meta["agents"]) while prepare_arms writes one log per MEMBER, so no v2
   experiment ever reached collect/evaluate and the v2 identity work produced zero
   `evaluations_v2` rows — commit eaf2f7b half-fixed. Logs now come from
   experiment_members + exp_log_path, which keeps the legacy shape working because
   a legacy member's id IS its agent. collect()'s branch recovery had the matching
   gap (v2 branches are `exp/<exp_id>-<member_id>`), reporting a recoverable arm as
   evidence-gone and stamping followup-skip.json, so arm_diff now takes member_id.

Three new tests, each with a deliberate-break -> revert demonstration run:
  - test_multi_agent_arm_set_is_admitted_only_on_a_round_bound_attempt
    (break: believe the declaration -> 2 tests fail)
  - test_followup_collects_and_evaluates_a_v2_manifest
    (break: list meta["agents"] logs -> legacy passes, v2 skipped, i.e. the exact
     half-fixed state that made this invisible)
  - test_branch_recovery_resolves_the_v2_member_branch
    (break: drop member_id -> recovery misses the branch)
Plus research_subjects --selftest cases for both refusals (unknown arm, an agent
that is an arm of a DIFFERENT round), the ambiguity->binding->resolution path, and
binding validation (break 1: newest-run-wins -> durable becomes reverted; break 2:
trust the caller's arm -> a bogus arm is accepted).

verify.py green in BOTH trees: 390 passed, 0 failed, 0 skipped, 83/83 selftests,
5/5 gates, from this checkout and from a scratch exec-mirror layout built by
orch-sync-mirror.sh (ORCH_MIRROR pointed at a scratch path — the LIVE mirror is
deliberately NOT synced from an unmerged branch). Floor 387 -> 390, no ceiling
moved and nothing new skips.

Dedup findings recorded in the capability ledger notes for abcd-experiment and
partitioned-review, per CLAUDE.md §0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 33 minutes

Limit details: You’ve used the included review currently available. Your 74 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 92dd8b0f-2c22-48a9-94ea-845b23321756

📥 Commits

Reviewing files that changed from the base of the PR and between fc1fd42 and 9672ffb.

📒 Files selected for processing (7)
  • .verify-floor.json
  • exp_abcd.py
  • experiment_recovery.py
  • partitioned_review.py
  • research_subjects.py
  • test_experiment_arm_identity.py
  • test_partitioned_review.py

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

Only `.verify-floor.json` conflicted. Resolved as the UNION per that file's own rule: #59's
rationale (floor 387 -> 391, the improvement-log accessor's four tests) is retained, this branch's
entry is appended, and the count was RE-MEASURED on the merge result -- 394 -- rather than
arithmetic on either side's number. No ceiling moved; the three new tests need no agent CLI, ledger
or installed runtime, so nothing new is skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@agents-workflows-bot

agents-workflows-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Workflow source detected

PR #62 now has valid workflow source context (origin=review_followup).

No linked GitHub issue is required for this PR.

@agents-workflows-bot

agents-workflows-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Automated Status Summary

Head SHA: fe920fb
Latest Runs: ⏳ pending — Gate
Required: core tests (3.12): ⏳ pending, core tests (3.13): ⏳ pending, docker smoke: ⏳ pending, gate: ⏳ pending

Workflow / Job Result Logs
(no jobs reported) ⏳ pending

Updated automatically; will refresh on subsequent CI/Docker completions.


Keepalive checklist

Scope

No scope information available

Tasks

  • No tasks defined

Acceptance criteria

  • No acceptance criteria defined

@stranske

stranske commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Runner dispatch state for autofix on PR #62. Do not edit.

@stranske

stranske commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Runner dispatch state for codex on PR #62. Do not edit.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Agents Gate Followups. Do not edit.

@stranske

stranske commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Now fully green — and the earlier red was upstream, as measured

All 23 checks pass, Gate / gate included ("All Gate checks succeeded"). The five python ci jobs that were red earlier in this PR's life were the fleet-wide autofix-versions.env pin-file failure, fixed on main by bd6da2e5 ("the Gate was failing on a missing pin file, not on undrained lint") and picked up here by merging main. Nothing in this diff touches .github/.

That diagnosis was measured before the fix landed, not guessed. Against git archive origin/main in a scratch tree, running the Gate's exact commands on unmodified main and on this branch:

unmodified main this branch
black --check --line-length 100 clean, 194 files clean, 194 files
ruff check --select E4,E7,E9,F 37 errors 37 errors
rule histogram 4×E402, 7×E731, 19×E741, 1×F601, 6×F841 identical

The only textual difference between the two error lists was two line numbers drifting in exp_abcd.py because this change adds lines above them — so this branch adds zero lint findings.

verify.py counts on the final head

tree result
this checkout 405 passed, 0 failed, 0 skipped; 84/84 selftests; 5/5 gates
exec-mirror layout 394 passed + 11 skipped = 405 collected; 84/84 selftests; 5/5 gates — the 11 name their prerequisite (.github/workflows, docs, scripts absent: the mirror carries root-level modules only), which is exactly what main's own floor note predicts for this layout
CI 379 passed + 26 skipped = 405 collected (floor 405); 77/84 selftests; 3/5 gates — every skip naming its missing prerequisite, exactly at the agreed ceilings 26/26, 7/7, 2/2

Local and CI agree on 405 collected. The CI skips are the bare-runner profile .verify-floor.json documents (no populated capability ledger, no agent CLIs); no ceiling was raised by this branch.

The mirror run is the cmp-clean-is-not-agreement second run. The live ~/.codex/orchestrator-mirror was deliberately not synced — ORCH_MIRROR pointed at a scratch path — so CLAUDE.md §1's manual circuit breaker between an agent's change and the dispatcher stays intact.

On the floor

.verify-floor.json conflicted three times during this PR — with #59 (387→391), with the propensity work, and with the Gate fix (391→402). Each was resolved as the union of both rationales with the count re-measured on the merge result, never arithmetic on either side: 405 = 402 + this branch's 3, measured. Worth naming as a pattern rather than luck — it is the one file every concurrent branch touches, so N branches in flight produce N conflicts, and union-plus-remeasure is the only resolution that cannot leave the floor below reality.

Supersedes an earlier comment on this PR that reported the pre-merge counts and said "83/83 selftests"; the merge with main adds improvement_log.py, making it 84.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Keepalive Loop Reporter. Do not edit.

Tim Stranske and others added 2 commits August 23, 2026 09:30
Only `.verify-floor.json` conflicted, for the third time. Resolved the same way: the UNION of both
rationales, with the count RE-MEASURED on the merge result (405 = main's 402 + this branch's 3)
rather than computed from either side. No ceiling moved; the three new tests build their own
tmp_path Brain and manifests, so nothing new is skipped on any machine.

Picks up bd6da2e, which fixes the `autofix-versions.env` pin-file failure that was reddening
lint/typecheck/pytest on every consumer PR — including this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stranske

Copy link
Copy Markdown
Owner Author

Fixed: added <!-- workflow-source:review_followup --> to the PR body, which is the accurate source — this PR's origin is #42's CodeRabbit review, not a new issue. Also filled the keepalive checklist the summary reported as empty (#### Scope / #### Tasks / #### Acceptance criteria), with every box checked, since the work is complete and verified in all three trees. Deliberately no unchecked tasks: an unchecked box plus an agent:* label is what drives keepalive rounds, and there is nothing here for an agent to continue.

🤖 Addressed by Claude Code

…anch

Fourth `.verify-floor.json` conflict for this branch, and the first under the EQUALITY that #56
introduced — which is the mechanism working as designed, not friction: that change's own rationale
says git conflict detection is what enforces "measure on the merge result, not on the branch".

Measured accordingly: 405 = main's 402 + this branch's 3. Under the old minimum, keeping the larger
number would have passed; under the equality it has to be measured, and it was. Union of both
rationales retained in the note. No ceiling moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	.verify-floor.json
#	langsmith-fleet-worker-attempt.json
@stranske
stranske merged commit a247a55 into main Aug 23, 2026
54 checks passed
stranske pushed a commit that referenced this pull request Aug 23, 2026
Seventh floor union on this branch. main a247a55 records 410; the merge result
MEASURES 411 with `pytest --collect-only -q` -- main's 410 plus this branch's one
test. Measured, never derived: #56 made `collected` an EQUALITY, so an assumed
number is a hard RED.

#69 also flips CI coverage ON, so this branch's merge is the first to be measured
under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 23, 2026
langsmith-fleet-worker-attempt.json is a per-run CI provenance record, not
source. reusable-codex-run.yml (in stranske/Workflows) rewrites it on every
invocation and uploads it as artifact langsmith-fleet-v1-worker-attempt-<pr>.
It became tracked in 2118f57, and because gitignore does not apply to an
already-tracked path, the runner's `git add -A` commit step staged it on every
run: ten commits across PRs #59/#61/#62/#63, one add/add conflict per concurrent
PR, and a silently widened diff on each.

Nothing reads it from the tree. The langsmith-fleet/v1 consumers here
(langsmith_pull.py, langsmith_fetch.py) ingest the NDJSON Actions artifact
`langsmith-fleet.ndjson`; neither names this path. The 30-day artifact upload
already provides the replacement path, so untracking loses no provenance.
stranske/Workflows reached the same verdict for its own copy.

The ignore entry sits outside the synced WORKFLOWS STATUS FILES block, since
that block mirrors a consumer template which does not carry this pattern and
would drop it on the next rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 23, 2026
langsmith-fleet-worker-attempt.json is a per-run CI provenance record, not
source. reusable-codex-run.yml (in stranske/Workflows) rewrites it on every
invocation and uploads it as artifact langsmith-fleet-v1-worker-attempt-<pr>.
It became tracked in 2118f57, and because gitignore does not apply to an
already-tracked path, the runner's `git add -A` commit step staged it on every
run: ten commits across PRs #59/#61/#62/#63, one add/add conflict per concurrent
PR, and a silently widened diff on each.

Nothing reads it from the tree. The langsmith-fleet/v1 consumers here
(langsmith_pull.py, langsmith_fetch.py) ingest the NDJSON Actions artifact
`langsmith-fleet.ndjson`; neither names this path. The 30-day artifact upload
already provides the replacement path, so untracking loses no provenance.
stranske/Workflows reached the same verdict for its own copy.

The ignore entry sits outside the synced WORKFLOWS STATUS FILES block, since
that block mirrors a consumer template which does not carry this pattern and
would drop it on the next rewrite.

Co-authored-by: t <t@e>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 23, 2026
…rop a claim #3210 falsifies

This branch ended up with two independent explanations of the same rule, because a
concurrent session added `4b5764a` while this branch already carried its own block.
Both were correct; having both in a file whose header is specifically about
gitignore-comment discipline is not.

Kept from the second block, because the first did not have it:
- the artifact was tracked from 2118f57, not merely "on main until today";
- ten commits across PRs #59/#61/#62/#63 rewrote it, one add/add conflict per
  concurrent PR, silently widening unrelated diffs;
- untracking loses no provenance — the producer already uploads the same bytes as
  Actions artifact `langsmith-fleet-v1-worker-attempt-<pr>` with 30-day retention,
  which is the only reason the file is written at all.

Dropped from it, because it is about to become false: "that block mirrors Workflows'
templates/consumer-repo/.gitignore, which does NOT carry this pattern". Workflows
PR #3210 adds exactly these two patterns to that template at Template-Version 6. The
placement above the managed block is still right, but the reason is that a version
bump rewrites the block wholesale — not that the template lacks the pattern.

Also drops that block's bare literal `langsmith-fleet-worker-attempt.json`, which was
already subsumed by the root-anchored `/langsmith-fleet*.json` above it.

Comments only. Verified identical behaviour with `git check-ignore`: the root artifact
and artifacts/langsmith/ stay ignored, docs/contracts/schemas/langsmith-fleet-v1.schema.json
and langsmith_*.py stay committable, and no tracked file is shadowed by any pattern.
427 collected = floor 427; black, ruff and the 11 guard tests unchanged.
@stranske
stranske deleted the claude/pr42-deferred-four-rebased branch August 23, 2026 22:37
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.

1 participant