412 lines
23 KiB
Markdown
412 lines
23 KiB
Markdown
# Memory Architecture — Waggle OS
|
||
|
||
**Audience:** Internal engineers and contributors touching `packages/hive-mind-core/src/mind/` or the agent recall path.
|
||
**Scope:** How memory is stored, searched, scored, and recalled — grounded in the source, not in CLAUDE.md.
|
||
**Status:** Current as of April 15, 2026. Schema version `1`.
|
||
|
||
> ⚠️ **SUPERSEDED / partially stale (2026-06-26).** This doc predates the April 2026 monorepo migration: the substrate moved from `packages/hive-mind-core/src/mind/` to **`packages/hive-mind-core/src/mind/`**, and several line-of-code counts here are out of date. For the current, code-verified architecture see **[`docs/transfer/MEMORY-SYSTEM-EXPLAINED.md`](./transfer/MEMORY-SYSTEM-EXPLAINED.md)**. This file is retained for historical reference.
|
||
|
||
> **Path discrepancy fixed:** An earlier revision of CLAUDE.md §2 listed memory-layer files (`frames.ts`, `awareness.ts`, etc.) at the top level of `packages/core/src/`. They actually live in `packages/hive-mind-core/src/mind/`. Corrected in the commit that promoted this doc.
|
||
|
||
---
|
||
|
||
## 1. Ten-second summary
|
||
|
||
A single SQLite database per mind (`better-sqlite3` + `sqlite-vec`, WAL mode, FK on) holds every memory layer. One file per mind, one schema, loaded via `MindDB`. Five cooperating layers sit on top of that database:
|
||
|
||
1. **Identity** — single-row table. Who this mind is.
|
||
2. **Awareness** — ≤10 rolling items. What this mind is working on right now.
|
||
3. **Frames** — append-only event log. Everything this mind has learned, as I/P/B frames grouped by GOP.
|
||
4. **Knowledge Graph** — entities and relations with temporal validity. The structured distillation of the frame log.
|
||
5. **Hybrid Search** — FTS5 + `vec0` fused via Reciprocal Rank Fusion, re-ranked by a configurable scoring profile.
|
||
|
||
Additional tables in the same DB: sessions (GOP → project mapping), improvement signals, install audit, procedures (GEPA-optimized templates), AI interactions (EU AI Act Art. 12), execution traces, evolution runs, harvest sources. Those are subsystems; the five above are the memory substrate.
|
||
|
||
Agents consume this through one function: `Orchestrator.recallMemory(query, limit)` — invoked on every user turn, fused across personal and workspace minds.
|
||
|
||
---
|
||
|
||
## 2. Storage substrate: `MindDB`
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/db.ts`
|
||
|
||
```ts
|
||
export class MindDB {
|
||
constructor(dbPath: string) {
|
||
this.db = new Database(dbPath);
|
||
this.db.pragma('journal_mode = WAL');
|
||
this.db.pragma('foreign_keys = ON');
|
||
const vecPath = process.env.WAGGLE_SQLITE_VEC_PATH;
|
||
if (vecPath) this.db.loadExtension(vecPath);
|
||
else sqliteVec.load(this.db);
|
||
this.initSchema();
|
||
}
|
||
}
|
||
```
|
||
|
||
Three facts to remember:
|
||
|
||
- **WAL mode is on.** Readers don't block writers. Don't turn it off.
|
||
- **`sqlite-vec` is loaded either from `WAGGLE_SQLITE_VEC_PATH` or the npm package.** The env-var override is the desktop-build path — Tauri bundles the native `.dll`/`.dylib` and the sidecar points at it. If you see "no such module: vec0" at runtime, the extension didn't load — check that env var.
|
||
- **Schema init is idempotent and runs migrations.** `initSchema` checks for the `meta` table. If absent: fresh DB, run `SCHEMA_SQL` + `VEC_TABLE_SQL`, write `schema_version = '1'`. If present: run `runMigrations()` (currently only adds the `source` column to `memory_frames` for pre-W2.1 databases).
|
||
|
||
Every layer class takes a `MindDB` and calls `db.getDatabase()` to get the raw `better-sqlite3` handle. There is no ORM. Every query is hand-written prepared SQL. That's a feature — it's a deliberate ~500 LOC of surface area, not 50k.
|
||
|
||
---
|
||
|
||
## 3. Schema at a glance
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/schema.ts` — one `SCHEMA_SQL` string for the whole mind, plus a separate `VEC_TABLE_SQL` for the virtual table.
|
||
|
||
| Table | Role | Layer |
|
||
|---|---|---|
|
||
| `meta` | schema_version | infra |
|
||
| `identity` | single-row CHECK (id = 1) | 0 — Identity |
|
||
| `awareness` | ≤10 active items, category-constrained | 1 — Awareness |
|
||
| `sessions` | `gop_id` ↔ `project_id`, status | infra |
|
||
| `memory_frames` | I/P/B, gop_id, t, importance, source, access_count | 2 — Frames |
|
||
| `memory_frames_fts` | FTS5 shadow, `content_rowid='id'`, porter+unicode61 | 2 — Frames |
|
||
| `memory_frames_vec` | `vec0(embedding float[1024])` virtual | 2 — Frames |
|
||
| `knowledge_entities` | typed nodes with `valid_from`/`valid_to` | 3 — KG |
|
||
| `knowledge_relations` | typed edges, confidence, temporal | 3 — KG |
|
||
| `procedures` | GEPA prompt templates | 4 — Procedures |
|
||
| `improvement_signals` | recurring patterns, `(category, pattern_key)` unique | 5 |
|
||
| `install_audit` | capability install trust trail | 6 |
|
||
| `harvest_sources` | import sync state | 8 |
|
||
| `ai_interactions` | EU AI Act Art. 12 event log | 7 |
|
||
| `execution_traces` | agent run history, outcome, trace_json | 9 |
|
||
| `evolution_runs` | self-evolution proposals/outcomes | 10 |
|
||
|
||
Embedding dimensionality is pinned at **1024**, baked into the vec0 DDL. If you change it, you must rebuild every vec table and re-embed every frame. Don't change it without a migration.
|
||
|
||
---
|
||
|
||
## 4. Layer 0 — Identity
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/identity.ts` — 73 LOC.
|
||
|
||
```sql
|
||
CREATE TABLE identity (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
name, role, department, personality, capabilities, system_prompt,
|
||
created_at, updated_at
|
||
);
|
||
```
|
||
|
||
One row. Primary key forced to 1 via CHECK. `IdentityLayer` exposes `create`, `get`, `exists`, `update`, and `toContext()`. The `toContext()` output is pasted directly into the system prompt under `# Identity` (see §8).
|
||
|
||
Everything but `name` defaults to empty string. A bare `name` is enough to have an identity. Token budget target: <500 tokens (per schema comment).
|
||
|
||
---
|
||
|
||
## 5. Layer 1 — Awareness
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/awareness.ts` — 170 LOC.
|
||
|
||
Working memory. Volatile, small, priority-ordered.
|
||
|
||
```sql
|
||
CREATE TABLE awareness (
|
||
id, category CHECK IN ('task','action','pending','flag'),
|
||
content, priority, metadata, created_at, expires_at
|
||
);
|
||
```
|
||
|
||
Hard cap: `MAX_ITEMS = 10`. `getAll()` and `getByCategory()` both apply `LIMIT 10`. `expires_at` is honored in every read query (`expires_at IS NULL OR expires_at > datetime('now')`).
|
||
|
||
Two things worth knowing:
|
||
|
||
- **`ensureMetadataColumn()` runs in the constructor** — PRAGMA-inspects the table and runs `ALTER TABLE ... ADD COLUMN metadata` if it's missing. This is the graceful-upgrade path for pre-metadata databases. If you ever see the metadata column missing on a new DB, something went wrong in `schema.ts`.
|
||
- **`getByStatus(status)` is an in-memory filter over parsed metadata JSON.** Each awareness item stores its own JSON blob — there's no index on status. Fine because `MAX_ITEMS = 10`.
|
||
|
||
`toContext()` groups active items by category (Active Tasks, Recent Actions, Pending Items, Context Flags) — this is what gets surfaced in the agent's self-awareness section of the system prompt.
|
||
|
||
---
|
||
|
||
## 6. Layer 2 — Frames
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/frames.ts` — 377 LOC. This is the heart.
|
||
|
||
### 6.1 Data model
|
||
|
||
```sql
|
||
CREATE TABLE memory_frames (
|
||
id, frame_type CHECK IN ('I','P','B'),
|
||
gop_id, t, base_frame_id REFERENCES memory_frames(id),
|
||
content, importance CHECK IN ('critical','important','normal','temporary','deprecated'),
|
||
source CHECK IN ('user_stated','tool_verified','agent_inferred','import','system'),
|
||
access_count, created_at, last_accessed
|
||
);
|
||
```
|
||
|
||
Three frame types:
|
||
|
||
- **I-frame (Index)** — full-content snapshot. Standalone. No base.
|
||
- **P-frame (Patch)** — incremental update referencing an I-frame via `base_frame_id`. Requires the base to reconstruct full state.
|
||
- **B-frame (Branch/reference)** — alternative to a frame without replacing it. Points at another frame via `base_frame_id`.
|
||
|
||
`gop_id` is the Group-of-Pictures identifier — a session/conversation/topic bucket. `t` is the monotonically-increasing sequence number within a GOP. The `(gop_id, t)` index is the primary scan path for reconstructing a GOP's history.
|
||
|
||
### 6.2 Frame lifecycle
|
||
|
||
Create, read, update, delete, reconstruct, compact. The three creates:
|
||
|
||
- `createIFrame(gopId, content, opts)` — `INSERT` with `frame_type='I'` and `base_frame_id=null`. Assigns `t` = `MAX(t)+1` for the GOP.
|
||
- `createPFrame(gopId, baseFrameId, content, opts)` — asserts the base is an I-frame in the same GOP before inserting.
|
||
- `createBFrame(gopId, referencedFrameId, content, opts)` — branch/alternative on any base.
|
||
|
||
Every insert triggers FTS indexing (`INSERT INTO memory_frames_fts(rowid, content)`) and — if an embedder is configured — vector indexing via `HybridSearch.indexFrame`. Vector indexing is wrapped in try/catch because `memory_frames_vec` may not exist in older DBs.
|
||
|
||
### 6.3 Reconstruction
|
||
|
||
`reconstructState(gopId)` returns a `ReconstructedState`:
|
||
|
||
```ts
|
||
{ iFrame: MemoryFrame, patches: MemoryFrame[], currentContent: string, frameCount: number }
|
||
```
|
||
|
||
Logic:
|
||
|
||
1. Find the most recent I-frame in the GOP.
|
||
2. Pull every P-frame with `base_frame_id = iFrame.id`, ordered by `t`.
|
||
3. Apply patches in order (current implementation concatenates — see §6.6 "Known rough edges").
|
||
|
||
### 6.4 Compaction
|
||
|
||
`compact({ deleteTempOlderThan = 30, deleteDeprecatedOlderThan = 90, compactPFramesWhen = 10 })`:
|
||
|
||
- Delete `temporary` frames older than 30 days.
|
||
- Delete `deprecated` frames older than 90 days.
|
||
- Per GOP: if P-frames on the current I-frame ≥ 10, merge them into a new I-frame and delete all but the **5 most recent** P-frames. (The recent tail is kept so branches still have something to diff against.)
|
||
|
||
This is designed to be run nightly or on-demand. It's not automatic.
|
||
|
||
### 6.5 Deduplication
|
||
|
||
`findDuplicate(content)` computes `SHA-256(content.trim())` and checks against the last 500 frames. Returns the frame ID if found, null otherwise. Callers should use this before `createIFrame` on import paths (Memory Harvest does).
|
||
|
||
Trim-stable hash means trailing/leading whitespace won't spawn duplicates. Case-sensitive, punctuation-sensitive. If you want semantic dedup, that's what vector search and `contradiction-detector.ts` are for — don't touch this.
|
||
|
||
### 6.6 Known rough edges
|
||
|
||
- **Patch application is string concatenation**, not a real diff. P-frames say "here's more" rather than "here's what changed at line N." Fine for the current usage (each P-frame is a new fact on top of the index), but it means you can't use P-frames to *retract* or *correct* — use `update()` on the I-frame or mark deprecated.
|
||
- **Vector indexing uses rowid-literal SQL** (see §7.2) because sqlite-vec cannot parameterize rowid. If you add a new insert path, remember to mirror the FTS + vec writes.
|
||
- **Access count updates** are best-effort — they happen on read paths, not inside a transaction. Under concurrent readers the count can drift; that's acceptable because it only feeds a log-scaled popularity score.
|
||
|
||
---
|
||
|
||
## 7. Layer 3 — Hybrid Search
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/search.ts` — 255 LOC.
|
||
|
||
### 7.1 Top-level shape
|
||
|
||
```ts
|
||
class HybridSearch {
|
||
async search(query, opts?: SearchOptions): Promise<SearchResult[]>
|
||
async keywordSearch(query, opts?): Promise<...>
|
||
async vectorSearch(query, opts?): Promise<...>
|
||
async indexFrame(frame): Promise<void>
|
||
async indexFramesBatch(frames): Promise<void>
|
||
}
|
||
```
|
||
|
||
`SearchOptions`: `limit`, `gopId`, `profile` (scoring profile — see §7.3), `context` (graph distances from the KG), `since`, `until`.
|
||
|
||
`SearchResult`: `{ frame, rrfScore, relevanceScore, finalScore }`.
|
||
|
||
### 7.2 The fusion
|
||
|
||
```
|
||
keyword results (FTS5) ─┐
|
||
├─► RRF (K=60) ─► re-rank via computeRelevance ─► finalScore = rrfScore * relevanceScore
|
||
vector results (vec0) ─┘
|
||
```
|
||
|
||
`const RRF_K = 60` at the top of the file — the canonical RRF constant, well-attested in the IR literature. Don't tune it without a reason.
|
||
|
||
Keyword path:
|
||
|
||
- Strip a hardcoded ~60-word stop-word list ("the", "is", "of", …) from the query.
|
||
- Build an **OR-joined** MATCH expression (`term1 OR term2 OR …`). We do not require all terms — memory queries are too short and ad-hoc for AND-matching to be useful.
|
||
- `SELECT rank FROM memory_frames_fts MATCH ? ORDER BY rank LIMIT ?`.
|
||
- Join back to `memory_frames` to hydrate the row.
|
||
|
||
Vector path:
|
||
|
||
- Embed the query via the injected `Embedder`.
|
||
- `SELECT rowid, distance FROM memory_frames_vec WHERE embedding MATCH ? ORDER BY distance LIMIT ?`.
|
||
- Hydrate from `memory_frames` by rowid.
|
||
|
||
Both paths respect `gopId`, `since`, `until` filters.
|
||
|
||
**RRF:** for each frame appearing in either result list, `rrfScore += 1 / (RRF_K + rank)`. This is the rank-fusion line — don't reimplement it elsewhere.
|
||
|
||
### 7.3 Scoring profiles
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/scoring.ts`
|
||
|
||
```ts
|
||
SCORING_PROFILES = {
|
||
balanced: { temporal: 0.4, popularity: 0.2, contextual: 0.2, importance: 0.2 },
|
||
recent: { temporal: 0.6, popularity: 0.1, contextual: 0.2, importance: 0.1 },
|
||
important: { temporal: 0.1, popularity: 0.1, contextual: 0.2, importance: 0.6 },
|
||
connected: { temporal: 0.1, popularity: 0.1, contextual: 0.6, importance: 0.2 },
|
||
};
|
||
```
|
||
|
||
Four signals, weighted:
|
||
|
||
- **Temporal:** full score for frames accessed in the last 7 days; exponential decay with 30-day half-life afterward.
|
||
- **Popularity:** `1 + log10(1 + access_count) * 0.1`. Log-scaled so a frame read 1000 times only beats a fresh one by ~0.3.
|
||
- **Contextual:** if the caller passed `graphDistances`, frames at BFS distance 0/1/2/3 from the query-relevant entities get scores 1.0/0.7/0.4/0.2. Otherwise 0.
|
||
- **Importance:** `critical=2.0, important=1.5, normal=1.0, temporary=0.7, deprecated=0.3` (these are the IMPORTANCE_WEIGHTS multipliers you'll see referenced across the codebase).
|
||
|
||
`finalScore = rrfScore * computeRelevance(frame, weights, context)`. Multiplicative on purpose — a frame with a rotten RRF score can't be saved by being important, and an important frame still needs to show up in one of the two index paths.
|
||
|
||
### 7.4 Indexing contract
|
||
|
||
Every `memory_frames` row should have a matching row in `memory_frames_fts` (rowid = frame id) and `memory_frames_vec` (rowid = frame id). If you write directly to `memory_frames` and skip `HybridSearch.indexFrame`, that frame is invisible to search. Don't do that.
|
||
|
||
`indexFramesBatch` exists for imports — uses a single transaction + `embedBatch` for throughput.
|
||
|
||
---
|
||
|
||
## 8. Layer 4 — Knowledge Graph
|
||
|
||
**File:** `packages/hive-mind-core/src/mind/knowledge.ts` — 265 LOC.
|
||
|
||
```sql
|
||
knowledge_entities(id, entity_type, name, properties, valid_from, valid_to, recorded_at)
|
||
knowledge_relations(id, source_id, target_id, relation_type, confidence, properties, valid_from, valid_to, recorded_at)
|
||
```
|
||
|
||
Two departures from a vanilla graph DB matter:
|
||
|
||
### 8.1 Bitemporal validity
|
||
|
||
`valid_from` and `valid_to` are the "when this was true" dimension. `recorded_at` is the "when we wrote it down" dimension. A fact that was true last month and is no longer true now sets `valid_to` to last month's date. Reads default to *current* ("valid_to IS NULL OR valid_to > now"). Historical queries can override.
|
||
|
||
`retireEntity(id)` and `retireRelation(id)` set `valid_to = datetime('now')`. We soft-delete. Hard-delete is only for schema violations.
|
||
|
||
### 8.2 Schema validation
|
||
|
||
`ValidationSchema` defines required properties and allowed relation types per entity type. Violations throw at insert time. This is the main reason entities are typed rather than free-form: catch bad data at write time, not at query time.
|
||
|
||
### 8.3 Traversal
|
||
|
||
`traverse(startId, depth)` is BFS, returns visited entity set. `bfsDistances(frameAnchoredEntityIds)` is the one that feeds `HybridSearch`'s contextual score — returns `Map<frameId, distance>` for the closest entity.
|
||
|
||
---
|
||
|
||
## 9. How the agent actually uses it
|
||
|
||
**File:** `packages/agent/src/orchestrator.ts`
|
||
|
||
### 9.1 System prompt assembly
|
||
|
||
`Orchestrator.buildSystemPrompt()` stitches three sections:
|
||
|
||
1. **Identity** (cached) — `this.identity.toContext()` under `# Identity`. Cache key is `'exists'|'empty'`.
|
||
2. **Self-awareness** (uncached) — `buildSelfAwareness(caps)` with tools, skills, memory stats, mode, version, and any actionable `improvementSignals`.
|
||
3. **Recent context** (uncached) — `loadRecentContext()` output under `# Context From Your Memory`.
|
||
|
||
The identity section is cached because it changes rarely. The other two are rebuilt every turn.
|
||
|
||
### 9.2 Recall path
|
||
|
||
`Orchestrator.recallMemory(query, limit = 10)` runs on every user turn.
|
||
|
||
Two modes:
|
||
|
||
- **Catch-up mode** — triggered by a regex match on the query ("catch me up", "where were we", "what did we decide", "brief me", "what's next", etc.). Bypasses semantic search. Goes straight to `memory_frames` and pulls:
|
||
- up to `limit` frames with `importance IN ('critical','important')` OR `content LIKE 'Decision%'` OR `content LIKE '%decided%'`, ordered by importance then recency,
|
||
- plus the 3 most recent non-deprecated, non-temporary frames for recency context,
|
||
- deduplicated by first 100 chars of content.
|
||
This is the right move: when a user says "catch me up," they want the important things, not the semantically-closest things. Literal text similarity will return recent small talk over last week's critical decision every time.
|
||
- **Normal mode** — `this.search.search(query, { limit, profile: 'balanced' })` on both the personal mind and the workspace mind (if present), fused into one result list with source attribution.
|
||
|
||
The returned `{ text, count, recalled? }` gets injected into the conversation as additional context before the model sees it.
|
||
|
||
### 9.3 Per-workspace minds
|
||
|
||
An agent instance can hold one personal mind + one workspace mind. Each is a full `MindDB` + layer stack. The workspace mind is scoped to a workspace; the personal mind is scoped to the user. `recallMemory` queries both. Frames written during a workspace session land in the workspace mind.
|
||
|
||
This is why the codebase has `FrameStore` and `HybridSearch` instantiated twice on the `Orchestrator` — once in the constructor for the personal mind, and again in `setWorkspace` for the workspace mind.
|
||
|
||
---
|
||
|
||
## 10. Embeddings
|
||
|
||
**Files:** `packages/hive-mind-core/src/mind/embedding-provider.ts`, `embeddings.ts`, `api-embedder.ts`, `inprocess-embedder.ts`, `litellm-embedder.ts`, `ollama-embedder.ts`.
|
||
|
||
### 10.1 The fallback chain
|
||
|
||
`createEmbeddingProvider({ provider: 'auto' })` probes in order:
|
||
|
||
```
|
||
inprocess → ollama → voyage → openai → mock
|
||
```
|
||
|
||
Each probe embeds the literal string `'waggle embedding probe'`, checks the dim count matches the target (1024), and either returns an active embedder or logs and moves on. The first one that works wins. If all fail, you get `createMockEmbedder(1024)` — a deterministic hash-to-floats stub that keeps the app running with degraded semantic search quality.
|
||
|
||
**Mock mode is not silent.** `getStatus().activeProvider === 'mock'` with `lastError` populated. If you're debugging weird search results, check that first.
|
||
|
||
### 10.2 Tier enforcement
|
||
|
||
`TIER_CAPABILITIES[tier].embeddingProviders` lists what a tier can use. Free gets inprocess+ollama (local only). Pro gets voyage+openai. Enterprise gets litellm. An explicitly-requested provider outside the tier throws `TierError`. In auto mode, disallowed providers are skipped.
|
||
|
||
### 10.3 Quota
|
||
|
||
Monthly embed count tracked in the `embedding_usage` table (`user_id, year_month, count`). `checkQuota(n)` before each call; `recordUsage(n)` after. Exceeding throws `EmbeddingQuotaExceededError` with tier + quota + current. 80% of quota logs a warn. Quota is only enforced if `quotaDb` is wired and `userTier` is explicitly set — local dev doesn't hit it.
|
||
|
||
### 10.4 Dimensionality
|
||
|
||
Hardcoded 1024 in `schema.ts` (`memory_frames_vec USING vec0(embedding float[1024])`). Every embedder normalizes to 1024 via truncation or zero-padding. If an embedder can't hit 1024, the probe fails and we move on.
|
||
|
||
---
|
||
|
||
## 11. Additional tables in the same DB (not "memory" but worth naming)
|
||
|
||
These share the `MindDB` but are not part of the five-layer stack:
|
||
|
||
- **`sessions`** — GOP registry. Every frame's `gop_id` must FK here. Sessions have `project_id`, `status`, `summary`.
|
||
- **`procedures`** — GEPA-optimized prompt templates with version, success_rate, avg_cost.
|
||
- **`improvement_signals`** — `(category, pattern_key)` unique; counts recurring corrections / capability gaps / workflow patterns. `surfaced` flag prevents re-nagging.
|
||
- **`install_audit`** — every capability install (native/skill/plugin/mcp) gets a row. risk + trust_source + approval_class + action.
|
||
- **`ai_interactions`** — EU AI Act Art. 12 event log. Every model call gets a row. Don't disable.
|
||
- **`execution_traces`** — per-run trace_json + outcome. Foundation for self-evolution.
|
||
- **`evolution_runs`** — the other half: proposed/accepted/rejected prompt/schema evolution runs with baseline vs. winner and gate verdict.
|
||
- **`harvest_sources`** — Memory Harvest sync state per source.
|
||
|
||
---
|
||
|
||
## 12. Conventions to follow when you extend this
|
||
|
||
1. **Hand-write SQL, use prepared statements, put the prepared statement in a method.** No ORM creep. Every `prepare()` call in `frames.ts`/`search.ts` can be traced back to one method.
|
||
2. **New tables go in `schema.ts`.** Add the `CREATE TABLE IF NOT EXISTS` there. If the schema shape changes, bump `SCHEMA_VERSION` and write a migration branch in `MindDB.runMigrations()`.
|
||
3. **New search sources feed RRF, not replace it.** If you add a third index path (say, a graph-walk index), fuse its results into the same RRF loop — don't build a parallel `search2()`.
|
||
4. **Importance + source are cheap. Use them.** Every frame writer should set `importance` honestly — `temporary` gets compacted in 30 days, `deprecated` in 90. `source` is enforced at insert time and feeds the trust model.
|
||
5. **Don't bypass `HybridSearch.indexFrame`.** Writing to `memory_frames` directly leaves frames invisible to search. There's no "rebuild index" script — it'd be expensive and slow, so don't rely on one.
|
||
6. **Every layer's `toContext()` goes into the system prompt.** Keep that output terse. Token budget for identity is <500 tokens; awareness is 10 items max. Don't inflate these.
|
||
7. **Schema version is a single string in `meta`.** If you break backwards compatibility, change both the version string and the migration path. Don't let the two drift.
|
||
|
||
---
|
||
|
||
## 13. Where to read next
|
||
|
||
- `packages/hive-mind-core/src/mind/reconcile.ts` — how conflicting frames get reconciled (not covered here).
|
||
- `packages/hive-mind-core/src/mind/ontology.ts` — the entity/relation type registry.
|
||
- `packages/hive-mind-core/src/mind/entity-normalizer.ts` — how raw strings become KG entities.
|
||
- `packages/hive-mind-core/src/mind/sessions.ts` — GOP lifecycle (open, close, summarize).
|
||
- `packages/agent/src/orchestrator.ts` lines 273–400 — the recall path end-to-end.
|
||
- `packages/core/src/harvest/pipeline.ts` — how Memory Harvest turns external sources into frames.
|
||
|
||
---
|
||
|
||
## 14. Changelog for this doc
|
||
|
||
| Date | Change | Owner |
|
||
|---|---|---|
|
||
| 2026-04-15 | Initial grounded deep-dive. Notes CLAUDE.md §1.3 path discrepancy (memory files live in `mind/`, not at top level of `packages/core/src/`). | Claude + Marko |
|