Skip to content

fix: stamp beliefs.last_retrieved_at on hook-driven retrieval (#222) - #266

Merged
robotrocketscience merged 1 commit into
mainfrom
fix/issue-222-stamp-retrieved
Apr 29, 2026
Merged

fix: stamp beliefs.last_retrieved_at on hook-driven retrieval (#222)#266
robotrocketscience merged 1 commit into
mainfrom
fix/issue-222-stamp-retrieved

Conversation

@robotrocketscience

Copy link
Copy Markdown
Owner

Summary

Closes #222. The retrieval audit loop was structurally half-implemented — the hook wrote feedback_history rows but never stamped beliefs.last_retrieved_at. Verified live: 0 of 20,852 beliefs populated on canonical store despite 586 hook-tagged feedback rows from one day.

Root cause: no code path anywhere in src/aelfrice/ issued UPDATE beliefs SET last_retrieved_at = ? outside update_belief()'s full-row rewrite (which fires on belief edits, not retrieval). All INSERT sites hard-coded None.

Changes

  • MemoryStore.stamp_retrieved(belief_ids, ts=None) — single batched UPDATE; defaults ts to UTC now; returns rowcount; no-op on empty.
  • hook_search.record_retrieval — after the apply_feedback loop, calls stamp_retrieved with only the ids whose feedback write succeeded. Same best-effort posture as the audit loop (errors logged to stderr, never block).

Test plan

  • tests/test_store_crud.py — 5 new: populate, explicit ts, empty input, missing id, overwrite prior ts.
  • tests/test_hook_search.py — 4 new: stamp on retrieval, skip-on-failure, no-op on empty, search_for_prompt end-to-end.
  • Full suite: 1052 passed, 1 pre-existing unrelated failure (test_serve_raises_clear_error_when_fastmcp_missing — environment-dependent, fails on main HEAD too).

Out of scope

No backfill of historic rows. The 20,852 already-NULL beliefs stay NULL — column is now populated forward from this commit. If a backfill is wanted, a separate aelf migrate --backfill-retrieval could derive timestamps from feedback_history.created_at WHERE source='hook' for the 413 belief ids that have any hook-tagged event. Filing a follow-up issue if the user wants this.

The retrieval audit loop was structurally half-implemented: the
UserPromptSubmit hook wrote one feedback_history row per retrieved
belief but never stamped beliefs.last_retrieved_at. Result on the
canonical store: 0 of 20,852 beliefs populated, despite 586
feedback_history rows tagged source='hook' from a single day.

Grep across src/aelfrice/ confirmed no code path issued
\`UPDATE beliefs SET last_retrieved_at = ?\` outside update_belief()'s
full-row rewrite (which fires on belief edits, not retrieval). All
INSERT sites hard-coded the column to None.

Add MemoryStore.stamp_retrieved(belief_ids, ts=None): single batched
UPDATE, defaults ts to UTC now, returns rowcount, no-op on empty.
Wire it into hook_search.record_retrieval after the apply_feedback
loop, stamping only the ids whose feedback_history write actually
succeeded — failed writes don't get the recency mirror.

Tests:
- store_crud: populate, explicit ts, empty, missing-id, overwrite.
- hook_search: stamp on retrieval, skip-on-failure, no-op on empty,
  search_for_prompt end-to-end stamping.

Closes #222.
@robotrocketscience robotrocketscience added the review-Gylf PR coordination mutex label Apr 29, 2026
@robotrocketscience
robotrocketscience merged commit 64a56f6 into main Apr 29, 2026
8 checks passed
@robotrocketscience
robotrocketscience deleted the fix/issue-222-stamp-retrieved branch April 29, 2026 00:00
robotrocketscience added a commit that referenced this pull request Apr 29, 2026
## Summary

Spec memo for #289. Phase-2a of the #286 redesign — the gating contract
change so `rebuild_v14()` can return empty when no candidate clears the
floor. **Doc-only — no code change. Posted for ratification.**

Five recommendations:

1. **Yes — add a silent path.** All-floored-out → `""`. No empty-marker
tag.
2. **Composite score formula.** `bm25_normalized * (0.5 + 0.5 *
posterior_mean)`. Posterior-mean weighting prevents
stale-but-token-overlapping beliefs (the `1bc8ab45a40351d9` example in
#281) from passing.
3. **Three-tier per-lane floors.** L0 locked = no floor (operator intent
dominates). L2 session-scoped = soft floor `T_session = 0.10`. L1 / L2.5
= hard floor `T_l1 = 0.40`.
4. **No `last_retrieved_at` stamp on floored-out hits.** Stamp moves to
*after* floor application; floored-out beliefs never reach the stamp
loop. Protects the hibernation staleness signal (#196).
5. **Ship with placeholder T defaults; calibrate from #288 logs.** Floor
*value* blocks on #288 eval harness data; floor *shape* lands in v1.x
with operator-tunable `[rebuild_floor]` config.

## Decision asks (five)

Bottom of the memo. Composite formula, per-lane structure, empty-path
shape, stamp ownership, calibration plan.

## What ships if ratified

One PR (~400 lines):

- `rebuild_v14` composite + floor + empty-path return
- `hook.py` precompact-envelope drop on empty
- `hook_search.py` stamp move (only survivors get stamped)
- `[rebuild_floor]` config block + tests

## Test plan

- [x] Memo cross-references #286, #288, #281, #222/#266
- [x] No code change; nothing to test
- [ ] Ratification or override comment from maintainer

Refs #289, #286
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-Gylf PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

last_retrieved_at not populated; retrieval-audit-loop not landing on user store

1 participant