cc-os/docs/memory-system/03-architecture-decisions.md

189 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architecture Decision Records
A running log of decisions and *why*. Format per entry: Context · Decision · Rationale ·
Alternatives rejected · Status. Newest decisions extend the log; supersede rather than delete.
---
## ADR-001 — Two memory types, kept as separate systems
- **Context**: Earlier attempts to make one tool serve both "what happened" and "how do we do
X" felt forced (e.g. trying to make memsearch filter knowledge by tags).
- **Decision**: Model **episodic** memory and **semantic/knowledge** memory as two separate
systems with different tools.
- **Rationale**: They have different lifecycles (episodic accretes and decays; knowledge is
deliberately maintained), different write paths (auto-captured vs curated with guardrails),
and different query patterns ("when did we…" vs "how do we…"). Separation dissolves the
earlier integration tension entirely.
- **Alternatives rejected**: One unified store (memsearch-for-everything, or OpenBrain's single
`thoughts` table) — conflates the two and forces awkward filtering.
- **Status**: Accepted.
## ADR-002 — memsearch for the episodic layer
- **Context**: Need timeline/"what happened" memory (Goal 3) that's NL-queryable and lazy.
- **Decision**: Adopt **memsearch** (Zilliz) off-the-shelf for episodic memory.
- **Rationale**: It already implements the OpenClaw daily-notes + "dreaming" pattern and the
markdown-as-truth / disposable-shadow-index philosophy we'd otherwise build. Embedded
**Milvus Lite** (single file), hybrid BM25+vector+RRF search, local ONNX embeddings (no API
key/cost), a FileWatcher that handles deletions — **no Docker, no server**. Two-line install.
- **Alternatives rejected**: claude-mem (MCP-based — Claude must actively call search; opaque
blobs vs readable markdown; overkill features). Hand-building daily notes + dreaming
ourselves (reinventing a solved tool).
- **Status**: Accepted.
## ADR-003 — Flat vault with namespaced tags, not folders
- **Context**: Connelly/Huryn organize by folders (`tools/`, `domain/`). User wants a flat
Obsidian vault with tags as virtual indexes, and cross-cutting filters (client × tool ×
convention).
- **Decision**: One **flat markdown vault**; organize via **namespaced, nested tags**
(`tool/`, `client/`, `domain/`, `convention/`, `scope/`). Slashes are valid Obsidian nested
tags, so `#tool` matches all children.
- **Rationale**: A note can carry several namespaces at once (`tool/semrush` +
`client/sesame3g` + `convention/react-ts`) — folders can't express that. Enables "filter by
client+tool to narrow the index." Enumerable virtual indexes ("what clients/tools exist").
- **Alternatives rejected**: Folder hierarchy (single-axis; can't do cross-cutting filters).
Pure-prefix path filtering via memsearch `source_prefix` (would force directories back in).
- **Trade-off accepted**: Tags give the *human/Obsidian* free filtering, but the *AI* gets
nothing for free from tags — we must materialize them into a queryable index (see ADR-004).
- **Status**: Accepted.
## ADR-004 — SQLite + Sequel (Ruby) tag index as the knowledge-layer cache
- **Context**: The AI can't use Obsidian tags directly; tag filtering needs a machine-queryable
index. A previous `~/Documents/SecondBrain/` tag database was lost track of.
- **Decision**: A small **Ruby program using the Sequel ORM over SQLite**, exposed as a **CLI**.
Schema: `files(path, mtime, summary, scope)`, `tags(name)`, `files_tags` join
(`many_to_many`). The summary is a **column on `files`** (an attribute), not a join.
- **Rationale**: Normalized `tags` table makes enumerating the vocabulary a first-class cheap
query (the "virtual index" goal). The `summary` column is what turns the index from a
*finder* into a *router* — the AI sees enough to pick a file without opening it (progressive
disclosure, low tokens). Ruby + Sequel + CLI keeps the contract clean and the DB swappable;
the AI never touches SQLite directly.
- **Failure-mode guard (the lost-SecondBrain lesson)**: **markdown is always authoritative; the
SQLite file is a disposable cache** that is never synced and can be rebuilt from frontmatter
anytime (`index update --rebuild`).
- **Alternatives rejected**: Plain-markdown generated `INDEX.md` (must regenerate; grep-at-scale
is token-heavy). Frontmatter grep on demand (scales badly). Milvus/Postgres for knowledge
(overkill; QMD/memsearch prove SQLite is enough — see ADR-006/008).
- **Query output**: returns **path + summary + matched tags** (option C) — tags are cheap and
show *why* a result matched, useful for cross-client queries.
- **Status**: **Superseded by ADR-010** (Graphify replaces the Ruby/SQLite tag index). The
`summary` + namespaced-tag frontmatter this ADR introduced is **retained as note metadata**;
only the bespoke Ruby/SQLite index and its CLI are dropped.
## ADR-005 — Structured-first; semantic search over the vault deferred
- **Context**: Tag filtering ("client/sesame3g + tool/semrush") may miss notes whose wording
doesn't match the query ("how do we use semrush" vs a note titled "search analytics
integration").
- **Decision**: Ship the knowledge layer **structured-only** (tags + summaries). **Defer**
meaning-based search over the vault until it demonstrably bites.
- **Rationale**: Structured tagging is the lightweight/fast thing the user wants, and the
summary+tag design is built to make it work. Follow the video's "only level up when it bites."
- **Status**: **Superseded by ADR-010.** The premise (ship structured-only, bolt on semantics
later) no longer holds: Graphify makes the knowledge layer a graph from day one, giving
structured *and* connection-based recall together. The "only level up when it bites" instinct
carries forward to whether a *vector* layer is ever needed on top of the graph.
## ADR-006 — QMD as the (deferred) semantic-over-knowledge layer
- **Context**: When ADR-005's structured-only proves insufficient, we want a set-and-forget
semantic layer over the vault, local and Docker-free.
- **Decision**: Earmark **QMD** (github.com/tobi/qmd) for that role; do **not** install yet.
- **Rationale**: Local markdown search using **SQLite + FTS5/BM25 + local vector embeddings
(EmbeddingGemma-300M GGUF) + LLM rerank**; CLI + optional **MCP server**; no Docker, no API
keys. Validates that SQLite + a local vector model suffices (no Milvus/Postgres for
knowledge). Complements the tag index (QMD filters by path/collection context, not
first-class frontmatter tags), so it adds semantic recall without replacing structured
filtering.
- **Alternatives rejected**: Pointing memsearch at the vault (mixes episodic and knowledge
corpora; its filtering is path-prefix not tags). A bespoke embedding index (reinvents QMD).
- **Status**: **Superseded by ADR-010.** Graphify's knowledge graph fills the
semantic-recall role (traversal/`explain` over connections) without a separate vector system,
so QMD is no longer earmarked. Revisit a vector layer only if graph traversal demonstrably
misses cases where embedding similarity would win.
## ADR-007 — Lazy freshness: write-hook + session-start reconcile, no daemon/cron
- **Context**: The cache must reflect new/edited/deleted/renamed notes without becoming a
resource hog or going stale on renames.
- **Decision**: **Option A (lazy).** A `PostToolUse` hook updates the index on **AI** writes
(single-file, prunes on delete). **Manual** edits are caught by a **session-start reconcile**
(`index update --since` + prune of vanished paths). **No daemon, no cron.**
- **Rationale**: The AI is the primary writer, so write-time hooks give event-driven freshness
with no polling. The user rarely edits the vault by hand, so a session-start reconcile is
enough; a continuous `inotify` daemon (the `listen` gem) would add an always-on process to
manage/sync for negligible benefit. Matches the user's "lazy sync is fine" stance.
- **Alternatives rejected**: `inotify`/`listen` daemon (live freshness, but always-on process
to manage — unnecessary). Cron reconcile ("seems silly" per user; session-start covers it).
- **Status**: Accepted.
## ADR-008 — Markdown-as-truth; sync the vault, not the indexes
- **Context**: Must be accessible on a VPS / multiple machines but run local-fast (Goal 4).
- **Decision**: Sync the **markdown vault** to the VPS via **git or Syncthing** (choice deferred
to build time). **Graphs/indexes (Milvus Lite, Graphify `graphify-out/`) are rebuilt per
machine and never synced.**
- **Rationale**: Markdown is plain text — git/Syncthing sync it trivially; lazy (hourly or
continuous-async) is enough. Indexes are disposable caches; syncing binary DBs invites
conflicts for no gain. Local reads stay fast; ownership and portability stay with the user.
- **Alternatives rejected**: **OpenBrain / Mem0** hosted DBs — always-remote, adds per-query
latency and monthly cost, conflicts with local-fast; ownership weaker (Mem0 especially).
Only worth it for real-time cross-tool memory, which the user called overkill.
- **Status**: Accepted.
## ADR-009 — Package as a global Claude Code plugin with skills
- **Context**: Every project, on every machine, should know how to use the vault — write
conventions, query patterns, the hooks, and the CLI — without per-project setup.
- **Decision**: Ship hooks + scripts + CRUD know-how as a **global Claude Code plugin with
skills**, installed at the user level.
- **Rationale**: Skills carry the "when to write / what conventions / how & when to query"
guidance to the model; the plugin registers the session-start / session-end / PostToolUse
hooks and wires up Graphify (extraction/update/query + MCP server). Global install =
consistent behavior everywhere; single source of truth for the conventions themselves.
- **Status**: Accepted (to be built — see 04-build-plan.md).
## ADR-010 — Graphify knowledge graph as the knowledge layer (supersedes ADR-004/005/006)
- **Context**: ADR-004 specced a hand-built Ruby/Sequel/SQLite tag index (+ CLI) as the
machine-queryable layer over the vault, with ADR-005/006 deferring meaning-based recall to a
future QMD vector layer. Before building any of it, we evaluated **Graphify** (`graphify`,
PyPI `graphifyy`) — a tool that turns a folder into a queryable knowledge graph (local
tree-sitter AST for code, local-SLM entity/relationship extraction for docs). See
`06-graphify-evaluation.md`.
- **Decision**: Use **Graphify as the knowledge-layer engine** over the vault, with a **local
Ollama** backend for doc extraction and free AST for per-project code graphs. **Drop** the
Ruby/SQLite tag-index CLI (ADR-004) and the earmarked QMD layer (ADR-006); **retain** the
`summary` + namespaced-tag frontmatter from ADR-003/004 as note metadata.
- **Rationale**: One off-the-shelf tool delivers both what the tag index was for (structured
retrieval) and what QMD was deferred for (connection/meaning-based recall via graph
traversal + `explain`) — without writing or maintaining a bespoke index, and without a vector
store. Code graphs come free. Keeps the markdown-as-truth, no-Docker, no-API-key, local-first
properties (extraction runs against local Ollama). Net scope reduction: the entire Ruby build
(old critical-path Step 2) and the QMD layer are removed.
- **What's retained / changed**: `summary` stays the human-written router hint Graphify does not
generate; namespaced tags stay useful for Obsidian filtering and as node attributes. How
tightly metadata should feed graph queries is a **build-time refinement**, not settled here.
- **Trade-off accepted**: Graphify's `--update` doesn't prune deleted nodes (stale-node drift) —
mitigated by a periodic `--force` rebuild on the session-start staleness check (ADR-007's lazy
model still applies). Graphify also moves fast (flags are version-dependent; anchored to
v0.8.30) and its headline token-savings numbers are corpus-dependent — benchmark our own.
- **Alternatives rejected**: Building the Ruby/SQLite index as originally planned (more code to
own; no semantic recall); adding QMD as a second system on top (two stores where one graph
suffices).
- **Status**: Accepted (to be built — see 04-build-plan.md and 06-graphify-evaluation.md).
## Rejected tools (summary)
| Tool | Why rejected for our use |
|------|--------------------------|
| MemPalace (L4) | Storage not readable markdown; isolated drawers (knowledge not interconnected); fights self-managing + cross-linking goals |
| Recall / LightRAG (L5) | Content knowledge bases / deep research, not operational memory; Recall = hosted, you don't own data; LightRAG = enterprise overkill |
| OpenBrain / Mem0 (L6) | Always-remote DB → latency + cost; conflicts with local-fast lazy-sync; only pays off for real-time cross-tool memory (user: overkill) |
| Postgres / Milvus server | Unnecessary — Graphify's local graph (knowledge) + Milvus Lite (memsearch episodic) cover everything locally with no Docker |
| claude-mem | MCP-based (Claude must call search); opaque blobs vs readable markdown; feature overkill |
| Ruby/SQLite tag index CLI; QMD vector layer | Superseded by Graphify before build — one knowledge graph replaces both the structured index and the deferred semantic layer (ADR-010) |