Skip to content

bug: crystallize writes session-summary page without FTS5 indexing — pages invisible to kb.search #60

Description

@RenzoMXD

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

  1. 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
  2. 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"
  3. Confirm the summary page exists on disk:
    ls .vouch/pages/session-*.md      # present
  4. Search for any token from the summary page title or body:
    vouch search "Session $SID"       # no page hit
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions