Add 3-layer memory system design documentation

This documents the complete architecture for a personal, cross-project
memory system for Claude Code: three-layer (episodic/knowledge/deferred-
semantic) design using Milvus Lite for fast local storage with lazy
remote sync. Includes video synthesis, architecture decisions with
rejected alternatives (MemPalace, Mem0, Postgres, LightRAG), concrete
build plan with scripts and hooks, and initial handoff for implementation.
This commit is contained in:
jared 2026-06-03 16:47:50 -04:00
parent b0695dd4b5
commit 54b00364cd
6 changed files with 720 additions and 0 deletions

View File

@ -0,0 +1,39 @@
# Memory System — Documentation Set
A personal, cross-project memory system for Claude Code: lightweight, fast, tag-organized,
self-evolving, local-fast with lazy remote sync. Built for a multi-client freelancer who
wants the AI to learn things once and reuse them everywhere, without bloating project repos.
## Read in this order
1. **[01-video-synthesis.md](01-video-synthesis.md)** — Synthesis of the "6 Levels of Claude
Code Memory" video. What each level/tool is, where memory lives, how it's retrieved, and
the author's recommendations. Background reading; the source of many ideas below.
2. **[02-system-design.md](02-system-design.md)** — The architecture we settled on. Three
layers (episodic / knowledge / deferred-semantic), the knowledge-layer internals, data
model, hooks, sync, and guardrails. **Start here if you only read one.**
3. **[03-architecture-decisions.md](03-architecture-decisions.md)** — ADR log. Every decision
and *why*, including what we rejected and why (MemPalace, OpenBrain/Mem0, Postgres,
Recall/LightRAG) and what we deferred (semantic search over the vault).
4. **[04-build-plan.md](04-build-plan.md)** — How a human builds this, step by step. The
scripts, the hooks, the CRUD lifecycle, the AI's write/query conventions, and the
Claude Code plugin + skills that package it for global install.
5. **[05-handoff.md](05-handoff.md)** — Where we are, what's decided, what's open, and the
first concrete actions for the next session.
## One-paragraph summary
Two complementary memory types kept as **separate systems**: **episodic** ("what happened,
when") handled by **memsearch** (Milvus Lite, embedded, auto-captured session/journal notes),
and **semantic/knowledge** ("how do we…") handled by a **flat Obsidian markdown vault** that
is the single source of truth, organized by **namespaced nested tags** (`tool/`, `client/`,
`domain/`, `convention/`, `scope/`) rather than folders, and indexed by a small **Ruby +
Sequel + SQLite** tag index exposed through a CLI. Retrieval is **hook-injected and
on-demand** so project repos stay thin. Freshness is **lazy** (a write-time hook plus a
session-start reconcile — no daemon, no cron). Everything is markdown-as-truth and syncs to a
VPS via **git/Syncthing**; the indexes are disposable and rebuilt per machine. A meaning-based
search layer over the vault (**QMD**) is designed-in but **deferred** until structured tagging
proves insufficient. The whole thing ships as a **global Claude Code plugin with skills** so
every project knows how to use it.
_Last updated: 2026-06-03_

View File

@ -0,0 +1,122 @@
# Video Synthesis — The 6 Levels of Claude Code Memory
Synthesis of the YouTube video "The 6 Levels of Claude Code Memory" plus the referenced
articles (John Connelly / Pawel Huryn). This is background: the source of several ideas in our
design. Our system borrows from Levels 13 and deliberately ignores 46.
## The framing
Every memory system answers the same question: **when you give Claude Code a task, how does it
pull the right context at the right time?** Each level differs on just two axes:
- **Where memory lives** — the storage mechanism and file structure (markdown vs vectors,
local vs cloud).
- **How Claude gets it** — the retrieval stage (auto-injected into context, searched on
demand in a DB, etc.).
The recurring enemy is **context rot**: as you load more context, the model recalls less of it
reliably. The cure throughout is **progressive disclosure** — load a small *index*, pull the
detail only when needed.
## The levels
### Level 1 — What ships natively (CLAUDE.md + memory.md / automemory)
- **CLAUDE.md**: plain markdown in the project, always loaded every session like a system
prompt. Hierarchical (project-folder → root → individual project); lower levels inherit the
parent but local rules win on conflict.
- **Common mistake**: stuffing it full (brand guide, tone doc, client list) → burns context →
context rot. **Rule of thumb: keep CLAUDE.md under 200 lines**; push larger context into
separate files *referenced* from CLAUDE.md so they load only when needed.
- **memory.md / automemory**: `/memory` shows imported/project/user memory. Auto-memory keeps a
per-project `memory.md` that acts as an **index of pointers** to many small memory files —
Claude quietly takes notes in the background and builds the index.
- Anthropic is clearly working on this natively (leaked references to an unreleased always-on
consolidation daemon, "Chyros"). Native memory will only get better.
### Level 2 — Forcing reliable recall (Connelly hook / Huryn structure)
- Paste a memory-management prompt + rules into CLAUDE.md. Structured memory rooted at
`~/.claude/memory/`: `memory.md` (index), `general.md` (cross-project facts/prefs/env),
`domain/<topic>.md` (one file per topic), `tools/<tool>.md` (one file per tool, e.g.
`slack.md` with config/workarounds/edge cases).
- A **session-start hook** auto-injects the **index** (not full content) into every session and
sub-agent — not relying on Claude choosing to read it.
- A **"reorganize memory"** command periodically reads all memory files, removes
duplicates/outdated entries, merges related ones, splits overloaded files, sorts by date,
and rebuilds the index.
- Huryn's updated post (productcompass.pm) adds **active hypothesis tracking**, a **catalog of
"false beliefs,"** and an **AI-proposes-reorganization / human-keeps-editorial-control**
loop. The transferable mechanic (not the content-writing domain) is the hypothesis/
false-belief log + propose-and-approve loop — useful for per-client "what we tried / decided
/ what didn't work."
- **Author's take: most people should stop here.**
### Level 3 — Search by meaning, not keywords (memsearch; OpenClaw template)
- Add only if you've used Claude Code > 1 month, have many memory files, and have asked
something you *know* is in your notes that Claude couldn't find.
- **OpenClaw memory design (3 layers)**: (1) `memory.md` = long-term durable facts, loaded at
session start; (2) **daily note files** = one per date, a running log, today+yesterday
auto-loaded, older left on disk; (3) optional **"dreaming"** = background process that scores
daily notes and promotes recurring ones into long-term memory, forgetting stale stuff.
- **memsearch** (by Zilliz): ports this into Claude Code as a two-line plugin. Markdown-first,
same chunking/structure. Chunks into semantic vectors; a **user-prompt-submit hook
auto-injects the top-3 semantic matches** into every prompt. Plain-readable markdown.
- Alternative **claude-mem**: MCP-based, 3-tier storage (summaries/timeline/observations),
dashboard/team/cost features. Author's view: overkill, and MCP-based means Claude must
actively call the search tool; stores opaque blobs vs memsearch's readable markdown.
### Level 4 — Verbatim conversation recall (MemPalace)
- Local RAG, free, claims highest published benchmark. Stores words **verbatim** (nothing
summarized) indexed in a dense symbolic dialect ("memory palace": wings → rooms → closets →
drawers). Two DBs: SQL (entities/relationships) + Chroma (vector chunks). Background hooks on
session-end/pre-compaction. ~42ms retrieval.
- **Downsides**: storage is **not readable markdown**; drawers are **isolated** (knowledge not
interconnected); local-only.
### Level 5 — Self-organizing knowledge base (Karpathy LLM wiki; Recall; LightRAG)
- Karpathy's **LLM wiki**: `raw/` (you drop sources, Claude reads, never writes) + `wiki/`
(Claude owns entirely). Plain markdown, Obsidian graph. **Recall** is a hosted done-for-you
version (you don't own the data). **LightRAG** is enterprise knowledge-graph overkill.
- **Author's take**: these are for **content knowledge bases / deep research**, NOT operational
memory ("what did we decide about client X's landing page in March"). Skip for our use case.
### Level 6 — One brain for all AI tools (OpenBrain; Mem0)
- **OpenBrain**: memory in a **Postgres DB you own** (Supabase), one `thoughts` table (text +
embedding + tags + timestamp), MCP server fronting it so any AI tool shares the same memory.
Most portable/future-proof. **Downsides**: longer/harder setup; **always remote → latency on
every query**; small monthly cost. **Mem0**: cross-tool layer, fast setup, but data lives on
their servers permanently.
- **Author's take**: only if you live across many AI tools and want real-time shared memory.
## Author's recommendation
- Just starting → Level 1 done right.
- A bit in → Level 2 (Connelly hook). **Most should stop here.**
- Lots of context, losing old decisions → Level 3 (memsearch) or Level 4 (MemPalace).
- Levels 56 are a different realm for specific use cases.
- **Levels 1+2+3 stack** (similar folder structures). The author personally runs up to Level 3:
OpenClaw conventions + semantic search + injection hooks.
## What we took / left (see ADRs for why)
- **Took**: progressive-disclosure index (L1/L2), per-tool/per-domain granularity + reorganize
command + session hooks (L2), Huryn's propose-and-approve reorg loop, OpenClaw daily-notes +
dreaming and memsearch (L3).
- **Replaced**: L2's **folders** with **namespaced tags** in a flat vault.
- **Left**: MemPalace (opaque, isolated), Recall/LightRAG (content KB, not operational),
OpenBrain/Mem0 (always-remote, fights local-fast).
## Reference links
- John & Pawel's system — youngleaders.tech/p/how-i-finally-sorted-my-claude-code-memory
- Pawel Huryn (updated) — productcompass.pm/p/self-improving-claude-system
- memsearch — github.com/zilliztech/memsearch
- MemPalace — github.com/MemPalace/mempalace
- Karpathy LLM wiki — gist.github.com/karpathy
- Recall — recall.it · Mem0 — mem0.ai · OpenBrain — github.com/NateBJones-Project
- QMD (our deferred semantic layer) — github.com/tobi/qmd

View File

@ -0,0 +1,173 @@
# System Design
_Status: approved 2026-06-03. Implementation not yet started._
## Goals (what this system must do)
1. **Thin projects** — keep as little AI context inside each project repo as possible. Projects
focus on project files; knowledge is pulled in on demand or injected by hooks.
2. **Cross-project / cross-client knowledge** — the AI learns something once (e.g. the SEMrush
API) and references it from anywhere. Two scopes: **global** (broadly useful) and
**project/client-specific** (how a given client uses a tool) — both globally reachable.
Ask anything client- or project-related from any project.
3. **Timeline awareness** — from any project, lightweight awareness of recent activity ("what
was I doing an hour ago / yesterday"), with the ability to drill deeper.
4. **Remote, local-fast** — accessible anywhere (VPS / personal OS) but runs local-fast; lazy
sync (minutes/hourly) is fine; real-time is overkill.
Desired properties: **lightweight** (low tokens), **fast** (out of the way), **flexible**
(cross project/client), **self-evolving** (AI maintains it under clear rules), **easy to
manage** (AI-managed), **semi-structured** (organization that can evolve).
## Core principle: two memory types, kept separate
| Type | Question | Lifecycle | Write path | Our tool |
|------|----------|-----------|-----------|----------|
| **Episodic** | "What happened, when?" | accretes & decays | auto-captured | **memsearch** |
| **Semantic / knowledge** | "How do we…?" | deliberately maintained | curated by you/AI | **Obsidian vault + tag index** |
This is the classic **episodic vs. semantic** memory split. Keeping them separate is the key
architectural decision — they have different lifecycles, write paths, and query patterns, and
forcing one tool to do both is what made every earlier design feel forced.
## The three layers
```
┌─────────────────────────────────────────────────────────────────┐
│ EPISODIC ── memsearch (Milvus Lite, embedded, no Docker) │
│ auto-captured session/journal notes · NL semantic recall │
│ answers "when did we…", "what was I doing yesterday" │
├─────────────────────────────────────────────────────────────────┤
│ KNOWLEDGE ── flat Obsidian vault (single source of truth) │
│ + Ruby/Sequel/SQLite tag index (CLI) │
│ structured tag filtering · answers "how do we…", "what do we │
│ know about X for client Y" │
├─────────────────────────────────────────────────────────────────┤
│ SEMANTIC-OVER-KNOWLEDGE ── QMD (SQLite+vector, MCP) [DEFERRED] │
│ meaning-based recall over the vault when tags miss │
│ add ONLY when structured tagging proves insufficient │
└─────────────────────────────────────────────────────────────────┘
```
All three are **local-first, markdown-as-truth, no Docker, no server, no API keys.**
## Layer 1 — Episodic (memsearch)
- **What it is**: a Claude Code plugin (by Zilliz) that auto-captures session notes as daily
markdown, chunks them, and stores a **shadow index** in **Milvus Lite** (a single embedded
file — no server, no Docker). Hybrid search = BM25 + dense vectors + RRF, local ONNX
embeddings (`bge-m3`, no API key/cost). A FileWatcher (1500ms debounce) handles updates and
deletions; markdown stays the source of truth.
- **Why off-the-shelf**: it already implements the OpenClaw daily-notes + "dreaming" pattern
and the markdown-as-truth / disposable-shadow-index philosophy we'd otherwise hand-build.
- **Role in our system**: satisfies Goal 3 (timeline). The AI queries it in natural language
("what was decided about X last week"). We do **not** make it filter by our tags — it owns
the episodic corpus only.
## Layer 2 — Knowledge (vault + tag index)
The heart of the system, and the part we build.
### Vault
- **Flat markdown directory**, single source of truth, **configurable location** (NOT forced
into `~/.claude/`; symlink if a tool insists). Browsable in Obsidian as a viewer.
- **Replaces project-local documentation**: instead of docs scattered per repo, knowledge
lives once in the vault and is pulled into any project on demand.
### Frontmatter contract (every note)
```yaml
---
summary: One line, written at creation. The router shows this so the AI can pick a
file without opening it.
tags:
- tool/semrush # namespaced, nested (slash = Obsidian nested tag)
- client/sesame3g
- domain/seo
- scope/project # or scope/global
---
```
- **Namespaces** are the "virtual indexes": `tool/`, `client/`, `domain/`, `convention/`,
`scope/`. `#tool` matches all children — native prefix filtering, no folders needed.
- **Two knowledge scopes** via `scope/global` vs `scope/project` (+ a `client/` tag): global =
broadly useful tool/domain knowledge; project = how a specific client uses it. Both are
globally queryable; the scope tag is the shortcut that avoids scanning every client's usage.
### Index (the tag cache)
A small **Ruby program, Sequel ORM, SQLite** — the disposable structured cache over the vault.
- **Schema**:
- `files(id, path, mtime, summary, scope)` — one row per note.
- `tags(id, name)` — one row per distinct tag (enables enumerating the vocabulary:
"what clients/tools do I have notes on?").
- `files_tags(file_id, tag_id)``many_to_many` join.
- **CLI** (the only interface; the AI never touches SQLite directly):
- `index update --since <mtime>` — incremental: pulls `.md` files with `mtime >` last cache
time, re-reads their frontmatter, upserts; **also reconciles** (prunes rows whose path no
longer exists).
- `index update --rebuild` — full rebuild from scratch (default `false`).
- `index query --client X --tool Y [--scope global] [--domain Z]` — returns **path +
summary + matched tags** for each hit (decided: option C). Tags shown so the AI sees *why*
a file matched.
- `index tags --namespace tool/` — enumerate a virtual index.
- **Source of truth rule**: markdown is authoritative; the SQLite file is a rebuildable cache
that is **never synced** and can be deleted/rebuilt anytime.
### Freshness (lazy — chosen Option A)
- **AI writes** → a `PostToolUse` hook on `Write`/`Edit` targeting vault `.md` files calls
`index update --file <path>` (updates exactly that file, prunes if deleted). Event-driven,
no polling, no staleness for AI edits.
- **Manual edits** (rare) → caught by a **session-start reconcile** (`index update --since` +
prune). **No daemon, no cron.**
### Retrieval (hook-injected + on-demand)
- **Session-start hook** injects: (a) a compact index/overview, (b) the current project's
declared `convention/*` tags **resolved to files** (so coding conventions auto-pull and a
convention edit propagates to every project using that tag), (c) a pointer to recent
episodic journal.
- **On demand**: the AI runs `index query` to pull specific knowledge into context only when
the task needs it. Projects stay thin — their CLAUDE.md holds **tags/pointers**, not content.
## Layer 3 — Semantic over knowledge (QMD) — DEFERRED
- **What**: QMD (github.com/tobi/qmd) — local markdown search, **SQLite + FTS5/BM25 + local
vector embeddings (EmbeddingGemma-300M GGUF) + LLM rerank**. CLI + optional **MCP server**.
No Docker, no API keys. Proves SQLite + a local vector model is enough — no Milvus/Postgres
for knowledge.
- **Why deferred**: start structured-only. Add QMD as a **set-and-forget** semantic layer over
the vault **only when** we catch ourselves failing to retrieve notes we know exist (the
video's "only level up when it bites"). It complements, not replaces, the tag index (QMD
filters by path/collection context, not first-class frontmatter tags).
## Timeline (Goal 3) details
- A **session-end hook** appends a daily journal note (one file per date) with pointers to the
project/knowledge files touched. memsearch indexes these; today+yesterday are cheap to load,
older entries are reachable by query for drill-down.
## Self-evolution guardrails
- The AI **writes only to the vault**, never silently into project repos.
- **Required frontmatter schema** (summary + namespaced tags) is enforced so the index stays
queryable.
- **Daily notes are append-only**; consolidation/reorg is a **separate, reviewable step run in
plan mode** (Connelly's reorganize + Huryn's propose-and-approve loop).
- **Promotion to `scope/global`** requires a rule (e.g. a fact recurring N times) — not every
stray note gets promoted.
## Sync (Goal 4)
- The **vault** syncs to the VPS via **git** (versioned history, hourly) or **Syncthing**
(continuous, zero-thought). Decision deferred to build time.
- **Indexes are never synced** — Milvus Lite and (later) QMD shadow indexes are rebuilt per
machine. Sync only the markdown.
## Packaging
- The whole thing ships as a **global Claude Code plugin with skills** (hooks + scripts +
CRUD know-how) so every project, on every machine, knows how to use the vault effectively.
See [04-build-plan.md](04-build-plan.md).
## How each goal is met
| Goal | Met by |
|------|--------|
| 1. Thin projects | Knowledge in the vault, not repos; CLAUDE.md holds tags/pointers; on-demand `index query` |
| 2. Cross-project/client knowledge, global vs project scopes | Flat vault + namespaced tags + `scope/` + `client/`; enumerable virtual indexes |
| 3. Timeline | memsearch episodic layer + session-end journal hook |
| 4. Remote, local-fast | Markdown vault synced via git/Syncthing; disposable per-machine indexes |

View File

@ -0,0 +1,149 @@
# 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**: Accepted.
## 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**: Accepted (semantic deferred).
## 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**: Deferred / earmarked.
## 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). **Indexes (Milvus Lite, future QMD) 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 bundles the Ruby CLI. Global install = consistent behavior everywhere; single
source of truth for the conventions themselves.
- **Status**: Accepted (to be built — see 04-build-plan.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 — SQLite (tag index) + Milvus Lite (memsearch) + QMD's SQLite cover everything locally with no Docker |
| claude-mem | MCP-based (Claude must call search); opaque blobs vs readable markdown; feature overkill |

View File

@ -0,0 +1,155 @@
# Build Plan
How a human builds this system, step by step, and answers to the operational questions:
which scripts and hooks, how the AI knows when to write and what conventions to follow, how and
when it queries, the CRUD hooks, and how it's packaged as a global Claude Code plugin with
skills.
> This is a build outline, not the implementation plan. The next session should turn this into
> a proper implementation plan (writing-plans skill) and execute it.
---
## Part A — Build order (human builder's path)
Build bottom-up: the vault and CLI first (usable standalone), then the hooks, then the plugin
that packages it.
### Step 1 — Vault skeleton & conventions
- Decide the vault location (default: a synced home dir, e.g. `~/brain`; symlink into
`~/.claude/memory` only if a tool insists). **Vault is the single source of truth.**
- Write `CONVENTIONS.md` in the vault: the frontmatter contract and the tag namespaces
(`tool/`, `client/`, `domain/`, `convention/`, `scope/`). This doubles as the spec the
skills teach the AI.
- Seed a few real notes (e.g. `tool/semrush` global + a `client/<x>` project note) to test
against.
### Step 2 — The Ruby tag-index CLI (the core build)
- Ruby project, `sequel` + `sqlite3` gems. SQLite file in a cache dir (e.g.
`~/.cache/memory-index/index.sqlite`) — disposable, never synced.
- **Schema / migrations**: `files(id, path, mtime, summary, scope)`, `tags(id, name)`,
`files_tags(file_id, tag_id)`; `many_to_many` between files and tags.
- **Frontmatter parser**: read YAML frontmatter from a `.md` file → `{summary, scope, tags[]}`.
Fail loudly (or quarantine) notes missing the required `summary`/tags so the contract is
enforced.
- **Commands** (a thin CLI dispatch — `thor`/`optparse` or plain):
- `index update --since <iso8601|auto>` — find `.md` with `mtime >` last-cache-time, upsert
them; **reconcile**: delete rows for paths no longer on disk.
- `index update --file <path>` — upsert a single file (used by the write hook); prune if the
path is gone.
- `index update --rebuild` (default off) — drop & rebuild from a full vault scan.
- `index query --client X --tool Y [--domain Z] [--scope global]` — AND the filters; output
JSON lines of `{path, summary, tags[]}` (option C).
- `index tags [--namespace tool/]` — enumerate the vocabulary (virtual index).
- Store `last_cache_time` (a tiny meta table or a stamp file) so `--since auto` works.
- **Tests**: create/edit/delete/rename a note, assert the index reflects it; rebuild equals
incremental.
### Step 3 — Hooks (maintenance + retrieval)
See Part C for exact mapping. Implement as small shell wrappers that call the Ruby CLI:
- `pre-tool-memory` style **PostToolUse** updater (write path → `index update --file`).
- **SessionStart** reconcile + inject (index overview + resolved `convention/*` files + journal
pointer).
- **SessionEnd** journal appender (daily note with pointers).
### Step 4 — Episodic layer (memsearch)
- Install memsearch (`/plugin marketplace add zilliztech/memsearch`, then `plugin install
memsearch`), local to start. Verify daily memory files appear after a few conversations.
- Decide whether memsearch indexes our session-end journal notes or its own capture (likely its
own; our journal can point into the knowledge vault).
### Step 5 — Sync
- Pick **git** (versioned, hourly push/pull) or **Syncthing** (continuous, zero-thought) for the
vault → VPS. Configure on each machine. **Do not sync the SQLite/Milvus caches.**
### Step 6 — Package as a global plugin (Part D)
- Wrap Steps 23 into a Claude Code plugin with skills; install at user level.
### Step 7 (deferred) — QMD semantic layer
- Only when structured-only retrieval misses notes you know exist: `qmd` over the vault,
optionally as an MCP server the AI can query.
---
## Part B — The AI's write & query conventions (skills teach these)
### When the AI WRITES to the vault
Trigger writes when the AI learns something **evergreen and reusable across projects** — not
project-ephemeral state (that's the episodic layer / project files). Concretely:
- It worked out how a tool/API behaves (e.g. SEMrush auth, rate limits, an endpoint quirk).
- It established a convention, decision, or preference that should apply beyond this task.
- It discovered a client-specific fact worth reusing (how *this* client uses a tool).
**What conventions to follow when writing** (enforced by the frontmatter contract):
- One concept per note; keep notes small (the L1 "under ~200 lines / one topic" discipline).
- **Required frontmatter**: a one-line `summary` (written now, not deferred), `scope/global`
or `scope/project`, and namespaced tags (`tool/…`, `client/…`, `domain/…`, optionally
`convention/…`).
- **Scope rule**: default new tool/domain knowledge to `scope/global`; mark `scope/project` +
a `client/<name>` tag when it's specific to how a client uses something.
- **Write only to the vault**, never silently into a project repo.
- **Promotion to global** (project → global) and **consolidation** are *not* done inline —
they happen in the reorganize step (Part C), in plan mode, for human review.
### When & how the AI QUERIES
- **At session start**: the injected index overview + resolved `convention/*` files tell it
what's available without reading everything.
- **On demand, during a task**: when the task touches a tool/client/domain, run
`index query --tool semrush --client sesame3g` → get `{path, summary, tags}` → open only the
files whose summary matches. This is the progressive-disclosure loop that keeps tokens low.
- **Cross-client lookups**: query by `--tool` alone (omit client) to surface what was learned
with *any* client; tags in the output show provenance.
- **"What happened" questions** go to the **episodic layer (memsearch)** in natural language,
not the tag index.
---
## Part C — Hooks & CRUD mapping
CRUD over the knowledge vault, and which hook/command services each operation:
| Operation | Trigger | Mechanism |
|-----------|---------|-----------|
| **Create** | AI writes a new note | AI writes `.md` (Write tool) → **PostToolUse** hook → `index update --file` |
| **Read / query** | AI needs knowledge | AI calls `index query …` (no hook; on-demand CLI) |
| **Update** | AI/user edits a note | AI edit → **PostToolUse** hook → `index update --file`; user edit → **SessionStart** reconcile |
| **Delete / rename** | note removed/renamed | covered by `--file` (prune) on AI ops; by **SessionStart** reconcile (prune vanished paths) on manual ops |
| **Inject** | new session/sub-agent starts | **SessionStart** hook injects index overview + resolved `convention/*` + journal pointer |
| **Journal** | session ends | **SessionEnd** hook appends a dated journal note with pointers |
| **Reorganize** | periodic, user-invoked | `reorganize memory` run in **plan mode**: dedupe, merge, split, re-tag, promote `scope/global`, rebuild index — human approves |
Hooks are thin shell wrappers over the Ruby CLI so the logic lives in one place.
**Hooks summary:**
- **SessionStart** — reconcile (`index update --since auto`) + inject context.
- **PostToolUse** (on `Write`/`Edit` of vault `.md`) — `index update --file`.
- **SessionEnd** — append daily journal note.
- (memsearch brings its own `UserPromptSubmit` + capture hooks for the episodic layer.)
---
## Part D — Claude Code plugin with skills (global install)
Goal: one global install so every project/machine knows the vault, conventions, hooks, and CLI.
**Plugin contents:**
- **Hooks** registered in settings: SessionStart, PostToolUse, SessionEnd (the shell wrappers
from Part C), pointed at a configurable vault path + cache dir.
- **The Ruby CLI** (the tag index) bundled or installed as a dependency, on `PATH`.
- **Skills** (these carry the *know-how* to the model):
- `memory-write` — when to record evergreen knowledge, the frontmatter contract, scope rules,
"vault not repo." (Part B write section.)
- `memory-query` — how/when to query the tag index vs the episodic layer; the progressive-
disclosure loop; cross-client lookups. (Part B query section.)
- `memory-reorganize` — the plan-mode consolidation/promotion procedure + guardrails.
- **Config**: vault path, cache path, sync method — set once at user level.
**Why a plugin + skills (not just CLAUDE.md):** hooks must be registered by the harness (the
plugin does that), and the conventions are taught as skills so they load on demand without
bloating every project's context. A single global install keeps the conventions themselves a
single source of truth — edit the skill once, every project follows.
**Open question for build time**: do the coding-`convention/*` notes live *in the vault* (data,
edited freely, resolved by the SessionStart hook) while the *memory-system skills* live *in the
plugin* (behavior, versioned)? Recommended: yes — conventions are data in the vault; the skills
are the mechanism.

View File

@ -0,0 +1,82 @@
# Session Handoff
_Written 2026-06-03 at the end of the brainstorming session. For the next session to pick up._
## What this is
Designing a personal, cross-project **memory system for Claude Code** for a multi-client
freelancer. We finished **brainstorming/architecture** and wrote the design + ADRs. **No
implementation has started.** The source material is the transcript at
`/home/jared/Documents/cc-os/memory-systems-compared060326` (the "6 Levels of Claude Code
Memory" video).
## Where we are
- ✅ Synthesized the video (`01-video-synthesis.md`).
- ✅ Settled the architecture and got user sign-off (`02-system-design.md`).
- ✅ Recorded every decision + rejected/deferred options (`03-architecture-decisions.md`).
- ✅ Wrote the build outline + operational answers (`04-build-plan.md`).
- ⏳ **Next**: turn `04-build-plan.md` into a real implementation plan and start building.
## The design in 30 seconds
Three local, markdown-as-truth, no-Docker layers:
1. **Episodic** ("what happened, when") = **memsearch** (Milvus Lite, off-the-shelf).
2. **Knowledge** ("how do we…") = **flat Obsidian vault** (single source of truth) + a
**Ruby/Sequel/SQLite tag index** with a CLI. Organized by **namespaced nested tags**
(`tool/ client/ domain/ convention/ scope/`), not folders.
3. **Semantic over the vault** = **QMD**, **deferred** until structured tagging proves
insufficient.
Retrieval is hook-injected + on-demand (projects stay thin). Freshness is lazy (write-time
hook + session-start reconcile; no daemon/cron). Vault syncs to a VPS via git/Syncthing;
indexes are disposable and rebuilt per machine. Ships as a **global Claude Code plugin with
skills**.
## Decisions locked (don't relitigate without reason)
- Two separate systems for episodic vs knowledge (ADR-001).
- memsearch for episodic (ADR-002). Flat vault + tags, not folders (ADR-003).
- SQLite + Sequel (Ruby) CLI tag index; markdown authoritative, cache disposable (ADR-004).
- Structured-first; semantic (QMD) deferred (ADR-005/006).
- Lazy freshness: PostToolUse write hook + SessionStart reconcile, no daemon/cron (ADR-007).
- Sync the vault not the indexes; reject OpenBrain/Mem0/Postgres (ADR-008).
- Package as a global plugin with skills (ADR-009).
- Query output = path + summary + matched tags (option C).
## Open questions for build time
1. **Vault location** — default `~/brain` (or similar synced home dir)? Symlink into
`~/.claude/memory` only if a tool requires it.
2. **Sync mechanism** — git (versioned history, hourly) vs Syncthing (continuous, zero-thought).
3. **Convention notes placement** — confirmed direction: coding `convention/*` live as **data
in the vault** (resolved by the SessionStart hook); the memory-system **skills live in the
plugin** (behavior, versioned). Validate when building.
4. **memsearch journal** — does memsearch index our SessionEnd journal notes, or only its own
auto-capture? Decide how the journal points into the knowledge vault.
5. **Promotion rule** — the concrete threshold for project→`scope/global` promotion (e.g.
recurrence count) used by the reorganize step.
6. **CLI ergonomics** — command framework (thor vs optparse), output format details, where
`last_cache_time` is stored.
## Recommended first actions next session
1. Re-read `02-system-design.md` and `04-build-plan.md`.
2. Invoke the **writing-plans** skill to convert `04-build-plan.md` Part A into a staged
implementation plan (Step 2, the Ruby tag-index CLI, is the critical path — build and test
it standalone first).
3. Resolve open questions 12 (vault location + sync) before writing code, since they affect
paths in the hooks and CLI.
4. Build Step 2 (CLI) with tests, then Step 3 (hooks), then validate end-to-end on the seeded
notes before touching the plugin packaging (Step 6).
## Context notes
- Working dir `/home/jared/Documents/cc-os` is **not a git repo**; consider `git init` if we
want history for these docs and the build.
- User prefers **Docker when reasonable** — but this design deliberately needs **none** (all
layers are embedded/local). Only full Milvus or QMD-server modes would involve Docker, and
we're not using those.
- User is Ruby-comfortable (chose Ruby + Sequel deliberately).
- A past `~/Documents/SecondBrain/` attempt (tag DB) was lost track of — the "markdown
authoritative, cache disposable & rebuildable" rule (ADR-004) is the explicit fix for that.