Description
In src/vouch/sessions.py:99-110, crystallize() constructs the session-summary Page and writes it directly via store.put_page(page):
if write_summary_page and approved_artifact_ids:
page = Page(
id=f"session-{sess.id}",
title=f"Session {sess.id}",
type=PageType.SESSION,
body=_build_summary_body(sess, approved_artifact_ids),
claims=[...],
)
store.put_page(page)
summary_page_id = page.id
KBStore.put_page() only writes the markdown file and calls _embed_and_store(kind="page", ...). The FTS5 side of the index is
populated by index_db.index_page(), which is called only from proposals.approve() on the PAGE branch src/vouch/proposals.py:240-247). Because crystallize bypasses approve() for the summary page, FTS5 is never updated.
rg "index_page" src/vouch/ confirms the only callers are proposals.approve() and health.py's rebuilder (used by vouch index) — nothing in sessions.py.
The retrieval path in src/vouch/context.py:29-40 and the FTS5 branch in src/vouch/server.py short-circuit on the first non-empty FTS5 result, which means in any KB with a populated state.db, every session-<id> summary page is silently absent from search results. Substring fallback would find them, but the fallback only runs when FTS5 returns nothing — so on a normal-sized KB, summary pages are effectively unsearchable.
This breaks the documented intent of vouch crystallize from the README:
vouch crystallize promotes a session's durable parts into proposals;
one approve and they're permanent.
The summary page is one of the durable parts, but it isn't retrievable through the primary search path that every agent and kb.context caller uses.
Steps to Reproduce
- Initialise a fresh KB and rebuild the FTS5 index so
state.db has rows from at least one other artifact:
vouch init
vouch source add ./some-file.md
# propose + approve at least one claim/page so state.db is non-empty
vouch index
- Start a session, file a proposal, approve via crystallize:
SID=$(vouch session start --task "demo" --json | jq -r .id)
vouch propose-claim --text "demo claim" --source <sid> --session "$SID"
vouch session end "$SID"
vouch crystallize "$SID"
- Confirm the summary page exists on disk:
ls .vouch/pages/session-*.md # present
- Search for any token from the summary page title or body:
vouch search "Session $SID" # no page hit
- Force the substring fallback (e.g. delete
state.db) and re-run the
same search — the page now appears.
Actual Behavior
The session-summary page is written to disk and embedded, but the FTS5 table has no row for it. vouch search, kb.search, and the FTS5 branch of kb.context all miss the page. Only the substring fallback (which only fires when FTS5 returns zero results) can find it.
Expected Behavior
The summary page should be FTS5-indexed the same way an approved Page proposal is. Either:
sessions.crystallize() calls index_db.index_page(...) after store.put_page(page) on src/vouch/sessions.py:109, mirroring the
PAGE branch of proposals.approve(); or
KBStore.put_page() becomes responsible for keeping FTS5 in sync (and the redundant call in proposals.approve() is removed). This is the larger change and should probably go through a VEP, but the first option is a localised one-call fix.
Either way, session-<id> pages must be discoverable via the same search surface as any other approved page, without epending on a vouch index rebuild.
Environment
- Python 3.11+
- vouch pre-1.0
state.db present and non-empty (the bug is masked on empty / missing index databases because the substring fallback runs)
Notes
PR #43 currently touches sessions.py (the session_end refactor) but does not modify the crystallize summary-page write — a fix here will need a trivial rebase if both land.
Description
In
src/vouch/sessions.py:99-110,crystallize()constructs the session-summary Page and writes it directly viastore.put_page(page):KBStore.put_page()only writes the markdown file and calls_embed_and_store(kind="page", ...). The FTS5 side of the index ispopulated by
index_db.index_page(), which is called only fromproposals.approve()on the PAGE branchsrc/vouch/proposals.py:240-247). Because crystallize bypassesapprove()for the summary page, FTS5 is never updated.rg "index_page" src/vouch/confirms the only callers areproposals.approve()andhealth.py's rebuilder (used byvouch index) — nothing insessions.py.The retrieval path in
src/vouch/context.py:29-40and the FTS5 branch insrc/vouch/server.pyshort-circuit on the first non-empty FTS5 result, which means in any KB with a populatedstate.db, everysession-<id>summary page is silently absent from search results. Substring fallback would find them, but the fallback only runs when FTS5 returns nothing — so on a normal-sized KB, summary pages are effectively unsearchable.This breaks the documented intent of
vouch crystallizefrom the README:The summary page is one of the durable parts, but it isn't retrievable through the primary search path that every agent and
kb.contextcaller uses.Steps to Reproduce
state.dbhas rows from at least one other artifact:state.db) and re-run thesame search — the page now appears.
Actual Behavior
The session-summary page is written to disk and embedded, but the FTS5 table has no row for it.
vouch search,kb.search, and the FTS5 branch ofkb.contextall miss the page. Only the substring fallback (which only fires when FTS5 returns zero results) can find it.Expected Behavior
The summary page should be FTS5-indexed the same way an approved Page proposal is. Either:
sessions.crystallize()callsindex_db.index_page(...)afterstore.put_page(page)onsrc/vouch/sessions.py:109, mirroring thePAGE branch of
proposals.approve(); orKBStore.put_page()becomes responsible for keeping FTS5 in sync (and the redundant call inproposals.approve()is removed). This is the larger change and should probably go through a VEP, but the first option is a localised one-call fix.Either way,
session-<id>pages must be discoverable via the same search surface as any other approved page, without epending on avouch indexrebuild.Environment
state.dbpresent and non-empty (the bug is masked on empty / missing index databases because the substring fallback runs)Notes
PR #43 currently touches
sessions.py(thesession_endrefactor) but does not modify thecrystallizesummary-page write — a fix here will need a trivial rebase if both land.