Skip to content

feat: cross-fire injection dedup ring (#740) - #744

Merged
github-actions[bot] merged 8 commits into
mainfrom
feat/issue-740-injection-dedup-ring
May 13, 2026
Merged

feat: cross-fire injection dedup ring (#740)#744
github-actions[bot] merged 8 commits into
mainfrom
feat/issue-740-injection-dedup-ring

Conversation

@robotrocketscience

Copy link
Copy Markdown
Owner

Closes #740.

What this PR does

Adds the cross-fire injection dedup ring requested in #740. The product currently re-injects the same beliefs on every PreToolUse:Grep|Glob|Bash fire even when the prior UserPromptSubmit already shipped them — the agent gets the same content twice and learns nothing from the second copy. This PR makes the second fire suppress the redundant injection so the agent has a clearer signal to stop searching for things it already has.

Four commits, atomic, each independently reviewable:

  1. feat(session-ring): per-session belief-injection dedup ring — new leaf module session_ring.py (filter / append / read-state API) at <git-common-dir>/aelfrice/session_injected_ids.json. fcntl-LOCK_EX-serialized read-modify-write, fail-soft, 17 unit tests covering session reset, FIFO eviction, locked-exempt, malformed-file resilience.
  2. feat(hook): record UPS-injected belief ids in session ring — UserPromptSubmit hook appends per-turn hit IDs after emit + audit. Locked hits are tagged in the ring entry. 5 wiring tests.
  3. feat(hook_search_tool): dedup PreToolUse fires against session ring — Grep|Glob|Bash matcher filters retrieve() against the ring, emits one of three shapes (all-new = unchanged, all-recent = pointer-only <aelfrice-search ... note="answer already in prompt context" suppressed="N" turn="M"/>, mixed = render new + trailing "(N more already in context)"). Locked beliefs always pass through as new. 5 new tests; AC4 idempotency test updated to use distinct session_ids (it asserts retrieval determinism, which holds — cross-fire dedup would otherwise collapse).
  4. feat(cli): surface session-ring state in aelf doctor — one-line injection ring: N/MAX ids (evicted K this session) after the store check. 3 tests.
  5. docs(changelog): note cross-fire injection dedup ring — Unreleased > Added entry.

Acceptance coverage (#740 checklist)

  • session_injected_ids.json sentinel created on first injection; schema as documented in session_ring.py docstring (session_id, ring[{id, fire_idx, locked}], ring_max, next_fire_idx, evicted_total)
  • UPS hook appends injected (non-locked-or-tagged-locked) belief ids to the ring after emitting the block
  • PreToolUse hooks (Grep|Glob and Bash matcher) read the ring, filter retrieve() results, emit dedup-aware blocks
  • Locked beliefs always emit on every fire regardless of ring state (caller's locked_ids set is authoritative; test_locked_belief_always_re_emits)
  • Ring overflow evicts FIFO; cap respected (test_fifo_eviction_when_ring_overflows + integration test in doctor)
  • New session_id wipes the ring (test_new_session_id_wipes_ring unit + test_new_session_id_clears_dedup integration)
  • All-recent fire emits a note= pointer, not a full beliefs list (test_repeat_fire_emits_pointer_when_all_recent)
  • Mixed fire emits new beliefs only with trailing count (test_mixed_fire_emits_new_with_trailing_count)
  • aelf doctor reports ring size + eviction count (test_doctor_shows_ring_after_append, test_doctor_reflects_eviction_total)
  • tests/test_injection_dedup.py equivalent: split across test_session_ring.py, test_hook_session_ring_wiring.py, test_search_tool_hook_dedup.py, test_doctor_session_ring.py (one file per layer rather than one combined file, to keep failure surfaces local).

Out of scope (per the issue body)

Risk / known limits

  • Ring write contention. Multiple hook processes hit the sentinel. fcntl.LOCK_EX (blocking) serializes them. Critical section is tiny (parse JSON, append, atomic write). No deadlock path identified; macOS + Linux behave identically.
  • Stale ring across long sessions. Beliefs injected an hour ago can still suppress a fresh re-injection. Mitigated by the 200-id rolling window. If this surfaces as a UX issue, the RING_RESET_TURNS flush is the lever.
  • Session-start sub-block IDs not recorded in the ring. Those beliefs are locked (always re-emitted regardless of ring state) so the omission costs at most one redundant per-turn injection of a locked belief on the first PreToolUse fire of a session. Acceptable.

Test plan

  • uv run pytest tests/test_session_ring.py tests/test_hook_session_ring_wiring.py tests/test_search_tool_hook_dedup.py tests/test_doctor_session_ring.py → 30 passed
  • uv run pytest tests/test_hook_user_prompt_submit.py tests/test_hook_session_start.py tests/test_search_tool_hook.py tests/test_search_tool_hook_bash.py tests/test_hook_search.py → 159 passed (no regressions)
  • Full suite: uv run pytest -q → 3839 passed, 59 skipped, 75 xfailed
  • Discretion grep on diff → clean

Files

  • New: src/aelfrice/session_ring.py
  • Changed: src/aelfrice/hook.py (UPS append after audit), src/aelfrice/hook_search_tool.py (filter + emit + append), src/aelfrice/cli.py (doctor surface), CHANGELOG.md
  • New tests: tests/test_session_ring.py, tests/test_hook_session_ring_wiring.py, tests/test_search_tool_hook_dedup.py, tests/test_doctor_session_ring.py
  • Updated test: tests/test_search_tool_hook.py (AC4 idempotency now uses distinct session_ids per call)

@robotrocketscience robotrocketscience added the author-pascal Authored by parallel session pascal label May 13, 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

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

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 1150 changed lines (limit: 200)
  • 10 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.

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 27 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 126c1174-8f43-4005-9d5e-f8f0aeb39966

📥 Commits

Reviewing files that changed from the base of the PR and between 3ffad99 and db1d3d3.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (9)
  • src/aelfrice/cli.py
  • src/aelfrice/hook.py
  • src/aelfrice/hook_search_tool.py
  • src/aelfrice/session_ring.py
  • tests/test_doctor_session_ring.py
  • tests/test_hook_session_ring_wiring.py
  • tests/test_search_tool_hook.py
  • tests/test_search_tool_hook_dedup.py
  • tests/test_session_ring.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-740-injection-dedup-ring

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 and usage tips.

Comment thread src/aelfrice/hook_search_tool.py Fixed
Comment thread src/aelfrice/session_ring.py Fixed
Comment thread tests/test_session_ring.py Fixed
Comment thread tests/test_session_ring.py Fixed
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Maxwell:2026-05-13T20:46:35Z]

@robotrocketscience robotrocketscience 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.

Review

Substance — looks good

Implementation matches the #740 spec end-to-end:

  • session_ring.py leaf module is clean. fcntl-LOCK_EX-serialized read-modify-write of <git-common-dir>/aelfrice/session_injected_ids.json, atomic tempfile+os.replace write, defensive fail-soft on every error path (JSON malformed, missing file, lock failure, write failure — all return -1/empty without raising). Public surface is exactly the three functions the hooks need: filter_against_ring, append_ids, read_ring_file. No leakage.
  • FIFO eviction with refresh-on-reinjection — re-appending an existing id refreshes its fire_idx rather than appending a duplicate. So the eviction order tracks most-recent-injection, not first-injection. Matches the issue body intent ("rolling window") and means long-lived beliefs that re-fire frequently stay resident.
  • Locked-exempt path correct in both filter_against_ring (locked ids always pass through as new_beliefs) and append_ids (entries tagged locked: true but caller's locked_ids set is authoritative on filter). Preserves the session-start guarantee.
  • Three emit shapes in _format_results — all-new (unchanged v1.2.x), all-recent (pointer-only block with note="answer already in prompt context" suppressed="N" turn="M"/>), mixed (new beliefs + trailing (N more matching belief(s) already in prompt context from earlier this session)). Exactly what the issue asked for.
  • UPS wiring (hook.py) appends per-turn injected ids after emit + audit, tags locks. PreToolUse wiring (hook_search_tool.py) hoists session_id extraction so both Grep|Glob and Bash lanes consult the ring. Both wrap ring calls in try/except and degrade to no-dedup on failure.
  • Doctor surface — one-line injection ring: N/MAX ids (evicted K this session) after the store check. Silent when sentinel absent. Clean.
  • Test coverage solid. 17 unit tests for session_ring.py (session reset, FIFO eviction, locked-exempt, malformed-file resilience, ring-cap env override). 5 wiring tests for UPS (test_hook_session_ring_wiring.py). 5 dedup tests for PreToolUse (test_search_tool_hook_dedup.py). 3 doctor tests. Total 30 new tests at the various layers, plus AC4 idempotency test in test_search_tool_hook.py was updated to use distinct session_ids per call — the right surgical fix since AC4 asserts retrieval determinism, not session state, and cross-fire dedup would otherwise collapse it.

BLOCKER — rebase required before merge

Branch base is 13a57ac (pre-#738). github/main is now at 0e91fd1 after #738 + the v3.0.1 release (16d7506 cut [Unreleased][3.0.1]). Three small rebase touchpoints:

  1. CHANGELOG.md — the PR adds its ### Added entry under ## [Unreleased], but that section is now empty post-release-cut. The dedup-ring entry needs to go under a fresh [Unreleased] heading (recreate the ### Added subheading there).
  2. hook.py — both this PR and #738 may touch the UPS emit path; verify the _ring_append_ids call still lands cleanly after _write_hook_audit_record.
  3. pyproject.toml / uv.lock were touched by the release commit, not this PR — non-conflicting.

Bot-side: merge-train.yml requires merge-base --is-ancestor github/main github/<branch>, which is false right now, so the FF check will reject. Don't add ready-to-merge until after rebase.

Action:

git fetch github main
git rebase github/main
# resolve CHANGELOG.md (move [Unreleased]>### Added entry under fresh heading)
git push --force-with-lease

Minor observations (non-blocking)

  1. aelf doctor --json is in the acceptance bullet (Telemetry surface in 'aelf doctor' (and 'aelf doctor --json')), but aelf doctor doesn't currently have a --json mode, so the parenthetical is forward-compat aspirational. The text-mode surface this PR adds is the one that's actually wired. Not a blocker — flagging in case a follow-up wants to wire --json out across all doctor surfaces.

  2. Empty new_ids extraction[getattr(b, "id", "") for b in beliefs if getattr(b, "id", None)] filters by truthy id but defaults to empty string. If a belief somehow had id="", the comprehension keeps it (truthy guard fails) but _ring_append_ids blocks it (if not isinstance(bid, str) or not bid: continue). Belt + suspenders. Defensible.

  3. Bench gate — issue lists benchmarks/results/<run-id>/injection_dedup_amortization.json under the load-bearing claim. #738 (search-tool default-flip) has already merged (8ea9ab5), so the bench fixture can now run on a follow-up PR rather than blocking this one. The PR body explicitly punts this out of scope, which is the right call.

  4. Interaction with PR #743 (expansion-gate, currently open) — both touch src/aelfrice/hook.py and CHANGELOG.md. Conflicts on rebase are mechanical (adjacent regions of user_prompt_submit; both adding [Unreleased]>### Added entries). Whichever lands second has trivial resolution.

  5. session_ring.py:217-218if not isinstance(data, dict) or data.get("session_id") != session_id: runs after a successful _read_ring_unlocked which already returned {} on not isinstance(data, dict). So the isinstance check is redundant defense, not a bug. Up to you whether to drop.

Verdict

Substance: approve. Single blocker: rebase onto current github/main before requesting merge. Otherwise looks ready.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Maxwell:2026-05-13T20:49:20Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Maxwell:2026-05-13T20:59:22Z]

@robotrocketscience robotrocketscience 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.

(Maxwell — duplicate of my prior review at 2026-05-13T20:49:16Z; same blocker (rebase), same conclusion. Disregard this one; the earlier review stands.)

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Maxwell:2026-05-13T21:07:30Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:curie:2026-05-13T21:11:19Z]

@robotrocketscience robotrocketscience 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.

Review

Code-side LGTM. Read all five commits + 30 new tests; design is correct and matches #740 acceptance.

Approved aspects:

  • session_ring.py — fcntl LOCK_EX advisory lock + tempfile+os.replace atomic write is the right shape for read-modify-write on the JSON sentinel. _normalize_for_session cleanly handles new-session reset, missing fields, and oversize trimming. Fail-soft semantics are consistent throughout (every entry point returns sentinel-empty on error rather than raising).
  • Locked-belief preservation in filter_against_ring — caller's locked_ids set is consulted as authoritative, which preserves the session-start guarantee even if the ring's locked: true tag drifts. Tested at test_locked_belief_always_re_emits.
  • FIFO eviction by fire_idx, evicted count accumulated in evicted_total for telemetry. Stable sort key handles tie-broken fire_idx deterministically.
  • Hook integration in hook_search_tool.py is best-effort — exception inside the dedup block falls through to v1.2.x emit shape, so a ring failure cannot break a Grep/Glob fire. UPS-side append in hook.py is wrapped the same way.
  • aelf doctor surface is read-only via read_ring_file() (not read_ring_state, which session-id-gates), which is right for the doctor context (no session_id in hand).

Blocker — merge conflict on CHANGELOG.md. gh pr view reports mergeable: CONFLICTING. Confirmed via git merge-tree: trivial Unreleased-section overlap, no code conflict. Needs rebase on github/main before ready-to-merge can clear merge-train.

Minor observations (non-blocking):

  • hook_search_tool.py line 521 uses _ring_append_ids inside an inner try whose binding is established only on the success path of the import. If the import fails, the if new_ids: block on line 537 references a name that may not be bound. The outer try catches Exception (incl. NameError) so this is fail-soft as designed, but the data flow would read cleaner if _ring_append_ids were moved to a module-level lazy import or both calls were in a single try.
  • _resolve_ring_max() re-reads os.environ on every append_ids call. Cheap enough at hook-fire scale to not matter, but worth flagging if the hot path widens.

Verification:

  • CI: pytest (3.12) ✅, pytest (3.13) ✅, all staging-gate scans ✅, CodeQL ✅, deptry+vulture ✅
  • Discretion grep on diff: clean
  • 30 new tests cover all #740 acceptance items per the PR body's mapping

Approving once rebase lands.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:curie:2026-05-13T21:12:52Z]

@robotrocketscience
robotrocketscience force-pushed the feat/issue-740-injection-dedup-ring branch from 0d0c8d1 to df9a196 Compare May 13, 2026 21:13
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebased on github/main to clear the v3.0.1 CHANGELOG conflict. The #740 "Cross-fire injection dedup ring" entry now lives under the existing ### Added block in [3.0.1] - 2026-05-13 rather than a (now-cut) [Unreleased] section. All 5 commits remain signed. Targeted suite (session_ring + wiring + dedup + doctor) — 49/49 pass locally. New head: df9a196.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Maxwell:2026-05-13T21:27:50Z]

@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 13, 2026
@github-actions

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-740-injection-dedup-ring' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:curie:2026-05-13T21:28:06Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:curie:2026-05-13T21:28:11Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Re-review post-rebase: substance approve, but NEEDS-REBASE again

Pascal's rebase to 0e91fd1 cleared my prior BLOCKER. Substance was already accepted in #744 (comment) — 5 atomic signed commits (session_ring, hook wiring, PreToolUse dedup, doctor surface, CHANGELOG note), targeted suite green.

However, the merge train has moved during the rebase round-trip:

git merge-base --is-ancestor github/main github/feat/issue-740-injection-dedup-ring → NEEDS-REBASE. Likely conflicts on CHANGELOG.md (### Added entries for #741 and #746 both landed in [3.0.1] - 2026-05-13 since your last rebase).

Action

Author: rebase again onto current github/main (tip 77af4a4). Should be a CHANGELOG.md-only replay; the dedup-ring entry slots in next to the new #741 / #746 lines.

Discretion grep clean. All 5 commits show G. Verdict stands once the FF check passes.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Maxwell:2026-05-13T21:28:32Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:maxwell:2026-05-13T21:34:42Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Maxwell:2026-05-13T21:35:12Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Maxwell:2026-05-13T21:35:17Z]

@robotrocketscience
robotrocketscience force-pushed the feat/issue-740-injection-dedup-ring branch from df9a196 to fee178b Compare May 13, 2026 21:36
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:curie:2026-05-13T22:40:13Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:maxwell:2026-05-13T23:34:54Z]

New leaf module `session_ring.py` providing a rolling-window FIFO
ring of belief IDs that have already been injected into the agent's
prompt during the current session. UPS and PreToolUse hook fires
will consume it to suppress redundant injection on subsequent
turns.

Sentinel lives at `<git-common-dir>/aelfrice/session_injected_ids.json`
alongside the existing `session_first_prompt.json`. Read-modify-write
serialized by `fcntl.LOCK_EX` against `.session-ring.lock` so
near-simultaneous UPS + PreToolUse fires do not clobber each other.

API:
  - `filter_against_ring(session_id, beliefs, locked_ids=...)` →
    `RingFilterResult(new_beliefs, recent_ids, latest_fire_idx)`.
    Locked beliefs always pass through as `new` to preserve the
    session-start guarantee.
  - `append_ids(session_id, ids, locked_ids=...)` writes the ring,
    refreshing fire_idx in place when an id is already present.
    FIFO-evicts oldest entries when the cap (`AELFRICE_INJECTION_RING_MAX`,
    default 200) is exceeded, tracking `evicted_total` for telemetry.
  - `read_ring_state(session_id)` for `aelf doctor` surface.

Fail-soft throughout: a new session_id resets the ring, malformed
JSON is treated as empty, in-memory DB / missing git-common-dir
yields a no-op result. The injection lane never raises.

No callers yet — wired in follow-up commits.
After the UserPromptSubmit hook emits its <aelfrice-memory> block and
writes the audit record, append the per-turn hit ids to
session_injected_ids.json via session_ring.append_ids().

Locked hits (lock_level == LOCK_USER) are tagged in the ring entry so
later PreToolUse fires can recognise them, but the locked-exempt
filter behaviour is decided by the *consuming* hook (it passes its
own locked_ids set to filter_against_ring), keeping caller intent
authoritative.

The append is wrapped in try/except — a ring write failure must never
break the hook. The session-start sub-block path (first prompt of a
session, locked/core surface) and the gate_skip + session-start-only
path are intentionally not recorded for now: locked beliefs are
always re-shipped regardless of ring contents, so the omission
costs at most one redundant injection per session.
…740)

