From 9e61cad9fb2db2be03f064bbcc7cc07e552488b6 Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Wed, 15 Jul 2026 15:20:50 -0400 Subject: [PATCH] fix(recall): exclude archived pages from the session-start digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_digest filtered claims against _RETRACTED_CLAIM_STATUSES but passed store.list_pages() through unfiltered, so an archived page's title reached every new session's opening context while its claims stayed suppressed. recall is the one surface that runs unprompted on every SessionStart, so the leak lands in every future session's first turn. synthesize already filters PageStatus.ARCHIVED; this brings recall in line with it, and with the rationale already written on _RETRACTED_CLAIM_STATUSES — otherwise the archive control is decorative. only ARCHIVED is filtered: draft and stale pages stay, mirroring the reasoning that keeps contested claims in the claim set. --- CHANGELOG.md | 5 +++++ src/vouch/recall.py | 9 +++++++-- tests/test_recall.py | 28 +++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff39a63..577a78a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,11 @@ All notable changes to vouch are documented here. Format follows - demo image build: `hatch_build.py` (the build hook pyproject.toml declares for console bundling) is copied into the docker build context; the image had been unbuildable since the hook landed (#474). +- `vouch recall` excludes archived pages from the session-start digest. + claims were filtered by status but pages were not, so an archived page's + title kept reaching every new session's opening context while its claims + stayed suppressed — the same asymmetry `synthesize` already avoids. draft + and stale pages are unaffected (#490). ## [1.3.0] — 2026-07-14 diff --git a/src/vouch/recall.py b/src/vouch/recall.py index 911d512c..22328976 100644 --- a/src/vouch/recall.py +++ b/src/vouch/recall.py @@ -2,7 +2,7 @@ Claude session's context (memvid-style), via the SessionStart hook. Unlike ``kb.context`` (task-scoped retrieval), this emits EVERY live approved -claim as a compact ``[id] text`` line plus every approved page title, so a +claim as a compact ``[id] text`` line plus every live page title, so a fresh session is aware of the whole reviewed KB from the first turn. Page bodies are fetched on demand with ``kb_read_page`` / ``kb_search``. @@ -17,6 +17,7 @@ import yaml from .context import _RETRACTED_CLAIM_STATUSES +from .models import PageStatus from .storage import KBStore DEFAULT_ENABLED = True @@ -58,7 +59,11 @@ def build_digest(store: KBStore, *, max_chars: int = DEFAULT_MAX_CHARS) -> str: c for c in store.list_claims() if c.status not in _RETRACTED_CLAIM_STATUSES ] - pages = store.list_pages() + # archived pages are retracted knowledge, same as the archived claims + # filtered above — leaking a title back into every session's opening + # context would make the archive control decorative. DRAFT and STALE + # stay: unpolished or aging is still live. + pages = [p for p in store.list_pages() if p.status != PageStatus.ARCHIVED] if not claims and not pages: return "" diff --git a/tests/test_recall.py b/tests/test_recall.py index a08a02ce..2777fae7 100644 --- a/tests/test_recall.py +++ b/tests/test_recall.py @@ -7,7 +7,7 @@ import pytest from vouch import recall -from vouch.models import ClaimStatus +from vouch.models import ClaimStatus, PageStatus from vouch.proposals import approve, propose_claim, propose_page from vouch.storage import KBStore, _starter_config @@ -47,6 +47,32 @@ def test_digest_excludes_retracted_claims(store: KBStore) -> None: assert "archived fact" not in d +def test_digest_excludes_archived_pages(store: KBStore) -> None: + _approve_page(store, "live page") + archived = _approve_page(store, "archived page") + archived.status = PageStatus.ARCHIVED + store.update_page(archived) + d = recall.build_digest(store) + assert "live page" in d + assert "archived page" not in d + assert "1 page(s)" in d + + +def test_digest_keeps_stale_pages(store: KBStore) -> None: + """Only ARCHIVED is retracted — an aging page is still live knowledge.""" + stale = _approve_page(store, "stale page") + stale.status = PageStatus.STALE + store.update_page(stale) + assert "stale page" in recall.build_digest(store) + + +def test_digest_empty_when_only_archived_pages(store: KBStore) -> None: + archived = _approve_page(store, "archived page") + archived.status = PageStatus.ARCHIVED + store.update_page(archived) + assert recall.build_digest(store) == "" + + def test_empty_kb_digest_is_empty(store: KBStore) -> None: assert recall.build_digest(store) == ""