Skip to content

fix(recall): stamp injected memories with age, provenance grade, and stale flag - #442

Merged
cdeust merged 3 commits into
mainfrom
claude/freshness-stamp-injected-memories
Aug 24, 2026
Merged

fix(recall): stamp injected memories with age, provenance grade, and stale flag#442
cdeust merged 3 commits into
mainfrom
claude/freshness-stamp-injected-memories

Conversation

@cdeust

@cdeust cdeust commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

The harness-comparison rev.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, the source_attribution provenance grade, and is_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):

- PostgreSQL is the default store (decision)
- PostgreSQL is the default store (decision)  ·  3mo ago  ·  src=verified  ·  ⚠stale

Addresses cdeust/fleet-watch#110 (freshness seams) for the primary recall path. It does not fully close #110: the parallel formatters (session_start banner, recall_helpers.inject_triggered_memories) take the same shared/freshness.py helper as a follow-up, and #110 also covers post-commit re-verify + provenance-first seeding.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Audit-finding closure (partial — cdeust/fleet-watch#110, primary recall path only)

Test plan

Authoring environment has no numpy/psycopg/pytest, so the full suite runs in CI. What was verified locally:

  • New pure teststests_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.freshness and auto_recall._format_injection and exercised both the freshness and bare-memory cases — output matches the tests above.

  • Lint: ruff check + ruff format --check clean 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

  • Engineering review: self-review only — verified layer direction (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.
  • Genius review: not run here.
  • Outstanding deferred findings: parallel formatters (session_start, recall_helpers) still bare — follow-up under cdeust/fleet-watch#110.

Coding-standards compliance

  • §2.2 Layer dependency direction preserved (shared/freshness.py imports stdlib only).
  • §3.2 No untyped-boundary any.
  • §4.1 No file > 500 lines (freshness.py is 81).
  • §4.2 No function > 50 lines.
  • §4.4 No function with > 4 parameters.
  • §7 Local reasoning preserved.
  • §8 Numeric constants annotated — the time-unit boundaries carry a # source: note (calendar/SI definitions; 30-day/365-day display convention).
  • §9 No dead code / untracked TODOs.

Breaking changes

None. Memories without created_at/source_attribution/is_stale render exactly as before; _format_injection gains an optional now parameter (defaults to wall clock) used only to make tests deterministic.

Screenshots / logs

Injected block for a stale memory:

**Cortex context:**
- PostgreSQL is the default store (decision)  ·  3mo ago  ·  src=verified  ·  ⚠stale

Reviewer checklist

  • CHANGELOG.md updated — not touched; flagging for reviewer preference.
  • No secrets / credentials / PII in the diff.
  • CI passes on the latest commit — pending.

Generated by Claude Code

claude added 3 commits August 24, 2026 14:26
…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

cdeust commented Aug 24, 2026

Copy link
Copy Markdown
Owner Author

Scope update: this now covers both context-injection surfaces, not just the primary recall path.

  • 487e033 — refactor to keep _format_injection under the method-size gate (CI caught it; fixed).
  • 688fc11 — extends the same shared/freshness.py stamping to the SessionStart banner (_build_context): anchors, team decisions, and hot memories now carry age · provenance grade · stale marker, on both the PG fetches and the SQLite path (_partition_banner_rows).

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 is_stale actually fires on source change, and provenance-first seeding) are separate follow-up PRs — the display shipped here is only as useful as the staleness flag being set, which is the next change.

Verified locally: ruff + the craftsmanship gate clean; a stale anchor renders · 4mo ago · src=verified · ⚠stale; memories without the fields render unchanged. Full pytest runs in CI.


Generated by Claude Code

@cdeust
cdeust merged commit 5e8f60f into main Aug 24, 2026
25 checks passed
@cdeust
cdeust deleted the claude/freshness-stamp-injected-memories branch August 24, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 write paths bypass write_governed_page — governance and write-time normalization not enforced

2 participants