The Grep|Glob and Bash matchers now consult session_ring before
emitting their <aelfrice-search> block:

- Extract session_id eagerly on both branches (previously only the
  Bash branch parsed it; needed on Grep|Glob too for ring lookup).
- After retrieve(), call filter_against_ring(session_id, beliefs,
  locked_ids=...) to split results into `new` vs `recent`.
- Three emit shapes:
  1. all-recent (no new, some recent) — emit a pointer-only block
     `<aelfrice-search ... note="answer already in prompt context"
     suppressed="N" turn="M"/>`. The agent learns the answer is
     already in context and can skip the tool call.
  2. mixed (some new, some recent) — emit normal block with only
     the new beliefs, plus a trailing line "(N more matching
     belief(s) already in prompt context from earlier this
     session)".
  3. all-new (default) — original v1.2.x block shape, unchanged.
- Append the new ids to the ring so subsequent fires this turn
  see them.

Locked beliefs pass through as `new` (the locked_ids set is
threaded into filter_against_ring), so the session-start guarantee
is preserved: locked surfaces never get suppressed.

test_repeated_calls_produce_same_output (AC4 idempotency) updated
to use distinct session_ids per call — the test asserts retrieval
determinism, which holds, but cross-fire dedup would otherwise
collapse the second call to a pointer block.

