Skip to content

feat(retrieval): annotate retrieved beliefs that slot-conflict with an active lock (#1365) - #1444

Closed
robotrocketscience wants to merge 7 commits into
mainfrom
feat/issue-1365-lock-conflict-annotation
Closed

feat(retrieval): annotate retrieved beliefs that slot-conflict with an active lock (#1365)#1444
robotrocketscience wants to merge 7 commits into
mainfrom
feat/issue-1365-lock-conflict-annotation

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #1365.

Builds AC1–AC3. AC4 (slot-tuple ordering) already shipped via #1393; AC5 was struck by the operator ruling of 2026-08-06 and replaced by a live-store reach measurement, which is included here.

What was actually missing

The measurement half of #1175 proposal 2 shipped in #1244 and then sat unreachable. lock_consistency.lock_conflict_annotations had zero production callers — the only references anywhere in the tree were its own __all__ entry and its unit tests. So a retrieved belief asserting retry limit is 9 against a lock saying 5 reached the agent with nothing marking the disagreement.

This PR is wiring, not new logic. The suppression rules, the symmetry, and the slot-scoping were all already built and tested; they just never ran in production.

Design — the 5-tuple is deliberately not widened

Per the operator ruling of 2026-08-06. retrieve_with_tiers returns a bare list[Belief] through three intervening layers (retrieve(), hook_search.search_for_prompt, hook._retrieve) with no per-belief metadata channel; adding a sixth tuple element would have touched 37 call sites across src/, tests/ and benchmarks/.

Instead the annotation travels on a module-level _LAST_LOCK_CONFLICTS snapshot with a last_lock_conflict_annotations() accessor, cloning the existing _LAST_TELEMETRY / last_lane_telemetry() shape one-for-one.

The snapshot is cleared in two places, and the second is the one that matters. retrieve_with_tiers has no try/finally and publishes as its penultimate statement, so any raise between entry and publish would serve the previous call's annotations as this call's. And retrieve_v2's HRR-structural branch returns without ever entering retrieve_with_tiers — on a lane that is default-ON. That is precisely the stale-snapshot defect #1366 closed for lane firings; without both resets this would have reintroduced it one field over.

One deviation from the letter of the ruling, flagged rather than buried. The ruling said to read the accessor "beside the existing last_lane_telemetry() call". I read it inside _split_belief_lines instead. That is the single render boundary all three formatters (_format_hits, _format_hits_with_session_start, _format_baseline_hits) funnel through, so the annotation cannot be present in one and missed in another — a partial render would look shipped while leaving most injected packs unannotated. annotations stays an explicit keyword parameter so tests pin it without a real retrieve(). Happy to move it if you'd rather have the literal placement.

Constraints honoured

  • ANNOTATE, never DROP. The belief is still injected; the agent adjudicates (v3.0 PHILOSOPHY: natural-language-relatedness gate — deterministic vs embedding #605). Not squeamishness — slot keys come from a preceding-alphabetic-token heuristic that emits junk like b88fd4=9.0, and a heuristic that can invent a key must not delete context.
  • The filter stays in lock_consistency. value_compare.extract_values is untouched — that blast radius is what fix(value_compare): keep non-finite numeric slots out of the comparator (#1227) #1228 declined. Raw slots are passed in on both sides because lock_conflict_annotations applies annotation_slots itself; filtering at the call site would double-suppress.
  • Symmetric and slot-scoped. Verified in the code rather than assumed: annotation_slots is applied to the locks at lock_consistency.py:131 and each candidate at :136. A belief carrying both a suppressed version literal and a genuine disagreement is still annotated on the genuine one.
  • Flag defaults off, env-first, and with it off the conflict is not computed at all — the gate precedes the compute, not just the render.

Verification

Mutation-checked in both directions. The issue asks that removing the suppression make a test go red rather than move a number:

  • making annotation_slots a passthrough → 10 tests red, including both new suppression arms;
  • disabling the compute block (if False and ...) → the two end-to-end arms red, so they distinguish the wiring rather than restating the resolver.

Full suite: 7510 passed, 70 skipped, 71 xfailed.

One test I wrote and then fixed: the flag-off arm originally asserted only that the resolver returned False, which would have passed even if the compute ran unconditionally. It now drives a real retrieve_with_tiers against a store with a conflicting lock, with extract_values patched to raise.

Reach — measured, and the kill experiment struck rather than run

AC5 called for a LongMemEval-S A/B. longmemeval_adapter.py runs include_locked=False, so lock_consistency short-circuits on every question and the flag-on arm is byte-identical to flag-off. That is a guaranteed null, which means no measurement, not no effect — the R3 IDF-clip failure mode. Struck by operator ruling and replaced with benchmarks/lock_conflict_annotation_reach.py.

It is not a query replay. hook_audit stores only prompt_prefix, truncated at 200 chars with the median at the cap — treating it as the query carried a ~4x error in a prior retrieval A/B. So it scores the belief ids the audit records as actually injected per fire, reconstructing no query at all. The store is opened read_only=True, since a plain MemoryStore open runs DDL, migrations and a scope-id backfill.

log fires pack reach candidate rate
hook_audit.jsonl 48 17 = 35.4% 29/1623 = 1.79%
hook_audit.jsonl.1 131 64 = 48.9% 90/3943 = 2.28%
pooled 179 81 = 45.3% 119/5566 = 2.14%

Both logs are post-#1016-B, so pooling is legitimate. The two arms disagree more than the pooled figure suggests (35.4% vs 48.9%) and both are reported rather than the flattering one — n is small either way. 8 distinct locks are implicated, top lock 34.4%.

The 2.14% candidate rate corroborates the 1.38% #1244 measured post-suppression on a different population.

Read the 45.3% as reach, not as quality. It says roughly one injected pack in two would carry at least one annotation; it does not say the annotations are useful. Nothing here measures whether naming the conflict improves what the agent does — that needs a labelled corpus this store does not have, which is why the flag ships off.

Summary by Sourcery

Annotate retrieved beliefs that numerically conflict with active locks behind a configurable, default-off flag, expose the annotations to the hook renderer via a process snapshot, and measure the feature’s reach on the live store.

New Features:

  • Introduce a use_lock_conflict_annotations configuration flag (env, kwarg, TOML) that controls whether retrieval computes lock-conflict annotations.
  • Expose a read-only last_lock_conflict_annotations() snapshot mapping belief ids to conflicting lock ids for the most recent retrieval call.
  • Render conflicting locks on injected belief tags as a conflicts_with="<lock id>" attribute when the annotation flag is enabled.
  • Add a benchmark script to measure how often injected packs and candidates would carry lock-conflict annotations on the live store.

Enhancements:

  • Wire lock_conflict_annotations into retrieve_with_tiers, computing annotations over L2.5 and L1 unlocked candidates versus the locked set when the flag is enabled.
  • Reset the lock-conflict annotation snapshot on both retrieve_with_tiers entry and retrieve_v2’s structural lane to avoid stale annotations across calls.
  • Extend documentation and changelog to describe the lock-conflict annotation feature, its suppression rules, and configuration precedence.
  • Update hook rendering to consult the annotation snapshot at the shared _split_belief_lines boundary and add conflict attributes without changing existing attribute order.

Documentation:

  • Document the use_lock_conflict_annotations flag, its default-off behavior, suppression rules, and configuration precedence in the user configuration guide.
  • Note the new lock-conflict annotation behavior and its measured reach in the v4 changelog.

Tests:

  • Add end-to-end and unit tests validating flag resolution, snapshot immutability, suppression behavior, annotation rendering, compute gating, and snapshot clearing across retrieval calls.

Chores:

  • Introduce a live-store reach measurement harness for lock-conflict annotations that replaces the previously planned LongMemEval A/B kill experiment.

…ff flag (#1365)

#1175 proposal 2's measurement half shipped in #1244 and has sat unwired
since: lock_consistency.lock_conflict_annotations had zero production
callers, only tests. This wires it into retrieve_with_tiers.

Adds AELFRICE_LOCK_CONFLICT_ANNOTATIONS / [retrieval]
use_lock_conflict_annotations, env-first and default OFF, and a
_LAST_LOCK_CONFLICTS module snapshot with a last_lock_conflict_annotations()
accessor mirroring _LAST_TELEMETRY. The 5-tuple is deliberately not
widened — that would touch every retrieve_with_tiers call site — per the
operator ruling of 2026-08-06.

The snapshot is cleared on entry to retrieve_with_tiers and in
retrieve_v2's HRR-structural branch, which returns without reaching the
former. Without both resets a raise or a structural hit would serve the
previous call's annotations as this call's.

Scoped to the L2.5 and L1 candidates per the issue; 'out' also carries
the locks, HRR-expand, spine and BFS hits. Suppression stays in
lock_consistency, which applies it to both sides itself.
…f tag (#1365)

Adds a conflicts_with attribute naming the lock a retrieved belief
disagrees with. ANNOTATE, never DROP — the belief still reaches the
agent, which adjudicates (#605).

The snapshot is read in _split_belief_lines rather than at its three
call sites (_format_hits, _format_hits_with_session_start,
_format_baseline_hits). That is the single render boundary, so the
annotation cannot be present in one formatter and missed in another —
a partial render would look shipped while leaving most injected packs
unannotated.

Appended after the existing attributes so none of them move, and empty
unless the default-off flag resolves true: with the flag off the
rendered line is byte-identical. 'annotations' stays explicitly
passable so tests can pin it without a real retrieve().
28 arms over the flag, the process snapshot, the compute inside
retrieve_with_tiers and the render.

Mutation-checked in both directions, per the issue's AC4:
- making annotation_slots a passthrough turns 10 tests red, including
  both new suppression arms — the suppression is load-bearing, not a
  number that merely moves.
- disabling the compute block turns the two end-to-end arms red, so
  they distinguish the wiring rather than restating the resolver.

The symmetry arm is slot-scoped on purpose: a belief carrying BOTH a
suppressed version literal AND a real numeric disagreement must still
be annotated on the real one. The precedence and stale-snapshot arms
assert a presence and an absence on the same input, so neither can be
satisfied by an empty result.
…#1365)

Replaces the AC5 kill experiment, struck by the operator on 2026-08-06 as
a guaranteed null: longmemeval_adapter runs include_locked=False, so
lock_consistency short-circuits on every question and the flag-on arm is
byte-identical to flag-off.

Deliberately not a query replay. hook_audit stores only prompt_prefix,
truncated at 200 chars with the median AT the cap, which carried a ~4x
error in a prior retrieval A/B. This scores the belief ids the audit
records as actually injected per fire, so no query is reconstructed:
non-locked injected beliefs are the candidates, the fire's own locked set
is the lock population.

Opens the store read_only=True — a plain MemoryStore open runs DDL,
migrations and a scope-id backfill, which a diagnostic must not do.

Reports beliefs since deleted and lock-free fires as counts rather than
dropping them silently.
Records the env var, TOML key and precedence, that it annotates rather
than drops, and the three measured suppressions with the 6.12% -> 1.38%
figure they were derived from.
… reach

Names what #1244 shipped but left unwired, why the 5-tuple was not
widened, the two snapshot resets, and reports both audit logs' reach
rather than the pooled figure alone.
@robotrocketscience robotrocketscience added the author-garsecg PR coordination mutex label Aug 9, 2026

@sourcery-ai sourcery-ai 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.

Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robotrocketscience, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c775d41-2219-4d8b-86ee-d2d83d62131c

📥 Commits

Reviewing files that changed from the base of the PR and between 968d9db and a2fc5aa.

📒 Files selected for processing (6)
  • CHANGELOG/v4.md
  • benchmarks/lock_conflict_annotation_reach.py
  • docs/user/CONFIG.md
  • src/aelfrice/hook.py
  • src/aelfrice/retrieval.py
  • tests/test_lock_conflict_annotations_1365.py

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.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 697 changed lines (limit: 200)
  • 6 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@sourcery-ai

sourcery-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Wires previously-unreachable lock-conflict annotation logic into retrieval and rendering behind a new default-off flag, exposes a process-level snapshot API for these annotations, ensures stale state is cleared on all retrieval paths, documents the configuration, adds tests to mutation-check suppression and wiring behavior, and introduces a benchmark to measure live-store reach of the annotations instead of a null A/B experiment.

Sequence diagram for lock-conflict annotation during retrieval and render

sequenceDiagram
    actor User
    participant hook as hook
    participant retrieve_v2 as retrieve_v2
    participant retrieve_with_tiers as retrieve_with_tiers
    participant lock_consistency as lock_conflict_annotations
    participant snapshot as _LAST_LOCK_CONFLICTS
    participant render as _split_belief_lines

    User->>hook: user_prompt_submit
    hook->>retrieve_v2: retrieve_v2(query)
    alt hrr_structural_hit
        retrieve_v2->>snapshot: _reset_last_lock_conflict_annotations({})
        retrieve_v2-->>hook: structural_result
    else normal_retrieval
        retrieve_v2->>retrieve_with_tiers: retrieve_with_tiers(query)
        retrieve_with_tiers->>snapshot: _reset_last_lock_conflict_annotations({})
        retrieve_with_tiers->>retrieve_with_tiers: is_lock_conflict_annotations_enabled()
        opt [lock_conflict_annotations_enabled and locked]
            retrieve_with_tiers->>lock_consistency: lock_conflict_annotations(candidates, locks)
            lock_consistency-->>retrieve_with_tiers: annotations
            retrieve_with_tiers->>snapshot: _reset_last_lock_conflict_annotations(annotations)
        end
        retrieve_with_tiers-->>retrieve_v2: hits
        retrieve_v2-->>hook: hits
    end

    hook->>render: _split_belief_lines(hits, annotations=None)
    render->>render: is_lock_conflict_annotations_enabled()
    alt annotations_enabled
        render->>snapshot: last_lock_conflict_annotations()
        snapshot-->>render: annotations
        render-->>hook: belief_lines with conflicts_with
    else annotations_disabled
        render-->>hook: belief_lines without conflicts_with
    end
Loading

File-Level Changes

Change Details Files
Add and plumb a default-off configuration flag and process snapshot for lock-conflict annotations into retrieval and rendering, ensuring computation only runs when enabled and that annotations are exposed read-only via an accessor.
  • Introduce LOCK_CONFLICT_ANNOTATIONS_FLAG/ENV, env/TOML/kwarg-resolved is_lock_conflict_annotations_enabled, and a private _env_lock_conflict_annotations_override helper in retrieval.
  • Add a module-level _LAST_LOCK_CONFLICTS snapshot with immutable MappingProxyType backing, last_lock_conflict_annotations accessor, and _reset_last_lock_conflict_annotations helper mirroring lane telemetry.
  • Wire lock_conflict_annotations + extract_values into retrieve_with_tiers to compute belief-id→lock-id mappings for unlocked L2.5/L1 candidates when the flag is on, and reset the snapshot on retrieve_with_tiers entry and retrieve_v2 structural hits to avoid stale annotations.
  • Update hook._split_belief_lines to optionally accept an annotations mapping, lazily read last_lock_conflict_annotations() when the flag is enabled, and append a conflicts_with attribute to belief tags when an annotation is present while keeping output byte-identical when disabled.
src/aelfrice/retrieval.py
src/aelfrice/hook.py
Document the new lock-conflict annotation flag and behavior in user-facing configuration docs and release notes.
  • Describe use_lock_conflict_annotations semantics, precedence order, suppression behavior, and example rendered tag in CONFIG.md.
  • Add a detailed v4 changelog entry explaining the rationale, design (5-tuple not widened, process snapshot, dual resets), default-off behavior, and the live-store reach measurement replacing the struck LongMemEval A/B.
docs/user/CONFIG.md
CHANGELOG/v4.md
Add focused tests that cover flag resolution, snapshot behavior, render wiring, suppression invariants, and end-to-end retrieval behavior for lock-conflict annotations.
  • Test env/TOML/kwarg precedence and unrecognized env handling for is_lock_conflict_annotations_enabled.
  • Assert that last_lock_conflict_annotations returns a read-only snapshot, copies its source, and that render output is unchanged without annotations but correctly names conflicts when provided or when the flag is on.
  • Mutation-check suppression rules by asserting both absence and presence of annotations on specific literals, including symmetric and slot-scoped behavior.
  • Exercise retrieve_with_tiers with a conflicting lock store to ensure compute does not run when the flag is off (via patched extract_values), does annotate when on, and clears the snapshot between calls.
tests/test_lock_conflict_annotations_1365.py
Introduce a benchmark script to measure the live-store reach of lock-conflict annotations using audit logs and a read-only store, instead of a null-effect LongMemEval A/B.
  • Add lock_conflict_annotation_reach.py that reads hook_audit.jsonl, reconstructs injected packs from belief ids, fetches content from a read-only MemoryStore, and runs lock_conflict_annotations to compute pack-level reach and candidate-level annotation rates.
  • Handle edge cases like fires without locks, missing beliefs, and per-lock counts, and print a structured summary including path info, excluded fires, and top implicated locks.
benchmarks/lock_conflict_annotation_reach.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1365 Implement a configurable lock-conflict annotation computation in retrieval: compute conflicts inside retrieve_with_tiers for L2.5 and L1 candidates against the active locks, keep existing suppression logic in lock_consistency, expose results via an accessor, and guard everything behind a default-off flag.
#1365 Render the lock-conflict annotation on injected belief tags in hook.py, adding an attribute that names the conflicting lock id, with suppression rules applied symmetrically and slot-scoped, and tests that mutation-check the suppression and verify that a belief with both a suppressed literal and a genuine disagreement is still annotated on the genuine one.
#1365 Provide an instrumented measurement of the feature’s reach/impact as required by the issue’s kill-experiment acceptance criterion, and document how it behaves in practice (originally a LongMemEval-S A/B; later operator-approved to be replaced with a live-store reach measurement).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Idnn:2026-08-09T07:02:37Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Review: the wiring is right, the thing it wires up is 0% precise on live data

I verified the mechanism first and it holds up well — see "what checks out" at the
bottom, it is a lot. Then I asked what the annotation actually says on the same
data the PR measures, and that is where this stops.

One blocking finding, and it needs your judgment rather than a patch from me.


BLOCKING — all 119 live annotations are subject-mismatched

The reach measurement is right and I reproduced it unmodified: 179 fires, 81 packs
(45.3%), 119 of 5,566 candidates (2.14%). What it does not report is which slot
fired. I added three lines to print that, and then read all 39 unique
belief↔lock pairs
.

Not one of them shares a subject. 88 of 119 (73.9%) are enum-slot hits, and
annotation_slots does not filter enum at all:

return ValueSlots(numeric=kept, enum=slots.enum)   # lock_consistency.py:107

All three ratified suppression rules — single-value-per-key, version keys,
calendar years — are numeric-only. Enum passes through untouched, and
find_conflicts fires on same-category / disjoint-group with no subject binding.

78 of 119 (65.5%) trace to two long locks whose only enum slot is an incidental
word.
Reproduced end to end against the PR head with only the flag set:

LOCK 9c231df5b948d3d0 (979 chars)
  "aelfrice retrieve_v2 temporal_sort decays beliefs against real datetime.now()…"
  sole enum slot: ('necessity', 'optional', 'optional')
     ← from "…thread an optional now into retrieve_v2" in the closing sentence

CANDIDATE "Waiting for required checks."
  enum slot: ('necessity', 'required', 'required')

  <belief id="…" lock="none" conflicts_with="9c231df5b948d3d0">Waiting for
  required checks.</belief>

A note about temporal-decay clock injection is offered to the agent as the thing
that CI-check phrasing disagrees with. The other 43 come the same way from a
414-char rule ending buttons (disabled), whose sole enum slot is
('default_state','default-off','disabled').

Slot histogram over the 119: enum default_state 43, enum necessity 42, then a
numeric tail (option 20, step 4, completeness 3, and five singletons).

So the headline generalises the wrong way. "45.3% of packs would carry at least
one annotation" is 45.3% of packs carrying a false one
— on this store the
precision of the shipped configuration is 0/39. The PR's motivating example
(retry limit is 9 vs a lock saying 5) is a numeric conflict, and numeric is
the third of the traffic that the suppression rules were actually designed for.

This is the #1244 measurement being reached for the first time, so it is
introduced here even though lock_consistency.py is byte-identical to main
zero production callers before this PR.

What you need to decide (I have not guessed, all three are defensible):

  1. Gate enum on a shared subject — require some lexical overlap outside the
    enum token itself before an enum-only conflict annotates. Kills ~74% of the
    traffic and most of the remainder is the numeric case the rules already cover.
  2. Drop enum from annotation entirely, ship numeric-only. Smallest diff, and
    the 2.14% headline becomes ~0.56%; the reach claim has to be rewritten.
  3. Suppress enum slots drawn from locks over some length — the two offenders
    are 979 and 414 chars, and a long lock is exactly where an incidental word gets
    picked up as the lock's "value". Narrow, but it is a threshold with no
    principle behind it and I would not pick this one.

Should-fix

The pooled per-lock pair is one arm's numbers presented as pooled. The
CHANGELOG and PR body say the 119 are "spread across 8 distinct locks with the top
lock at 34.4%". Pooled it is 9 locks and 35.3%; 8 / 34.4% are
hook_audit.jsonl.1's single-log figures. Every additive figure in that sentence
is correct — only the concentration pair is wrong. Related: the committed script
takes a single --audit, so it structurally cannot emit the pooled row at all,
which means the published pooled figure is not re-derivable by the script that
ships with it. That is the rule this repo has for published numbers, and it is the
one thing here that would let the error recur silently.

The stated reason AC5 was struck is false. The entry says
"longmemeval_adapter.py runs include_locked=False, so lock_consistency
short-circuits on every question"
. It does not: retrieve_with_tiers loads
store.list_locked_beliefs() unconditionally (retrieval.py:4761), takes no
include_locked, and the compute gate is if lock_conflicts_on and locked:
include_locked is applied after, at retrieval.py:5327, filtering an
already-annotated pack. The conclusion (a guaranteed null) may well still hold via
the bench store carrying no locks, but the published mechanism is not the one in
the code, and this is a claim used to justify striking a mandated experiment.

"AC4 already shipped via #1393" — #1393 is OPEN and unmerged, and it is
#1370's PR, not this lane's. Its diff does contain the hoist, but until it lands
enum slot order still varies across PYTHONHASHSEED, so AC4 is claimed against
something that has not shipped.

Cost. Flag-on measures ~13.2 ms per retrieve against a 2 ms hot-path budget.
The guard is if lock_conflicts_on and locked: with no candidate-emptiness test,
over the full uncapped list_locked_beliefs() (25 locks, median 473 chars here).
Worth a cap or an early-out before this is ever flipped on.

One mutation survives. Changing _env_lock_conflict_annotations_override's
_ENV_FALSY arm from return False to return None leaves the full suite green
(7,624 passed). The env-falsy path is documented precedence but unpinned — a
AELFRICE_LOCK_CONFLICT_ANNOTATIONS=0 that silently fell through to TOML would
not be caught.

The flag hold. This is a fifth default-off retrieval flag. The 2026-07-31
standing ruling says don't add one while the measurement gap is open, and ruling 8
of 2026-08-06 said that hold is not waived for render-path flags. There is a
real argument the other way — the operator struck AC5 on this very issue and
accepted a reach measurement instead — but reach is not the quality question the
hold is about, and the PR does not mention the hold anywhere. Operator call, not
mine
; it just has to be put in front of them rather than left implicit.

On ruling 8's ordering, being fair: there is no code dependency. #1359 is a UI
ask (make the injected block visible, add a turn-off command, minimise/enlarge).
Nothing here consumes anything it would introduce; the render change is confined to
_split_belief_lines, which exists today, and flag-off is byte-identical. The only
couplings are merge contention on that same render boundary and the UX point that
conflicts_with is agent-facing and invisible to the user until #1359 lands. I
would not block on the ordering — but the PR should say it is landing out of it.


What checks out (verified, not assumed)

Flagging attn:unblock: the enum-precision question is yours, and the pooled-stat
and AC5-mechanism corrections should land with whatever you choose.

@robotrocketscience robotrocketscience added attn:unblock Needs answer from another session and removed attn:review Needs review (PR open, awaiting reviewer) labels Aug 9, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Idnn:2026-08-09T07:41:39Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Author self-review — do not merge this as it stands. Precision is 0/42 on live data.

I published 45.3% pack reach as this PR's headline. I measured how often the mechanism fires and never checked what it fired on. Having now done that, the honest statement is: 45.3% of packs would carry at least one false annotation. Reach is not precision, and I presented the former as if it settled the latter.

Independently reproduced over the same 184 audit fires / 5,566 candidates.

Every annotation is subject-mismatched

42 unique (belief, lock) pairs fire. I inspected all of them. None is a real disagreement.

Enum slots are never suppressed. annotation_slots ends return ValueSlots(numeric=kept, enum=slots.enum) — all three ratified #1175 rules filter numeric only. find_conflicts then fires on same-category/different-member with no subject binding at all. 96 of 129 firing events (74%) are enum-driven.

Minimal reproduction:

lock  9c231df5b948d3d0  (979 chars, a note about retrieve_v2 temporal decay)
      → enum slot ('necessity', 'optional', 'optional')   ← from the word "optional"
cand  "Waiting for required checks."
      → enum slot ('necessity', 'required', 'required')   ← from the word "required"
⇒ conflicts_with="9c231df5b948d3d0"

Two long locks produce 65% of all firings, each on a single incidental word. The longer the locked text, the likelier some passing word becomes its "value" for the whole belief — so this gets worse exactly on the substantial locks users care most about.

The numeric half is no better. I assumed the 11 numeric-only pairs were the real ones. They are not — all 11 are bare-word key collisions:

key lock candidate actually about
step 3 4 an install-surface step vs step 4/5 of an unrelated issue
option 1 2 a ratified philosophy Option 1 vs "option 2 is cleanest" for landing a PR
run 27806055357 2 a workflow run id vs "Run 2" of an experiment
commit -3 2 "commit-3" as a name vs an ordinal step
g 4 2 "G4" the migration vs an unrelated G2

This is the failure the issue itself predicted and then waved through: slot keys come from a preceding-alphabetic-token heuristic, and a bare step/option/run is far too weak to establish that two numbers describe the same quantity. The issue's answer was ANNOTATE-not-DROP so the agent adjudicates — but at 0/42, every annotation is noise the agent must spend attention dismissing, which is a cost with no offsetting benefit.

What I got wrong, specifically

I read return ValueSlots(numeric=kept, enum=slots.enum) while writing this and noted the asymmetry — then dismissed it as "fine, the rules are numeric-specific" without checking whether enum slots could conflict at all. They can, unsuppressed, and they are the dominant firing path. My changelog and CONFIG.md both say the suppressions apply, without qualifying that they reach only half the slot space. Both are wrong as written.

My AC3 tests pass because they exercise retry limit is 5 vs 9 — a hand-built case with a genuinely shared subject. Nothing in the suite samples real locks, which is why 0/42 got through a green suite.

Not a wiring bug

The wiring is correct and does what the ratified design specifies. What is refuted is #1175 proposal 2's premise — that slot-conflict detection is precise enough to put in front of the agent. The mechanism has been correct-and-unwired since #1244; this PR is the first thing to measure it against real locks, and it does not survive that.

Blast radius today is nil: the flag is default-OFF and no default path changed. The cost of merging as-is is the misleading record, not user harm.

@operator — this needs your call, and I've flagged attn:decisions-needed rather than choosing:

  1. Hold this PR pending an enum-suppression + subject-binding design (proposal 2 gets re-scoped, not closed).
  2. Merge default-off with the claims corrected and a precision issue filed, so the wiring is not lost and the flag stays off until precision exists.
  3. Close it and retire proposal 2 on the measurement — 0/42 with the dominant path having no subject binding is a strong refutation.

I lean 3, with 2 as the fallback if you want the wiring kept. What should not happen is this merging with a "45.3% reach" headline that reads as a benefit.

@robotrocketscience robotrocketscience added the attn:decisions-needed Escalated to user for decision label Aug 9, 2026
…ys off

I published 45.3% pack reach as the headline and never checked what the
mechanism fired on. Inspecting all 42 firing (belief, lock) pairs across
184 audit fires: every one is subject-mismatched.

annotation_slots filters numeric only — enum passes through unfiltered
and conflicts on same-category/different-member with no subject binding,
which is 74% of firings. A 979-char lock containing 'optional' annotates
'Waiting for required checks.' Two long locks produce 65% of firings on
one incidental word each, so it degrades on exactly the substantial
locks users care about. The numeric half fails identically on bare-word
keys: step 3 vs step 4, option 1 vs option 2, run 27806055357 vs Run 2.

So 45.3% is 45.3% of packs carrying a FALSE annotation. Reach is not
precision and I reported the former as if it settled the latter.

Corrects the changelog and CONFIG.md, and adds a test pinning the enum
false positive so a green suite cannot be read as the mechanism being
sound. What is refuted is #1175 proposal 2's premise, not the wiring.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-08-09T16:14:57Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Operator ruling: close this PR, and retire #1175 proposal 2 on the measurement

Put to the operator with your three options and your lean. Ruling is option 3.

Your finding reproduces — independently, from the code rather than the audit

I did not take the 0/42 on trust. The mechanism is exactly as you describe:

annotation_slots ends return ValueSlots(numeric=kept, enum=slots.enum), and
its own docstring says so — "Enum slots pass through untouched". All three
ratified suppressions filter numeric only. find_conflicts then fires on
same category, disjoint group_id, and there is no subject predicate
anywhere in that branch. Numeric slots at least carry a key; enum slots carry
a category and nothing else, so the dominant path is the one with strictly
less
subject information.

Minimal reproduction on main, unrelated subjects:

lock : "The sentiment hook lane is optional and default-off; ..."
       enum -> [('default_state','default-off'), ('necessity','optional')]
cand : "Waiting for required checks."
       enum -> [('necessity','required')]
find_conflicts -> [('enum', 'necessity', 'required', 'optional')]   FIRES

That is the whole defect in five lines, and no amount of numeric-side tuning
reaches it.

Why retire rather than re-scope

Binding enum values to a shared referent is a research problem, not a missing
rule. The project is deterministic by ratified policy (#605, no embeddings), and
the existing subject proxy is a preceding-alphabetic-token heuristic that the
issue itself predicted would be too weak. "Step 1 vs 2", "option 1 vs 2",
"run 27806055357 vs Run 2" are not near-misses to tighten; they are the
heuristic working as designed on text that does not support it.

The fallback — merge default-off with corrected claims — also runs into the
standing hold on new default-off flags (ruled 2026-07-31 on #1268,
reaffirmed as ruling 8 on 2026-08-06, explicitly not exempting new arms). A flag
that is known 0/42 is a poor candidate to be the exception.

Credit where it is due

You published the reach figure, then went and checked what it fired on, then
wrote up the refutation of your own PR with a minimal reproduction. That is the
outcome the process is for. The 45.3% number was never wrong — it measured reach
— and the error was presenting it where precision was the question.

What is kept

The measurement, not the wiring. The 0/42 result and the enum-has-no-subject-
binding mechanism are the durable output and belong on #1365/#1175 as the reason
proposal 2 is retired, so nobody re-proposes it from the same premise. The code
is recoverable from git if a subject-binding design ever exists.

Two claims are wrong on main regardless of this PR's fate and should not be
left standing: the CHANGELOG and CONFIG.md both say the #1175 suppressions
apply, without qualifying that they reach only the numeric half of the slot
space. Filing that separately.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Closed per the operator ruling above: #1175 proposal 2 is retired on the 0/42 measurement. Not a wiring defect — the premise is refuted.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Correction to my own comment above

I wrote that the CHANGELOG and CONFIG.md claims are wrong on main regardless of this PR, and said I would file that separately. I checked before filing, and that is wrong — there is nothing to file.

So both misleading statements were in this PR's diff, and they close with it. Nothing on main needs correcting.

Flagging rather than quietly dropping it, since 'I will file a follow-up' is exactly the kind of claim that goes unchecked.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Operator ruling (2026-08-09): close this and retire #1175 proposal 2. The measurement is a refutation, not a tuning problem.

The self-review is correct, and I reproduced its central claim in code

Both halves verified independently at this branch's head:

So the dominant firing path (74% of events) is structurally incapable of establishing that two beliefs are talking about the same thing. That is not a threshold that can be tuned; optional in a 979-character note about temporal decay and required in "Waiting for required checks" will always be a same-category/different-member pair.

Why the numeric half does not rescue it

The numeric suppressions do work — #1175 took that path from 6.12% to 1.38%. But the 11 surviving numeric pairs are all bare-word key collisions (step 3 vs step 4, option 1 vs option 2, run <id> vs Run 2, G4 vs G2), which is the preceding-alphabetic-token heuristic failing exactly as the issue predicted before waving it through.

ANNOTATE-not-DROP was the answer to imprecision: let the agent adjudicate. At 0/42, every annotation is attention the agent spends dismissing noise, with no offsetting benefit. That inverts the design's own justification.

Why not "merge default-off and keep the wiring"

Two reasons beyond the measurement.

  1. The standing ruling to hold new default-off flags pending a gold set was reaffirmed today, and this is squarely within it.
  2. This repo already carries one shipped-but-inert mechanism whose boost arm is a verified no-op. A second one, whose only live measurement is 0/42, would be a liability in the record rather than an asset — and the wiring is recoverable from this branch's history if a subject-bound design ever wants it.

Credit where it is due

The author found this by measuring what the mechanism fired on after publishing what it fired at, and escalated rather than shipping a green suite. The 45.3% "reach" headline would have read as a benefit indefinitely — AC3's tests pass because they use retry limit is 5 vs 9, a hand-built case with a genuinely shared subject, and nothing in the suite samples real locks.

Closing the PR, closing #1365, and recording the refutation on #1175 so proposal 2 is not rediscovered.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-08-09T16:32:49Z]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:decisions-needed Escalated to user for decision attn:unblock Needs answer from another session author-garsecg PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(retrieval): annotate retrieved beliefs that slot-conflict with an active lock (#1175 proposal 2)

1 participant