MINOR. Loose Ends Release 8 of 8 — the arc's own last conditional rung, correctly not taken when the arc closed at 7/8, picked back up whole in a dedicated session per wiki/designs/agentm-recall-trace.md.
A recalled memory's score breakdown used to exist for one prompt turn: printed into the injected block, then discarded. Auditing a bad recall meant re-running the query later and hoping the index, the decay clock, and the vault hadn't moved on since. recall_counter.py's per-recall JSONL ledger (recall-history.jsonl) already recorded which slugs surfaced on every prompt — it just had zero production readers, and nothing recorded why a hit was there. This release widens that same ledger with the evidence recall.py's query() already computes, and adds the first reader for it.
The build caught its own bug before it shipped: a first-draft capture method — recovering a recall's surviving hits by filtering the full result set against the post-truncation slug list — mis-identifies entries whenever two results share a slug, which is common in a real vault (_index, _summary, and dated notes all repeat as basenames, and _index/_summary are exactly the altitude-boosted anchors recall ranks higher). The shipped version instead packs (slug, hit) pairs through the existing token-budget helper unchanged, so truncation and duplicate slugs can never desync evidence from the entry it actually describes — proven by a test built around that exact scenario.
Added
- Per-hit recall evidence (
recall_counter.py) —record_recall()gains an optionalhitsarray (sim, keyword, combined, rank, and lifecycle tier/decay score when available), written alongside the existinghit_slugs. Omitted by default: a caller not yet passing it writes today's exact row shape, nohitskey at all. - Duplicate-slug-safe capture (
recall.py) —prompt_submit()now packs the evidence through_apply_token_budget's existing walk instead of recovering it afterward by slug membership;rankis read off the full result list before any per-file read skip can renumber it._apply_token_budget's own logic and arity are untouched. memory-recall trace <slug> [-n N](recall.py) — explains why a memory surfaced: the N most recent recall events for a slug, most-recent-first, with the score breakdown at fixed precision. Degrades honestly on three "nothing to show" cases — no ledger, never recalled, or recalled before this feature existed to record why — each an explicit line, never silence.- Console pointer (
console.py) — one documented line in the Memory Activity section pointing at the new reader; no new always-rendered row, since a per-slug trace is a drill-down, not a summary metric. - 19 new tests across
test_recall_counter.py, the newtest_recall_trace.py, andtest_console.py. All 129 tests in the eight other recall-adjacent test files reconfirmed green, untouched.
Two things found in passing during the build were flagged as separate follow-ups rather than fixed inline — both landed independently before this tag, so they ship in this same release: test_recall_token_budget.py's prompt_submit() integration tests wrote real rows into the production telemetry ledger instead of a mock (PR #390); the pinned retrieval eval's query set had drifted 4 expected-notes paths against the live vault, tripping its own fail-loud gate on a cause unrelated to this feature (PR #391).
A pre-tag adversarial review (after merge, before this tag) caught two more things. A real crash: trace() only guarded malformed JSON syntax, so a shape error (valid JSON that isn't the row's object shape) or invalid UTF-8 anywhere in the ledger raised uncaught instead of degrading honestly — fixed same day, with tests reproducing all three failure shapes. And a gap left open on purpose: each hit's stored path discloses vault directory structure, with no redaction tied to a note's later deletion or move. The operator reviewed it, judged it low-severity on this single-user, single-machine deployment (hashing or stripping path would only defeat the reader's whole purpose), named it plainly in the design's Risks section, and a rotation/redaction policy was spawned as its own separate follow-up rather than shipping the gap silently.