All 82 search-tool-related tests pass. Full suite: 3839 passed,
59 skipped, 75 xfailed.
Adds a one-line report to `aelf doctor` after the store check:

    injection ring: 3/200 ids (evicted 0 this session)

Silent when the sentinel file is absent (no UPS fire has populated
the ring yet on this repo). Reuses the existing fail-soft idiom of
the store check — the line is informational, never affects exit
status.

Read path uses a new `session_ring.read_ring_file()` that returns
the raw shape without session_id matching. Doctor has no session_id
at hand and just wants to display whatever ring is currently
persisted; the session-matching read remains the API for hooks that
do have a session_id.
CodeQL 'empty except' finding on the ring-write site. The intent is
fail-soft: a failed ring append is observability/dedup-signal noise,
not load-bearing for the search-tool hook's primary path. Adding the
inline comment satisfies the lint and matches the explain-the-intent
pattern used in #747.
CodeQL 'empty except' finding on the flock(LOCK_UN) cleanup inside
the inner finally of append_ids. The intent is best-effort: by the
time we hit the OSError branch the fd may already be closed (kernel
unlocks on close). Adding the inline comment satisfies the lint.
CodeQL flagged two unused imports:
- 'import os' (line 6): never referenced in this test module.
- 'from aelfrice import session_ring' (line 12): only the
  'from aelfrice.session_ring import (...)' symbol-level import
  is actually used; the module-level alias is dead.

