You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
kb.context returns claims that have been archived, superseded, contested, or redacted as if they were live — agents receive
retracted knowledge in their context pack. Two compounding bugs
combine to produce this:
Bug A — build_context_pack has no status filter. src/vouch/context.py:75-101 walks the hits from _retrieve and
appends every match to the context pack. There is no check on claim.status. Once a claim is indexed, it stays in retrieval
forever, regardless of subsequent lifecycle calls.
Bug B — store.update_claim never refreshes the FTS5 row. src/vouch/storage.py:308-313:
The embedding cache is refreshed; the FTS5 row is not. So lifecycle.archive, lifecycle.supersede, and lifecycle.contradict — all of which finish with store.update_claim(...) and never themselves call index_db.index_claim — leave claims_fts.status stuck at whatever
value it had at first-index time.
index_db.index_claim is called by proposals.approve (at src/vouch/proposals.py:258) on first approval and by health.rebuild_index during vouch index / vouch doctor. No
update path keeps it in sync with lifecycle mutations.
The compound effect: even a future fix that adds a status filter
to context.py would still leak archived claims, because the FTS5 status column it would filter on is stale. Both bugs must be
fixed for retrieval to be correct.
This breaks the read-side promise of the KB. The whole point of ClaimStatus.{ARCHIVED, SUPERSEDED, REDACTED} is to remove a
claim from active circulation while keeping its history. Today
those states are decorative.
What you expected
kb.context (and the underlying build_context_pack) should
exclude claims whose status is ARCHIVED, SUPERSEDED, or REDACTED from the returned items. CONTESTED claims may be
surfaced (per kb.lint and vouch lint, "contested" is a
caution, not a removal), but the host should be able to see
their status — adding status to ContextItem lets agents and
hosts decide.
lifecycle.archive, lifecycle.supersede, and lifecycle.contradict should keep claims_fts.status in sync
with the on-disk claim — either by calling index_claim directly
or by making store.update_claim re-index the FTS5 row in
addition to refreshing the embedding cache.
Reproduction
$ python archived_context_repro.py
work dir: /tmp/vouch-archived-XXXXXXXX
--- after approval (stable claim) ---
items: [('mongodb-is-faster-than-postgres', '«MongoDB» is faster than Postgres')]
--- claim mongodb-is-faster-than-postgres archived ---
on-disk claim.status: archived
FTS5 row after archive: id=mongodb-is-faster-than-postgres status='working' (expected: 'archived')
--- after archive, kb.context for 'mongodb' ---
items returned: 1
id='mongodb-is-faster-than-postgres' summary='«MongoDB» is faster than Postgres'
BUG CONFIRMED: archived claim still surfaces in kb.context. Agents
will quote retracted knowledge as if it were live.
archived_context_repro.py does:
KBStore.init(...); register a source.
propose_claim(text="MongoDB is faster than Postgres", evidence=[src.id])
then approve(...).
lifecycle.archive(claim_id=...).
Read the on-disk YAML → confirms status: archived.
Direct SQL query against state.db → claims_fts.status='working'. The FTS5 row was never
refreshed; the column still reflects the status at first-index.
context.build_context_pack(query='mongodb').
Observe the archived claim in items — the agent would receive
it as fresh context.
The same shape applies to supersede (status becomes SUPERSEDED) and contradict (status becomes CONTESTED) —
both call update_claim and never re-index.
Environment
vouch version: vouch 0.0.1 at main
Python version: Python 3.12.13
OS: any (logic is platform-independent)
Host: any (CLI vouch context …, MCP kb.context, JSONL kb.context)
.vouch/ state
$ vouch doctor
Not informative — doctor rebuilds the index, which masks Bug B after the rebuild completes. The bug reappears the moment any
new lifecycle op runs on a claim that survives the rebuild.
Anything else
Suggested fix
Two-file fix, both required:
src/vouch/storage.pyupdate_claim — after writing the
YAML, also refresh the FTS5 row by calling index_db.index_claim(conn, id=claim.id, text=claim.text, type=claim.type, status=claim.status, tags=claim.tags)
(mirroring what proposals.approve does on first-index at src/vouch/proposals.py:258). This keeps claims_fts.status
in sync with every lifecycle mutation without touching lifecycle.py.
src/vouch/context.pybuild_context_pack — after _retrieve and before assembling items, skip hits whose kind == "claim"and whose underlying claim.status is in {ARCHIVED, SUPERSEDED, REDACTED}. Resolve via store.get_claim(hid); if the claim is gone, drop the hit.
Optionally surface claim.status on ContextItem so CONTESTED is visible without filtering.
Tests in tests/test_context.py and tests/test_lifecycle.py:
After archive, kb.context returns zero items matching
the archived claim's text.
After supersede(old, new), kb.context returns new but
not old.
After archive, the claims_fts row's status column is 'archived' (direct SQL assertion).
Why it's worth fixing
The read-side promise of the KB is "what does vouch know now". The archive/supersede/redact statuses exist precisely
to retract knowledge from that answer. Today retracted
knowledge is still served, so the statuses are pure
bookkeeping with no operational effect.
Affects every agent that uses kb.context (i.e., every
intended consumer of vouch) on every platform and every
transport (CLI, MCP, JSONL).
Compound — fixing only one half (filter at context, or
re-index in update_claim) doesn't close the bug. The PR has
to touch both files, with tests for both halves.
Sibling guarantee to the audit-truthfulness work the team has
been investing in: "audit log truthfully says what happened"
is meaningless if "kb.context truthfully says what's live" is
broken.
Checked for duplicates
Searched open + closed issues on vouchdev/vouch for archived context, superseded search, lifecycle reindex, claims_fts status, kb.context filter. Closest matches:
What happened
kb.contextreturns claims that have beenarchived,superseded,contested, orredactedas if they were live — agents receiveretracted knowledge in their context pack. Two compounding bugs
combine to produce this:
Bug A —
build_context_packhas nostatusfilter.src/vouch/context.py:75-101walks the hits from_retrieveandappends every match to the context pack. There is no check on
claim.status. Once a claim is indexed, it stays in retrievalforever, regardless of subsequent lifecycle calls.
Bug B —
store.update_claimnever refreshes the FTS5 row.src/vouch/storage.py:308-313:The embedding cache is refreshed; the FTS5 row is not. So
lifecycle.archive,lifecycle.supersede, andlifecycle.contradict— all of which finish withstore.update_claim(...)and never themselves callindex_db.index_claim— leaveclaims_fts.statusstuck at whatevervalue it had at first-index time.
index_db.index_claimis called byproposals.approve(atsrc/vouch/proposals.py:258) on first approval and byhealth.rebuild_indexduringvouch index/vouch doctor. Noupdate path keeps it in sync with
lifecyclemutations.The compound effect: even a future fix that adds a status filter
to
context.pywould still leak archived claims, because the FTS5statuscolumn it would filter on is stale. Both bugs must befixed for retrieval to be correct.
This breaks the read-side promise of the KB. The whole point of
ClaimStatus.{ARCHIVED, SUPERSEDED, REDACTED}is to remove aclaim from active circulation while keeping its history. Today
those states are decorative.
What you expected
kb.context(and the underlyingbuild_context_pack) shouldexclude claims whose
statusisARCHIVED,SUPERSEDED, orREDACTEDfrom the returned items.CONTESTEDclaims may besurfaced (per
kb.lintandvouch lint, "contested" is acaution, not a removal), but the host should be able to see
their status — adding
statustoContextItemlets agents andhosts decide.
lifecycle.archive,lifecycle.supersede, andlifecycle.contradictshould keepclaims_fts.statusin syncwith the on-disk claim — either by calling
index_claimdirectlyor by making
store.update_claimre-index the FTS5 row inaddition to refreshing the embedding cache.
Reproduction
archived_context_repro.pydoes:KBStore.init(...); register a source.propose_claim(text="MongoDB is faster than Postgres", evidence=[src.id])then
approve(...).lifecycle.archive(claim_id=...).status: archived.state.db→claims_fts.status='working'. The FTS5 row was neverrefreshed; the column still reflects the status at first-index.
context.build_context_pack(query='mongodb').items— the agent would receiveit as fresh context.
The same shape applies to
supersede(status becomesSUPERSEDED) andcontradict(status becomesCONTESTED) —both call
update_claimand never re-index.Environment
mainPython 3.12.13vouch context …, MCPkb.context, JSONLkb.context).vouch/stateNot informative —
doctorrebuilds the index, which masks Bug Bafter the rebuild completes. The bug reappears the moment any
new lifecycle op runs on a claim that survives the rebuild.
Anything else
Suggested fix
Two-file fix, both required:
src/vouch/storage.pyupdate_claim— after writing theYAML, also refresh the FTS5 row by calling
index_db.index_claim(conn, id=claim.id, text=claim.text, type=claim.type, status=claim.status, tags=claim.tags)(mirroring what
proposals.approvedoes on first-index atsrc/vouch/proposals.py:258). This keepsclaims_fts.statusin sync with every lifecycle mutation without touching
lifecycle.py.src/vouch/context.pybuild_context_pack— after_retrieveand before assemblingitems, skip hits whosekind == "claim"and whose underlyingclaim.statusis in{ARCHIVED, SUPERSEDED, REDACTED}. Resolve viastore.get_claim(hid); if the claim is gone, drop the hit.Optionally surface
claim.statusonContextItemsoCONTESTEDis visible without filtering.Tests in
tests/test_context.pyandtests/test_lifecycle.py:archive,kb.contextreturns zero items matchingthe archived claim's text.
supersede(old, new),kb.contextreturnsnewbutnot
old.archive, theclaims_ftsrow'sstatuscolumn is'archived'(direct SQL assertion).Why it's worth fixing
now". The archive/supersede/redact statuses exist precisely
to retract knowledge from that answer. Today retracted
knowledge is still served, so the statuses are pure
bookkeeping with no operational effect.
kb.context(i.e., everyintended consumer of vouch) on every platform and every
transport (CLI, MCP, JSONL).
re-index in update_claim) doesn't close the bug. The PR has
to touch both files, with tests for both halves.
recently-merged PR feat(embeddings): CLI sweep, MCP/JSONL parity, integration test, docs #44 reshaped the semantic-search code path;
PR fix(sessions): index crystallize summary page into FTS5 (#60) #61 (open) is fixing FTS5 indexing of the crystallize
summary page (bug: crystallize writes session-summary page without FTS5 indexing — pages invisible to kb.search #60). Both signal that retrieval correctness is
on the team's radar.
been investing in: "audit log truthfully says what happened"
is meaningless if "kb.context truthfully says what's live" is
broken.
Checked for duplicates
vouchdev/vouchforarchived context,superseded search,lifecycle reindex,claims_fts status,kb.context filter. Closest matches:page. Different artifact kind (Page, not Claim); doesn't
touch the lifecycle/update path. PR fix(sessions): index crystallize summary page into FTS5 (#60) #61 in flight.
retrieval. Adds the embedding backend; does not touch
status filtering.
None propose a status filter in
build_context_packor are-index in
update_claim.context filter status,update_claim reindex,lifecycle index. Nothing.