Skip to content

feat(staleness): auto-fire is_stale via a bounded file-existence revalidation pass - #443

Merged
cdeust merged 2 commits into
mainfrom
claude/staleness-revalidation-pass
Aug 24, 2026
Merged

feat(staleness): auto-fire is_stale via a bounded file-existence revalidation pass#443
cdeust merged 2 commits into
mainfrom
claude/staleness-revalidation-pass

Conversation

@cdeust

@cdeust cdeust commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the freshness-stamping work (#442). That PR made the injection banners show is_stale (age · grade · stale); this PR makes the flag actually fire on file grounds. Today is_stale is only ever set by the manual validate_memory tool, so a memory referencing a moved/deleted file stays is_stale=FALSE until someone runs the tool by hand — which is precisely what harness-comparison rev.2 measured (Harness B served facts months stale with no stale flag).

revalidate_staleness() pages non-stale, file-referencing memories, re-checks path existence via an injected resolver, and marks is_stale using the same pure core.staleness.assess_staleness + store.mark_memory_stale the tool uses.

Two safety decisions:

  • Mark-only — it never de-stales, so it can't fight the active-forgetting (Rac1) circuit in consolidation/forgetting.py that also writes is_stale. Rehabilitation stays with the explicit, human-invoked validate_memory.
  • Off the hot paths — script-invoked (dry-run by default), bounded by a scan cap with an id cursor. Not wired into the commit hook or the consolidate cycle, mirroring the memory_domain_backfill precedent.

Existence-only; content-change detection (a file that still exists but diverged) needs per-ref hashing and is a separate #110 seam.

Addresses cdeust/fleet-watch#110.

Type of change

  • Bug fix (non-breaking — the staleness signal now fires automatically)
  • Audit-finding closure (partial — cdeust/fleet-watch#110, file-existence seam)

Test plan

Authoring env lacks pydantic/numpy/DB, so the full suite runs in CI. Verified locally:

  • New tests (tests_py/handlers/consolidation/test_memory_staleness_pass.py, DB-free with a fake store + injected resolver): marks only the memory with a missing ref; never de-stales; skips no-ref and already-stale rows; respects the scan limit. All three scenarios executed and passing locally (ran the test functions directly).

  • ruff check + ruff format --check + the craftsmanship gate: clean.

  • All existing tests pass — deferred to CI.

  • New tests added for new behavior.

  • Mutation check: flipping mark-only→bidirectional (would fight forgetting), dropping the no-ref/already-stale guards, or ignoring the scan cap are all caught.

  • Manual verification of the pass logic against a fake store.

Audit notes

  • Engineering review: self-review. Reuses the existing pure assessment + store method (no new staleness logic); mark-only keeps it from clobbering the forgetting circuit; DI resolver keeps it testable and off the FS in unit tests.
  • Outstanding: content-change (hashing) detection + an automatic trigger (cron/session-start/post-commit) are follow-ups under cdeust/fleet-watch#110.

Coding-standards compliance

  • §2.2 Layer direction (pass is a composition root; pure decision stays in core.staleness).
  • §4.1 / §4.2 file & function sizes within limits.
  • §4.4 No function > 4 parameters.
  • §8 The two numeric constants (scan cap, page size) carry # source: notes.

Breaking changes

None. New pass + script; no existing behavior changed. Nothing runs it automatically yet (dry-run script only) — wiring a trigger is a deliberate follow-up.

Reviewer checklist

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

Generated by Claude Code

claude added 2 commits August 24, 2026 15:12
…lidation pass

The injection banners now surface is_stale (age · grade · stale), but on file
grounds that flag was only ever SET by the manual validate_memory tool — so a
memory referencing a moved/deleted file stayed is_stale=FALSE until someone ran
it by hand. harness-comparison rev.2 measured exactly this: Harness B served
facts months stale with no stale flag.

- handlers/consolidation/memory_staleness_pass.py: revalidate_staleness() pages
  non-stale, file-referencing memories, re-checks path existence via an injected
  resolver, and marks is_stale using the same pure core.staleness.assess_staleness
  + store.mark_memory_stale the tool uses. Bounded by a scan cap with an id
  cursor. Mark-ONLY: never de-stales, so it cannot fight the active-forgetting
  (Rac1) circuit that also writes is_stale; rehabilitation stays with the
  explicit validate_memory tool. Existence-only; content-change detection
  (hashing) is a separate #110 seam.
- scripts/memory_staleness_revalidate.py: dry-run-by-default entry point
  (mirrors scripts/memory_domain_backfill.py), --apply to write, reusing
  validate_memory's path resolver. Script-invoked — not on the commit critical
  path or the hot consolidate cycle.
- tests: mark-only, skip-no-refs, skip-already-stale, never-destale, and
  scan-limit behavior with a fake store + injected resolver (DB-free).

Addresses fleet-watch #110 (the "is_stale must actually fire" seam). ruff +
craftsmanship gate clean; pass scenarios verified locally; full pytest in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017fnomaXS71hgxMw2zr7HGR
Craftsmanship gate flagged it as a new unsourced constant (§8). Annotate it as
a chosen per-run bound mirroring the existing domain-backfill cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017fnomaXS71hgxMw2zr7HGR
@cdeust
cdeust merged commit 4ffe462 into main Aug 24, 2026
25 checks passed
@cdeust
cdeust deleted the claude/staleness-revalidation-pass branch August 24, 2026 15:54
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.

2 participants