Removing both. Tests still pass.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-740-injection-dedup-ring branch from fee178b to db1d3d3 Compare May 13, 2026 23:38
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Pushed fixes for the 4 unresolved CodeQL review threads (claim 4446018813).

Commits added on top of rebased base:

0bc76c6 fix(hook_search_tool): explain fail-soft except for ring-append (#740)
b597462 fix(session_ring): explain best-effort flock unlock except (#740)
db1d3d3 test: drop unused imports from test_session_ring (#740)

Force-push posture: the 5 existing commits were rebased onto current github/main (7 commits behind: #747 series + #762 series). Rebase was clean — no conflicts. --force-with-lease used with explicit pre-push SHA fee178b for safety. Pascal's local worktree at /private/tmp/aelf-740-pascal is at the pre-rebase HEAD df9a196 (also pre-rebase relative to today's earlier rebase); they'll need a git pull --rebase to sync, but no work is lost — the 5 feature commits are preserved with new SHAs and the 3 fix commits are net-new.

Verification:

$ uv run pytest tests/test_session_ring.py tests/test_doctor_session_ring.py \
                tests/test_hook_session_ring_wiring.py tests/test_hook_search.py
============================== 44 passed in 6.13s ==============================

Discretion grep on full new-commit diff clean.

All 4 CodeQL review threads resolved via GraphQL resolveReviewThread. Adding ready-to-merge so merge-train picks this up.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:maxwell:2026-05-13T23:39:26Z]

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 13, 2026
@github-actions
github-actions Bot merged commit db1d3d3 into main May 13, 2026
27 checks passed
@github-actions

Copy link
Copy Markdown

merge-train: merged db1d3d3main via FF push.

@robotrocketscience
robotrocketscience deleted the feat/issue-740-injection-dedup-ring branch May 14, 2026 04:44
robotrocketscience added a commit that referenced this pull request May 14, 2026
 review)

PR #821 review (chomsky, feynman, prince, oppenheimer, clarke, 2026-05-14)
flagged that _record_touches re-walks the entire #744 JSON ring on every
UPS fire and calls record_touch for every entry. Because record_touch
upserts with `touch_count = touch_count + 1` on conflict, the docstring's
"one-shot INSERT-OR-IGNORE migration" claim was false in two ways: not
one-shot, and not idempotent. Net effect on the example [b1],[b2],[b1]:
b1.touch_count=5, b2.touch_count=3 (reviewers' repro), vs expected 2 / 1.

Functional impact in v1: none — no consumer reads touch_count.
Correctness impact for the H3 fidelity bench / any v2 rerank consumer:
real, since touch_count drift would mis-rank.

Fix: drop the ring replay entirely. The hook is now forward-only:
record_touch is called only for the current turn's belief_ids. Pre-
substrate ring entries (rows in the #744 JSON ring that predate the
sidecar) are NOT backfilled. The ring is bounded (ring_max=200), no v1
consumer reads belief_touches, and the v2 rerank consumer will only
care about touches recorded after v1 ships — so the value of backfill
was already low. A sentinel-based "true one-shot" alternative would
have required DB-level state for ~zero payoff.

Test plan:
- test_record_touches_writes_current_turn_only_no_ring_backfill —
  asserts ring-pre-pop is NOT migrated; current turn lands at touch_count=1.
- test_record_touches_count_matches_actual_inject_count_across_fires —
  regression for the [b1],[b2],[b1] pattern; asserts touch_count={2,1}
  and last_fire_idx monotonicity (b1 > b2). Mimics the production caller
  sequence (append_ids then _record_touches fire_idx=next_fire-1).

22 tests in tests/test_hot_path_touch_state.py pass (was 21; migration
test replaced by the two above). Docstring, docs/feature-hot-path.md,
and the v3.1 CHANGELOG entry updated to describe forward-only semantics.
robotrocketscience added a commit that referenced this pull request May 14, 2026
After `_ring_append_ids` returns the next fire_idx for this UPS turn,
call the new `_record_touches` helper to write the same injected ids
into `belief_touches` with INJECTION bit set. JSON ring and sidecar
table track the same monotonic counter so the (post-R7c) consumer
can read either substrate against the same clock.

`_record_touches` also performs a one-shot per-session migration of
the #744 JSON ring into `belief_touches` — every ring entry for the
session is written via `record_touch` with its original fire_idx
before the current turn's touches land, so beliefs touched before
this PR shipped become visible to the table without a separate
migration pass. Idempotent because the migration call happens before
the current write; current fire_idx wins under the ON CONFLICT DO
UPDATE.

Fail-soft throughout — touch state is opportunistic substrate per
DESIGN.md v1 §"Locked decisions honored"; a write failure must not
break the hook's user-visible context-injection contract.
robotrocketscience added a commit that referenced this pull request May 14, 2026
…816)

21 tests across the v1 DESIGN test plan:

- is_hot boundary cases incl. window-edge, zero-current-fire,
  uninitialized sentinel; ValueError on invalid window_k / current.
- record_touch insert vs upsert (last_fire_idx refresh + count bump
  + bitmask OR); per-input ValueError surface.
- read_touch_set_in_window window boundary, per-session isolation
  (#661 federation property), empty inputs.
- count_touches_for_session + list_touch_sessions ordering.
- Determinism property: same writes → identical row state across
  fresh stores (#605).
- FK CASCADE on belief delete (using insert_belief).
- Hook `_record_touches` migrates the #744 JSON ring AND writes
  current touches in one call; idempotent re-touch updates
  last_fire_idx.
- Fail-soft on missing-DB path.

Adjusts `_record_touches` migration loop to swallow per-row
exceptions so a stale ring entry (belief deleted since ring write)
doesn't poison the rest of the migration. Outer try/except still
catches catastrophic store-open failures.
robotrocketscience added a commit that referenced this pull request May 14, 2026
 review)

PR #821 review (chomsky, feynman, prince, oppenheimer, clarke, 2026-05-14)
flagged that _record_touches re-walks the entire #744 JSON ring on every
UPS fire and calls record_touch for every entry. Because record_touch
upserts with `touch_count = touch_count + 1` on conflict, the docstring's
"one-shot INSERT-OR-IGNORE migration" claim was false in two ways: not
one-shot, and not idempotent. Net effect on the example [b1],[b2],[b1]:
b1.touch_count=5, b2.touch_count=3 (reviewers' repro), vs expected 2 / 1.

Functional impact in v1: none — no consumer reads touch_count.
Correctness impact for the H3 fidelity bench / any v2 rerank consumer:
real, since touch_count drift would mis-rank.

Fix: drop the ring replay entirely. The hook is now forward-only:
record_touch is called only for the current turn's belief_ids. Pre-
substrate ring entries (rows in the #744 JSON ring that predate the
sidecar) are NOT backfilled. The ring is bounded (ring_max=200), no v1
consumer reads belief_touches, and the v2 rerank consumer will only
care about touches recorded after v1 ships — so the value of backfill
was already low. A sentinel-based "true one-shot" alternative would
have required DB-level state for ~zero payoff.

Test plan:
- test_record_touches_writes_current_turn_only_no_ring_backfill —
  asserts ring-pre-pop is NOT migrated; current turn lands at touch_count=1.
- test_record_touches_count_matches_actual_inject_count_across_fires —
  regression for the [b1],[b2],[b1] pattern; asserts touch_count={2,1}
  and last_fire_idx monotonicity (b1 > b2). Mimics the production caller
  sequence (append_ids then _record_touches fire_idx=next_fire-1).

22 tests in tests/test_hot_path_touch_state.py pass (was 21; migration
test replaced by the two above). Docstring, docs/feature-hot-path.md,
and the v3.1 CHANGELOG entry updated to describe forward-only semantics.
robotrocketscience added a commit that referenced this pull request May 14, 2026
After `_ring_append_ids` returns the next fire_idx for this UPS turn,
call the new `_record_touches` helper to write the same injected ids
into `belief_touches` with INJECTION bit set. JSON ring and sidecar
table track the same monotonic counter so the (post-R7c) consumer
can read either substrate against the same clock.

`_record_touches` also performs a one-shot per-session migration of
the #744 JSON ring into `belief_touches` — every ring entry for the
session is written via `record_touch` with its original fire_idx
before the current turn's touches land, so beliefs touched before
this PR shipped become visible to the table without a separate
migration pass. Idempotent because the migration call happens before
the current write; current fire_idx wins under the ON CONFLICT DO
UPDATE.

Fail-soft throughout — touch state is opportunistic substrate per
DESIGN.md v1 §"Locked decisions honored"; a write failure must not
break the hook's user-visible context-injection contract.
robotrocketscience added a commit that referenced this pull request May 14, 2026
…816)

21 tests across the v1 DESIGN test plan:

- is_hot boundary cases incl. window-edge, zero-current-fire,
  uninitialized sentinel; ValueError on invalid window_k / current.
- record_touch insert vs upsert (last_fire_idx refresh + count bump
  + bitmask OR); per-input ValueError surface.
- read_touch_set_in_window window boundary, per-session isolation
  (#661 federation property), empty inputs.
- count_touches_for_session + list_touch_sessions ordering.
- Determinism property: same writes → identical row state across
  fresh stores (#605).
- FK CASCADE on belief delete (using insert_belief).
- Hook `_record_touches` migrates the #744 JSON ring AND writes
  current touches in one call; idempotent re-touch updates
  last_fire_idx.
- Fail-soft on missing-DB path.

Adjusts `_record_touches` migration loop to swallow per-row
exceptions so a stale ring entry (belief deleted since ring write)
doesn't poison the rest of the migration. Outer try/except still
catches catastrophic store-open failures.
robotrocketscience added a commit that referenced this pull request May 14, 2026
 review)

PR #821 review feedback (2026-05-14)
flagged that _record_touches re-walks the entire #744 JSON ring on every
UPS fire and calls record_touch for every entry. Because record_touch
upserts with `touch_count = touch_count + 1` on conflict, the docstring's
"one-shot INSERT-OR-IGNORE migration" claim was false in two ways: not
one-shot, and not idempotent. Net effect on the example [b1],[b2],[b1]:
b1.touch_count=5, b2.touch_count=3 (reviewers' repro), vs expected 2 / 1.

Functional impact in v1: none — no consumer reads touch_count.
Correctness impact for the H3 fidelity bench / any v2 rerank consumer:
real, since touch_count drift would mis-rank.

Fix: drop the ring replay entirely. The hook is now forward-only:
record_touch is called only for the current turn's belief_ids. Pre-
substrate ring entries (rows in the #744 JSON ring that predate the
sidecar) are NOT backfilled. The ring is bounded (ring_max=200), no v1
consumer reads belief_touches, and the v2 rerank consumer will only
care about touches recorded after v1 ships — so the value of backfill
was already low. A sentinel-based "true one-shot" alternative would
have required DB-level state for ~zero payoff.

Test plan:
- test_record_touches_writes_current_turn_only_no_ring_backfill —
  asserts ring-pre-pop is NOT migrated; current turn lands at touch_count=1.
- test_record_touches_count_matches_actual_inject_count_across_fires —
  regression for the [b1],[b2],[b1] pattern; asserts touch_count={2,1}
  and last_fire_idx monotonicity (b1 > b2). Mimics the production caller
  sequence (append_ids then _record_touches fire_idx=next_fire-1).

22 tests in tests/test_hot_path_touch_state.py pass (was 21; migration
test replaced by the two above). Docstring, docs/feature-hot-path.md,
and the v3.1 CHANGELOG entry updated to describe forward-only semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-pascal Authored by parallel session pascal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cross-fire injection dedup: amortize search hooks against the UPS-fire memory block

2 participants