feat: cross-tool memory via FTS5 index#21
Merged
Conversation
Single SQLite/FTS5 store under ~/.config/vibecockpit/cache/memory.db
indexes every transcript message from every provider so Claude Code,
Cursor, Codex, etc. can all recall what was said in any prior session.
Surfaces:
- Web: dedicated Memory page with search, ±N context drawer, host badge,
Active/Excluded tabs, per-result delete, file-based import/export.
- MCP: search_memory + get_session_context tools. Agents share the
same backend the user does.
- CLI: vibecockpit memory {reindex,search,stats,export,import}.
Index design:
- Per-message FTS5 rows (porter+unicode61, BM25), dedup-by-best-message
on read so the list view stays clean while context anchors stay
precise.
- Sessions stamped with os.Hostname() at index time so cross-machine
imports keep their origin label; orphan cleanup is host-scoped so
imported rows aren't pruned just because their JSONL isn't local.
- session_tombstones table makes user-deleted sessions sticky across
reindex AND import; restorable from the Excluded view.
- Idempotent re-import: session_meta presence + WHERE filters mean
uploading the same .db twice is a no-op (no message duplication).
- Lenient query parser: punctuation barewords (example.com, v1.2.3,
feat/login) auto-quote on FTS5 syntax-error retry; power-user
expressions like "auth NOT okta" still parse first try.
Also folded in:
- a11y warnings cleared across Memory, Scheduler, ChatView (drawer
overlay → button, role/dialog combo, fieldset+legend for grouped
checkboxes, $state for bind:this targets).
- Web kicks off async IndexAll on boot and on every scan-cache refresh.
- All MCP tool calls audit-logged via existing s.audit.Log path.
- memory_cli: split each subcommand into its own error-returning helper;
runMemory dispatches and exits centrally. Lets the deferred Close
actually run (gocritic exitAfterDefer) and silences the unchecked-Close
errcheck via the standard `defer func(){ _ = idx.Close() }()` pattern.
- index.Search: add a //nolint:gosec G201 suppression with a comment
explaining that the only formatted SQL fragment is built from a fixed
literal allowlist; all user values stay parameterized via ?.
The previous nolint directive sat on the closing line of a multi-line fmt.Sprintf, where golangci-lint's nolint scanner doesn't apply it. Lift the SQL into a package-level searchSQLTemplate const so the call itself is one line, and the //nolint:gosec directive sits next to it. Verified locally with the same golangci-lint v2.1.6 the CI workflow uses (v2.1.6 reports 0 issues).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
~/.config/vibecockpit/cache/memory.dbthat indexes every transcript message from every provider, so Claude Code / Cursor / Codex / Gemini all share the same memory substrate.search_memory+get_session_contexttools (agents see what the human sees).vibecockpit memory {reindex,search,stats,export,import}.Notable design choices
os.Hostname()at index time. Orphan cleanup is host-scoped, so imported rows aren't pruned just because their JSONL isn't local.session_tombstonestable — user deletes are sticky across reindex AND import. Restorable from the Excluded view..dbis a no-op (no message duplication;session_metapresence + tombstone filters guard both directions).example.com,v1.2.3,feat/login) auto-quote on FTS5 syntax-error retry. Power-user expressions likeauth NOT oktastill parse first try.IndexAll, repeats on every scan-cache refresh. MCP tool calls audit-logged via existing path.<button>, dialog role conflict,<fieldset>/<legend>for grouped checkboxes,$state()forbind:thistargets).Test plan
make check(CI runs golangci-lint + race tests + frontend lint)added: 0and stats unchangedsearch_memorywith punctuation tokens (example.com,v1.2.3) returns hits without manual quotingget_session_contextwindow from 0 to 50 returns the expected message slice withisCenterflagged