fix(recall): stamp injected memories with age, provenance grade, and stale flag - #442
Merged
Merged
Conversation
…stale flag harness-comparison rev.2 measured the ai-architect stack (Harness B) serving facts "2-4 months stale with no age signal": every recalled memory entered the model's context as bare text, so a months-old fact and a fresh one were indistinguishable. The store already tracks the freshness (created_at, the source_attribution provenance grade, is_stale) — the injection formatter just discarded it. - shared/freshness.py: pure helper. humanize_age(created, now) → compact relative age; provenance_suffix(memory, now) → "age · src=<grade> · ⚠stale" with empty parts omitted. Caller owns the clock (deterministic/testable). - hooks/auto_recall.py: carry created_at/source_attribution/is_stale through both the PG and SQLite recall paths, and append the freshness suffix per memory in _format_injection (now injectable for tests; suffix counts toward the injection budget so the receipt still mirrors exactly what is printed). - Memories without these fields render exactly as before — bare call sites and existing budget/parity tests are unaffected. Addresses fleet-watch #110 (freshness seams) for the primary UserPromptSubmit recall path. The parallel formatters (session_start banner, recall_helpers) take the same helper as a follow-up. Tests: tests_py/shared/test_freshness.py (age buckets, ISO/naive coercion, grade/stale composition) + two _format_injection cases in tests_py/hooks/test_hook_receipts_unit.py (suffix rendered; bare memory unchanged). ruff check + format clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fnomaXS71hgxMw2zr7HGR
…gate The freshness suffix pushed _format_injection over the craftsmanship method-size limit (CI flagged it as a NEW violation vs the base-ref baseline). Extract per-memory line rendering into _render_memory_line; both functions are now well under the limit. No behavior change — verified the freshness suffix still renders and bare memories are unchanged; scripts/check_craftsmanship.py reports OK locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fnomaXS71hgxMw2zr7HGR
Extends the freshness stamping to the second injection surface: the SessionStart banner (_build_context) rendered anchors, team decisions, and hot memories as bare bullets, so the same "no age signal" staleness the harness-comparison rev.2 measured applied there too. - session_start.py: carry created_at / source_attribution / is_stale through the PG fetches (_fetch_anchors, _fetch_team_decisions, _fetch_hot_memories) AND the SQLite path (_partition_banner_rows), then append the freshness suffix per bullet in _build_context via a small _freshness() helper. - Reuses shared/freshness.py from the primary-recall-path change in this PR. - Memories lacking the fields render exactly as before (empty suffix). Test: tests_py/hooks/test_hook_receipts_unit.py — a stale anchor renders age + src=verified + ⚠stale. ruff + craftsmanship gate clean locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fnomaXS71hgxMw2zr7HGR
Owner
Author
|
Scope update: this now covers both context-injection surfaces, not just the primary recall path.
So the injection-side of fleet-watch#110 is now complete here. Remaining #110 seams (post-commit re-verify / content-hashing of file-derived memories so Verified locally: Generated by Claude Code |
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
harness-comparisonrev.2 A/B measured the ai-architect stack (Harness B) serving facts "2–4 months stale with no age signal": every recalled memory entered the model's context as bare text, so a months-old fact and a fresh one were indistinguishable (e.g. it served "PostgreSQL default" when the live default is SQLite, with no way for the reader to tell the claim was old). The store already tracks the freshness —created_at, thesource_attributionprovenance grade, andis_stale— but the injection formatter discarded all three. This threads them through the primary UserPromptSubmit recall path and renders them as a compact per-memory suffix.Before → after (rendered injection line):
Addresses cdeust/fleet-watch#110 (freshness seams) for the primary recall path. It does not fully close #110: the parallel formatters (
session_startbanner,recall_helpers.inject_triggered_memories) take the sameshared/freshness.pyhelper as a follow-up, and #110 also covers post-commit re-verify + provenance-first seeding.Type of change
Test plan
Authoring environment has no
numpy/psycopg/pytest, so the full suite runs in CI. What was verified locally:New pure tests —
tests_py/shared/test_freshness.py: age buckets (just-now → years), ISO-8601 + naive-datetime coercion, unknown/absent-field omission, grade+stale composition.New formatter tests — two cases in
tests_py/hooks/test_hook_receipts_unit.py: the suffix renders when fields are present; a bare memory renders identically to before (no·), so existing budget/parity tests are unaffected.Direct execution (bypassing pytest): imported
shared.freshnessandauto_recall._format_injectionand exercised both the freshness and bare-memory cases — output matches the tests above.Lint:
ruff check+ruff format --checkclean on all four files.All existing tests pass — deferred to CI (suite not runnable in authoring env).
New tests added for new behavior.
Mutation survival check: dropping any of the three fields, rendering the suffix unconditionally (would break bare-memory sites), or not counting the suffix toward the injection budget (would break the receipt-parity invariant) are all caught.
Manual verification of the injected-context output change (shown above).
Audit notes
shared/imports stdlib only), budget/parity invariant preserved (suffix counts toward_MAX_INJECTION_CHARS, so the receipt still mirrors exactly what is printed), and no behavior change for memories lacking the fields. No multi-agent audit cycle was run in this environment.session_start,recall_helpers) still bare — follow-up under cdeust/fleet-watch#110.Coding-standards compliance
shared/freshness.pyimports stdlib only).any.freshness.pyis 81).# source:note (calendar/SI definitions; 30-day/365-day display convention).Breaking changes
None. Memories without
created_at/source_attribution/is_stalerender exactly as before;_format_injectiongains an optionalnowparameter (defaults to wall clock) used only to make tests deterministic.Screenshots / logs
Injected block for a stale memory:
Reviewer checklist
Generated by Claude Code