agent-working-memory 0.8.8 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,59 +84,134 @@ Most "memory for AI" projects are vector databases with a retrieval wrapper. AWM
84
84
 
85
85
  The design is based on cognitive science — ACT-R activation decay, Hebbian learning, complementary learning systems, synaptic homeostasis, and synaptic tagging — rather than ad-hoc heuristics. See [How It Works](#how-it-works) and [docs/cognitive-model.md](docs/cognitive-model.md) for details.
86
86
 
87
+ > **New to AWM?** [`docs/pipeline-walkthrough.html`](docs/pipeline-walkthrough.html) is a visual, plain-language walkthrough (no background required) — what happens when AWM learns and recalls a fact, why it's built this way, and how it differs from a plain vector store. Open it in a browser.
88
+
89
+ > **Build an agent on it:** the [AWM-Native Agent Harness pattern](docs/patterns/awm-native-harness.md) shows how to use AWM as an always-on cognitive *substrate* (not a tool the model calls) so the agent learns automatically by working — letting a cheap model perform at a high level and get cheaper + better over time. Measured: gpt-5.4-mini + AWM beat a frontier model on a domain workload at ~1/40th the cost.
90
+
91
+ > **For builders & researchers:** [`docs/awm-for-agents.html`](docs/awm-for-agents.html) is the agent playbook — why AWM exists (the context-window wall), the PRIME→ACT→VERIFY→LEARN harness, the full agent feature surface (workspace, session IDs, bearer-token hooks, supersede/feedback), how multi-hop is solved in the harness, and the honest gauntlet findings (where AWM wins, ties, and what isn't measured yet). Open it in a browser.
92
+
87
93
  ---
88
94
 
89
- ## Benchmarks
95
+ ## Why it matters at scale
90
96
 
91
- ### Eval Harness
97
+ The reason AWM exists: **past roughly half a million tokens, you can no longer keep a large project's context alive by carrying it.** The codebase, the docs, the decision history, and the meeting/work transcripts outgrow every model's window — and summarizing to fit silently drops the fact you needed next.
92
98
 
93
- | Suite | Score (v0.8.5) | Score (v0.6.0 baseline) | Threshold | What it tests |
94
- |-------|---|---|-----------|---------------|
95
- | Retrieval | **Recall@5 = 0.980** | 0.800 | >= 0.80 | 200 facts, 50 queries — BM25 + vector + reranker pipeline precision |
96
- | Associative | **success@10 = 1.000** | 1.000 | >= 0.70 | 20 multi-hop causal chains — graph walk finds non-obvious connections |
97
- | Redundancy | **dedup F1 = 0.966** | 0.966 | >= 0.80 | 50 clusters × 4 paraphrases — consolidation removes duplicates correctly |
98
- | Temporal | **Spearman = 0.932** | 0.932 | >= 0.75 | 25 facts with controlled age/access — ACT-R decay ranking accuracy |
99
+ These figures come from real-world use on a large software platform project, where a single work agent has accumulated **20,000+ memories** over a multi-million-token codebase and documentation set:
99
100
 
100
- > **v0.8.5 recall fix:** A regression intermediate-step had Recall@5 drop to
101
- > 0.46. Root cause: the entity-bridge boost (Phase 3.7) inverted top-1 in
102
- > dense same-concept corpora it rewarded clones that shared tags with
103
- > the anchor while excluding the anchor itself. Fix: proportional gating
104
- > so the boost scales with the textMatch gap between candidate and anchor.
105
- > Clones near the anchor get near-zero boost; genuine lateral candidates
106
- > (low textMatch, shared entities) still get the full boost.
107
- > Result: Recall@5 0.46 → 0.980 with all 4 eval suites green.
101
+ | To answer one question, carry… | tokens | AWM scoped recall |
102
+ |---|---|---|
103
+ | the accumulated memory (~20K memories) | ~1.3M | **~630, flat** |
104
+ | the project's notes & transcript docs | ~2M | **~630, flat** |
105
+ | the whole system (code + docs) | ~29M fits in no window, any tier | **~630, flat** |
108
106
 
109
- ### Full Test Suite
107
+ A scoped recall answers from the relevant *slice*, independent of how large the store grows. Measured consequences on real questions against the real project:
108
+
109
+ - **~2,000× fewer tokens per query** than carrying the memory store — and **~5× fewer** than opening the single best-matching documentation file (a floor; agents usually open several and still miss cross-file facts).
110
+ - **At scale, "carry everything" isn't an option.** At ~20K memories no context window holds it, so retrieval is not an optimization — it is the only door. A static notes file or long-context approach is forced to truncate, which silently drops facts.
111
+
112
+ Two structural advantages a file or a flat vector store cannot match:
110
113
 
111
- | Command | Score | What it tests |
112
- |---------|-------|---------------|
113
- | `npm run eval` | **4/4 suites pass** | Retrieval, associative, redundancy, temporal benchmarks with ablation support |
114
- | `npm run test:run` | **77/77 tests** | Unit tests: salience, decay, hebbian, supersession, coordination |
115
- | `npm run test:mcp` | **5/5 pass** | MCP protocol: write, recall, feedback, retract, stats |
116
- | `npm run test:self` | **94.1% EXCELLENT** | Pipeline component checks across all cognitive subsystems |
117
- | `npm run test:edge` | **All pass** | 9 failure modes: narcissistic interference, identity collision, contradiction trapping, bridge overshoot, noise forgetting |
118
- | `npm run test:stress` | **96.2% (50/52)** | 500 memories, 100 sleep cycles, catastrophic forgetting, adversarial spam, recovery |
119
- | `npm run test:workday` | **93.3% EXCELLENT** | 43 memories across 4 projects, cross-cutting queries, noise filtering |
120
- | `npm run test:ab` | **AWM 20/22 vs Baseline 18/22** | AWM outperforms keyword baseline on architecture + testing topics |
121
- | `npm run test:sleep` | **71.4%** | 60 memories, 4 topic clusters, consolidation impact across 3 cycles |
122
- | `npm run test:tokens` | **56.3% savings, 2.3x efficiency** | Memory-guided context vs full history, keyword accuracy 72.5% |
123
- | `scripts/measure-claude-vs-awm.ts` | **9.8× lower aggregate cost vs file_retrieval** | Real Claude Code session audit: AWM recall vs Read/Grep/Glob workflows |
124
- | `npm run test:pilot` | **14/15 pass** | Production-like queries with noise rejection (5/5 noise rejected) |
125
- | `npm run test:locomo` | **28.2%** | Industry-standard LoCoMo conversational memory benchmark (1,986 QA pairs) |
126
-
127
- ### Consolidation Health (v0.6.0)
128
-
129
- | Metric | Value |
130
- |--------|-------|
131
- | Topic clusters formed | **10** per consolidation cycle |
132
- | Cross-topic bridges | **20** in first cycle |
133
- | Edges strengthened | **135** per cycle (access-weighted) |
134
- | Graph size at scale | **3,000-4,500 edges** (500 memories) |
135
- | Recall after 100 cycles | **90%** stable |
136
- | Catastrophic forgetting survival | **5/5** (100%) |
137
- | Post-dedup retrieval | **0.950** (consolidation improves recall) |
138
-
139
- All evals are reproducible. See [Testing & Evaluation](#testing--evaluation).
114
+ - **Staleness is tracked.** When a fact changes, `memory_supersede` retires the old value and recall stops returning it the system *knows* what changed. A notes file or repo goes stale silently; you would re-scan everything to find out. (This work agent has superseded and retracted dozens of facts as the project moved.)
115
+ - **Dead weight costs nothing.** ~90% of accumulated memories are never recalled for a given task — a notes file pays for all of them in every prompt; recall pays for ≈zero.
116
+
117
+ ### Honest about the trade-offs
118
+
119
+ - AWM does **not** win on small, one-shot tasks write/recall overhead exceeds the savings until knowledge is reused or the corpus grows past what fits in context.
120
+ - Recall is not free: a few seconds of latency per query buys the token reduction.
121
+ - Recall accuracy is bounded by what was written — write quality matters (lead with the fact; tag with identifiers like file, table, ticket).
122
+ - It does not replace your source of truth. The intended pattern is: **recall first, read/grep the code for ground truth on a miss, and supersede when reality differs.**
123
+
124
+ ---
125
+
126
+ ## Benchmarks
127
+
128
+ Two kinds of tests, both reproducible (see [Testing & Evaluation](#testing--evaluation)).
129
+ First, **recall quality** — does the pipeline return the right memory? Second,
130
+ **behavior under stress** — does it stay honest, filter noise, and hold up as the
131
+ store grows and ages? Numbers below were re-run on the 0.9-staged line (2026-06-17).
132
+
133
+ ### 1 · Recall quality (eval harness)
134
+
135
+ Each suite has a pass threshold; all four pass.
136
+
137
+ | Suite | Score | Threshold | What it measures |
138
+ |-------|-------|-----------|------------------|
139
+ | Retrieval | **Recall@5 = 0.980** | ≥ 0.80 | 200 facts, 50 queries — does the BM25 + vector + reranker pipeline surface the right fact in the top 5? |
140
+ | Associative | **success@10 = 1.000** | 0.70 | 20 multi-hop causal chains — does the graph walk find non-obvious connections? |
141
+ | Redundancy | **dedup F1 = 0.966** | ≥ 0.80 | 50 clusters × 4 paraphrases — does consolidation merge duplicates without losing the original? |
142
+ | Temporal | **Spearman = 0.932** | 0.75 | 25 facts with controlled age/access — does ACT-R decay rank recent/used memories ahead of stale ones? |
143
+
144
+ ### 2 · Behavior under stress & adversarial conditions
145
+
146
+ These are graded suites (not pass/fail). The headline risk they guard against is a
147
+ memory system that confidently returns the *wrong* thing — so the weakest area is
148
+ called out, not hidden.
149
+
150
+ | Suite | Score | What it measures |
151
+ |-------|-------|------------------|
152
+ | `test:run` (unit) | **569 / 569** | Salience, decay, Hebbian, supersession, coordination, scheduler |
153
+ | `test:self` | **93.9% (EXCELLENT)** | Every cognitive subsystem end-to-end; weakest = exact-topic retrieval |
154
+ | `test:workday` | **85.4% (GOOD)** | A realistic mixed day — 43 memories across 4 projects, cross-cutting queries; weakest = noise filtering |
155
+ | `test:edge` | **~32 / 34** | Named failure modes: identity collision, contradiction trapping, bridge overshoot, false generalization |
156
+ | `test:ab` | **AWM 10 / 11 vs keyword baseline 8 / 11** | Where the cognitive pipeline beats plain keyword search |
157
+ | `test:pilot` | **14 / 15** (5/5 noise rejected) | Production-like queries that must reject planted distractors |
158
+ | `test:locomo` | **25.7%** | LoCoMo conversational-memory benchmark (a *chatbot* benchmark — see note) |
159
+ | `test:mcp` | **5 / 5** | MCP protocol smoke: write, recall, feedback, retract, stats |
160
+
161
+ > **On LoCoMo (25.7%):** LoCoMo measures *chatbot* recall ("what did we say about X"
162
+ > across long conversations). It is not the workload AWM is tuned for (productivity /
163
+ > engineering, staying on topic, rejecting noise), and ~66% of AWM's misses there are
164
+ > retriever-coverage (the gold turn isn't in the top-10), not extraction. The 0.9 recall
165
+ > work lifted it from 22.7% with every category up. We report it for comparability, not
166
+ > as the headline.
167
+
168
+ ### 3 · The sleep cycle (consolidation)
169
+
170
+ The **sleep cycle** is AWM's offline maintenance pass (the term is borrowed from how
171
+ human memory consolidates during sleep). On each cycle it **clusters** related
172
+ memories, builds **cross-topic bridges**, **strengthens** co-used edges, **decays**
173
+ unused ones, and **prunes** duplicates. You run it so the association graph stays
174
+ *healthy and navigable* as the store grows — without it, edges accumulate into noise.
175
+
176
+ > **Reading the score:** `test:sleep` = **78.6%** is a *consolidation-quality* score —
177
+ > it asks "after the maintenance pass, is recall at least as good and is the structure
178
+ > better?" **It is not recall falling to 78.6%.** In this fixture recall is held flat
179
+ > across three cycles (78.6% before = 78.6% after) while the graph reorganizes. The
180
+ > scaling picture is the real proof:
181
+
182
+ | Under a 100-cycle stress run | Observed |
183
+ |---|---|
184
+ | Recall across cycles | **holds 90–100%** (no catastrophic forgetting) |
185
+ | Cross-topic recall | **~80%**, stable |
186
+ | Graph self-pruning | edges grow to ~2,300 then prune back to ~1,500 as unused links decay |
187
+ | Clusters / bridges per cycle | ~10 clusters, bridges formed early then settle |
188
+
189
+ So consolidation *protects* recall over the long run — the per-cycle score measures the
190
+ health of the maintenance, and the stress run shows recall doesn't degrade.
191
+
192
+ ### 4 · Token economics — honest
193
+
194
+ The win that matters is **structural**: at the scale AWM targets you can't carry the
195
+ project at all (see [Why it matters at scale](#why-it-matters-at-scale)). On real coding
196
+ sessions, scoped recall costs **9.8× less in aggregate** than the Read/Grep/Glob
197
+ rediscovery it replaces (`scripts/measure-claude-vs-awm.ts`).
198
+
199
+ The per-turn micro-benchmark (`test:tokens`) reports against **two** baselines, because the
200
+ baseline you pick *is* the result:
201
+
202
+ - **vs carrying the full history** (what a memoryless agent must actually do — it can't know
203
+ which past turn matters): **+67% savings at 97.5% recall accuracy.** This is the honest,
204
+ apples-to-apples number.
205
+ - **vs an oracle that pre-scoped context to the exactly-relevant task**: **≈ −13%.** A
206
+ deliberately brutal bar — it gives the baseline the very scoping that retrieval exists to do —
207
+ and on a tiny 6–8-turn task a fixed top-5 recall is break-even-to-negative *by construction*.
208
+
209
+ An earlier build reported ~56% on the oracle bar, but that was an **artifact**: pre-v0.8.5,
210
+ reinforce-on-duplicate silently *discarded* memory content, so recalls were artificially tiny.
211
+ v0.8.5 fixed the data loss (accuracy ~72% → 97.5%); better recall now fills all five slots,
212
+ which *lowers* the oracle-bar number while *raising* correctness. Net: the at-scale structural
213
+ win above is the real story; the oracle bar shows AWM roughly matches perfect manual scoping
214
+ even on a corpus far too small to play to its strengths.
140
215
 
141
216
  ---
142
217
 
@@ -475,6 +550,49 @@ npm run test:locomo # LoCoMo industry benchmark (28.2%)
475
550
 
476
551
  All three ML models run locally via ONNX. No external API calls for retrieval. The entire system is a single SQLite file + a Node.js process.
477
552
 
553
+ ## What's New in v0.9.0
554
+
555
+ A recall-quality default + new tuning knobs + a builder/researcher doc set.
556
+ Every change is an **env-revertible default** with no API changes — existing
557
+ callers keep working unmodified. Validated: official LoCoMo **22.7% → 25.7%**
558
+ (every category up) **and** adversarial precision **73.4 → 74.9** (strictly
559
+ better on each axis); recall latency ~35 → ~77ms (sub-100ms, tunable); zero
560
+ regression across the standard suite (eval 4-suite identical, 569/569 unit,
561
+ edge 32/34, workday = old config).
562
+
563
+ - **Wide rerank pool + top-K abstention (the win).** A pipeline-attribution
564
+ study (new tracer, `tests/locomo-eval/trace.ts`) found the dominant recall
565
+ loss wasn't candidate generation *or* the reranker — it was the stage between:
566
+ ~50% of answerable queries had gold that *cleared the candidate floor* but was
567
+ squeezed out of the rerank pool by the decay-compressed composite **before the
568
+ high-lift (+3.29) reranker saw it**. Fix: the composite becomes a cheap **wide
569
+ pre-filter** (`AWM_TOPN_MULT=8`, was 3×), the reranker discriminates on a wider
570
+ pool (`AWM_RERANK_POOL=max(limit*4,40)`, was `max(limit*2,15)`), and the
571
+ out-of-domain abstention gate judges only the **post-rerank top-K**
572
+ (`AWM_ABSTAIN_GATE_K=5`) so widening for recall doesn't inflate the in-domain
573
+ signal. Reverses the v0.7.13 "pool reduction" change. See
574
+ [reference.md → Recall tuning](docs/reference.md#recall-tuning-env-overrides).
575
+
576
+ - **Tunable similarity floors.** `AWM_SIM_FLOOR_TARGETED` / `_EXPLORATORY`
577
+ (defaults 0.50 / 0.35, unchanged) and the candidate-entry floors are now env
578
+ overrides for retuning against a different embedder.
579
+
580
+ - **Opt-in / experimental flags (default-off).** `AWM_QUERY_BRIDGE`
581
+ (query-named-entity boost — lifts attribution "what does X think" 36% → 92% on
582
+ a controlled eval; small adversarial cost, so opt-in), `AWM_AUTOTAG`
583
+ (write-time `entity:`/`cat:` meta-tags), `AWM_BROAD_EDGES`. `AWM_SPREAD`
584
+ (in-engine spreading activation) is **parked** — it regressed recall by
585
+ displacing gold; multi-hop is solved harness-side instead (see the playbook).
586
+
587
+ - **New docs for builders & researchers.** [`docs/awm-for-agents.html`](docs/awm-for-agents.html)
588
+ — the agent playbook (why AWM exists, the PRIME→ACT→VERIFY→LEARN harness, the
589
+ full agent feature surface, how multi-hop is solved, and the honest gauntlet
590
+ findings). [`docs/pipeline-walkthrough.html`](docs/pipeline-walkthrough.html)
591
+ redesigned for devs/researchers. Both are published on GitHub Pages. A new
592
+ **Storage Backends + Postgres roadmap** section in
593
+ [architecture.md](docs/architecture.md) documents SQLite (default) vs PGlite
594
+ and the path to a networked-Postgres backend (v1 target).
595
+
478
596
  ## What's New in v0.8.5
479
597
 
480
598
  A research-grounded hardening pass on recall quality, retraction propagation,
@@ -582,6 +700,7 @@ callers keep working without modification. Full validation at the milestone:
582
700
  ## What's New in v0.7.13
583
701
 
584
702
  - **Reranker pool size reduction** — cross-encoder pool dropped from `max(limit*3, 30)` to `max(limit*2, 15)`. For typical agent queries (limit=5 or 10), that's 15-20 candidates reranked instead of 30, halving the cross-encoder cost. Top-K quality preserved (8/8 top-1, identical top-5/top-10 overlap) — reranking the 21st-30th candidates was wasted when the user only wants top-5 anyway.
703
+ > **⚠️ Superseded in 0.9.0 — this reduction was reversed.** A pipeline-attribution study found that "wasted" tail was actually where ~50% of retrievable answers were being squeezed out *before* the reranker saw them (the small 8-query A/B above missed it). 0.9.0 widens the pool back to `max(limit*4, 40)` and adds a top-K abstention gate — lifting LoCoMo recall 22.7%→25.7% **and** adversarial precision 73.4→74.9, with no regression. See the CHANGELOG and `docs/reference.md` → "Recall tuning."
585
704
 
586
705
  ## What's New in v0.7.12
587
706
 
@@ -655,9 +655,9 @@ export function registerRoutes(app, deps) {
655
655
  return reply.code(501).send({ error: 'export endpoint requires the SQLite backend' });
656
656
  }
657
657
  const db = store.getDb();
658
- let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
659
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
660
- retracted, retracted_by, retracted_at, tags
658
+ let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
659
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
660
+ retracted, retracted_by, retracted_at, tags
661
661
  FROM engrams`;
662
662
  const conditions = [];
663
663
  const params = [];
@@ -675,7 +675,7 @@ export function registerRoutes(app, deps) {
675
675
  engramSql += ' ORDER BY created_at ASC';
676
676
  const engrams = db.prepare(engramSql).all(...params);
677
677
  const engramIds = new Set(engrams.map(e => e.id));
678
- const allAssocs = db.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
678
+ const allAssocs = db.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
679
679
  FROM associations`).all();
680
680
  const associations = allAssocs.filter(a => engramIds.has(a.from_engram_id) && engramIds.has(a.to_engram_id));
681
681
  return reply.send({
@@ -700,9 +700,9 @@ export function registerRoutes(app, deps) {
700
700
  if (coordEnabled && typeof deps.store.getDb === 'function') {
701
701
  try {
702
702
  const db = deps.store.getDb();
703
- const stats = db.prepare(`SELECT
704
- (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
705
- (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
703
+ const stats = db.prepare(`SELECT
704
+ (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
705
+ (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
706
706
  (SELECT COUNT(*) FROM coord_locks) AS active_locks`).get();
707
707
  Object.assign(base, stats);
708
708
  }
package/dist/cli.js CHANGED
@@ -38,46 +38,46 @@ catch { /* No .env file */ }
38
38
  const args = process.argv.slice(2);
39
39
  const command = args[0];
40
40
  function printUsage() {
41
- console.log(`
42
- AgentWorkingMemory — Cognitive memory for AI agents
43
-
44
- Usage:
45
- awm setup [target] [options] Configure AWM for an AI CLI
46
- awm doctor [target|--all] Validate AWM integrations
47
- awm mcp Start MCP server (stdio)
48
- awm serve [--port <port>] Start HTTP API server
49
- awm health [--port <port>] Check server health
50
- awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
51
- Export memories to JSON
52
- awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
53
- Import memories from JSON
54
- awm merge --target <db> --source <db> [--source ...]
55
- [--remap uuid=name] [--remap-all-uuids <name>]
56
- [--dedupe] [--dry-run] Merge multiple memory DBs
57
- awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]
58
- Migrate SQLite DB to PGlite
59
-
60
- Setup targets:
61
- claude-code (default) .mcp.json + CLAUDE.md + hooks
62
- codex ~/.codex/config.toml + AGENTS.md
63
- cursor .cursor/mcp.json + .cursorrules
64
- http Connection info for HTTP API
65
-
66
- Setup options:
67
- --global Use global scope (recommended for claude-code)
68
- --agent-id <id> Agent identifier (default: project name)
69
- --db-path <path> Database path (default: <awm>/data/memory.db)
70
- --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
71
- --no-claude-md Alias for --no-instructions
72
- --no-hooks Skip hook installation
73
- --hook-port PORT Sidecar port for hooks (default: 8401)
74
-
75
- Examples:
76
- awm setup --global Claude Code, global (recommended)
77
- awm setup codex Codex CLI
78
- awm setup cursor Cursor IDE
79
- awm setup http Generic HTTP integration
80
- awm doctor --all Check all configured targets
41
+ console.log(`
42
+ AgentWorkingMemory — Cognitive memory for AI agents
43
+
44
+ Usage:
45
+ awm setup [target] [options] Configure AWM for an AI CLI
46
+ awm doctor [target|--all] Validate AWM integrations
47
+ awm mcp Start MCP server (stdio)
48
+ awm serve [--port <port>] Start HTTP API server
49
+ awm health [--port <port>] Check server health
50
+ awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
51
+ Export memories to JSON
52
+ awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
53
+ Import memories from JSON
54
+ awm merge --target <db> --source <db> [--source ...]
55
+ [--remap uuid=name] [--remap-all-uuids <name>]
56
+ [--dedupe] [--dry-run] Merge multiple memory DBs
57
+ awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]
58
+ Migrate SQLite DB to PGlite
59
+
60
+ Setup targets:
61
+ claude-code (default) .mcp.json + CLAUDE.md + hooks
62
+ codex ~/.codex/config.toml + AGENTS.md
63
+ cursor .cursor/mcp.json + .cursorrules
64
+ http Connection info for HTTP API
65
+
66
+ Setup options:
67
+ --global Use global scope (recommended for claude-code)
68
+ --agent-id <id> Agent identifier (default: project name)
69
+ --db-path <path> Database path (default: <awm>/data/memory.db)
70
+ --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
71
+ --no-claude-md Alias for --no-instructions
72
+ --no-hooks Skip hook installation
73
+ --hook-port PORT Sidecar port for hooks (default: 8401)
74
+
75
+ Examples:
76
+ awm setup --global Claude Code, global (recommended)
77
+ awm setup codex Codex CLI
78
+ awm setup cursor Cursor IDE
79
+ awm setup http Generic HTTP integration
80
+ awm doctor --all Check all configured targets
81
81
  `.trim());
82
82
  }
83
83
  // ─── SETUP ──────────────────────────────────────
@@ -135,18 +135,18 @@ async function setup() {
135
135
  const configAction = adapter.writeMcpConfig(ctx);
136
136
  const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
137
137
  const hooksAction = adapter.writeHooks(ctx, skipHooks);
138
- console.log(`
139
- AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
140
-
141
- Agent ID: ${ctx.agentId}
142
- DB path: ${ctx.dbPath}
143
- ${configAction}
144
- ${instructionsAction}
145
- ${hooksAction}
146
-
147
- Next steps:
148
- 1. Restart ${adapter.name} to pick up the MCP server
149
- 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
138
+ console.log(`
139
+ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
140
+
141
+ Agent ID: ${ctx.agentId}
142
+ DB path: ${ctx.dbPath}
143
+ ${configAction}
144
+ ${instructionsAction}
145
+ ${hooksAction}
146
+
147
+ Next steps:
148
+ 1. Restart ${adapter.name} to pick up the MCP server
149
+ 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
150
150
  `.trim());
151
151
  }
152
152
  // ─── DOCTOR ──────────────────────────────────────
@@ -374,23 +374,23 @@ async function importMemories() {
374
374
  const Database = (await import('better-sqlite3')).default;
375
375
  const db = new Database(dbPath);
376
376
  // Ensure tables exist in target
377
- db.exec(`
378
- CREATE TABLE IF NOT EXISTS engrams (
379
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
380
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
381
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
382
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
383
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
384
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
385
- episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
386
- memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
387
- );
388
- CREATE TABLE IF NOT EXISTS associations (
389
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
390
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
391
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
392
- created_at TEXT NOT NULL, last_activated TEXT
393
- );
377
+ db.exec(`
378
+ CREATE TABLE IF NOT EXISTS engrams (
379
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
380
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
381
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
382
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
383
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
384
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
385
+ episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
386
+ memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
387
+ );
388
+ CREATE TABLE IF NOT EXISTS associations (
389
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
390
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
391
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
392
+ created_at TEXT NOT NULL, last_activated TEXT
393
+ );
394
394
  `);
395
395
  // Build dedup set if needed
396
396
  const existingHashes = new Set();
@@ -405,15 +405,19 @@ async function importMemories() {
405
405
  let imported = 0;
406
406
  let skippedDupes = 0;
407
407
  let skippedRetracted = 0;
408
- const insertMem = db.prepare(`
409
- INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
410
- access_count, last_accessed, created_at, stage, tags, memory_class,
411
- episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
412
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
408
+ const insertMem = db.prepare(`
409
+ INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
410
+ access_count, last_accessed, created_at, stage, tags, memory_class,
411
+ episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
412
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
413
413
  `);
414
- const insertAssoc = db.prepare(`
415
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
416
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
414
+ // NOTE: associations.last_activated is NOT NULL on the engrams DB (storage/sqlite.ts),
415
+ // so importing into an existing store fails if it's omitted — and because import wraps
416
+ // memories+associations in ONE transaction, that rolls back the memories too (silent
417
+ // "empty store"). Set it alongside created_at. (migrate/merge paths already do this.)
418
+ const insertAssoc = db.prepare(`
419
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at, last_activated)
420
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
417
421
  `);
418
422
  const importTx = db.transaction(() => {
419
423
  // Import memories
@@ -516,21 +520,21 @@ async function mergeMemories() {
516
520
  targetDb.pragma('journal_mode = WAL');
517
521
  targetDb.pragma('foreign_keys = ON');
518
522
  // Ensure tables exist in target
519
- targetDb.exec(`
520
- CREATE TABLE IF NOT EXISTS engrams (
521
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
522
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
523
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
524
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
525
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
526
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
527
- );
528
- CREATE TABLE IF NOT EXISTS associations (
529
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
530
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
531
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
532
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
533
- );
523
+ targetDb.exec(`
524
+ CREATE TABLE IF NOT EXISTS engrams (
525
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
526
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
527
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
528
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
529
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
530
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
531
+ );
532
+ CREATE TABLE IF NOT EXISTS associations (
533
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
534
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
535
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
536
+ created_at TEXT NOT NULL, last_activated TEXT NOT NULL
537
+ );
534
538
  `);
535
539
  // Build dedupe hash set from existing target memories
536
540
  const existingHashes = new Set();
@@ -540,16 +544,16 @@ async function mergeMemories() {
540
544
  existingHashes.add(contentHash(row.concept, row.content));
541
545
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
542
546
  }
543
- const insertEngram = targetDb.prepare(`
544
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
545
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
546
- retracted, retracted_by, retracted_at, tags)
547
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
547
+ const insertEngram = targetDb.prepare(`
548
+ INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
549
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
550
+ retracted, retracted_by, retracted_at, tags)
551
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
548
552
  `);
549
- const insertAssoc = targetDb.prepare(`
550
- INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
551
- activation_count, created_at, last_activated)
552
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
553
+ const insertAssoc = targetDb.prepare(`
554
+ INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
555
+ activation_count, created_at, last_activated)
556
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
553
557
  `);
554
558
  let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
555
559
  for (const sourcePath of sources) {
@@ -558,10 +562,10 @@ async function mergeMemories() {
558
562
  continue;
559
563
  }
560
564
  const sourceDb = new Database(sourcePath, { readonly: true });
561
- const engrams = sourceDb.prepare(`SELECT id, agent_id, concept, content, confidence, salience, access_count,
562
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
565
+ const engrams = sourceDb.prepare(`SELECT id, agent_id, concept, content, confidence, salience, access_count,
566
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
563
567
  retracted, retracted_by, retracted_at, tags FROM engrams`).all();
564
- const assocs = sourceDb.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
568
+ const assocs = sourceDb.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
565
569
  activation_count, created_at, last_activated FROM associations`).all();
566
570
  const idMap = new Map();
567
571
  const skippedIds = new Set();