linksee-memory 0.7.1 → 0.8.0
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 +193 -34
- package/dist/bin/declare-anchor.d.ts +2 -0
- package/dist/bin/declare-anchor.js +146 -0
- package/dist/bin/detect-drift.d.ts +2 -0
- package/dist/bin/detect-drift.js +91 -0
- package/dist/db/migrate.js +52 -28
- package/dist/db/schema.sql +348 -232
- package/dist/lib/consolidate.d.ts +2 -0
- package/dist/lib/consolidate.js +8 -0
- package/dist/lib/drift-anchors.d.ts +78 -0
- package/dist/lib/drift-anchors.js +224 -0
- package/dist/lib/drift-detection.d.ts +62 -0
- package/dist/lib/drift-detection.js +416 -0
- package/dist/lib/drift-view.d.ts +62 -0
- package/dist/lib/drift-view.js +120 -0
- package/dist/lib/edge-detection.d.ts +22 -0
- package/dist/lib/edge-detection.js +178 -0
- package/dist/lib/session-extractor.js +49 -0
- package/dist/lib/truth-engine.d.ts +84 -0
- package/dist/lib/truth-engine.js +417 -0
- package/dist/mcp/server.js +292 -57
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# linksee-memory
|
|
2
2
|
|
|
3
|
-
> **Your agent forgets everything when a session ends.
|
|
3
|
+
> **Your agent forgets everything when a session ends. Worse — it silently drifts from what you decided last week.**
|
|
4
4
|
>
|
|
5
|
-
> Local-first cross-LLM memory MCP — one SQLite file that **Claude Code, Cursor, Windsurf, OpenAI Codex, and Gemini CLI** all read from. Not just "what happened" but **WHY** it happened: 6-layer structured memory with precision recall that
|
|
5
|
+
> Local-first cross-LLM memory MCP — one SQLite file that **Claude Code, Cursor, Windsurf, OpenAI Codex, and Gemini CLI** all read from. Not just "what happened" but **WHY** it happened: 6-layer structured memory with precision recall, **plus drift detection that catches when reality diverges from your decisions**.
|
|
6
|
+
>
|
|
7
|
+
> Memory is the entry point. Drift detection is the real value.
|
|
6
8
|
>
|
|
7
9
|
> `npx linksee-memory-setup` — one command, done.
|
|
8
10
|
|
|
@@ -79,7 +81,7 @@ That single `caveat` memory is what separates "flat fact storage" from "the agen
|
|
|
79
81
|
Returns match_reasons explaining each hit
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
Every memory is tagged with **exactly one layer**. `caveat`-layer entries are protected from auto-forgetting. Cold low-importance memories
|
|
84
|
+
Every memory is tagged with **exactly one layer**. `caveat`-layer entries are protected from auto-forgetting. Cold low-importance memories are auto-consolidated into `learning` entries on server startup.
|
|
83
85
|
|
|
84
86
|
---
|
|
85
87
|
|
|
@@ -87,21 +89,69 @@ Every memory is tagged with **exactly one layer**. `caveat`-layer entries are pr
|
|
|
87
89
|
|
|
88
90
|
Most "agent memory" services (Mem0, Letta, Zep) save a flat list of facts. Then the agent looks at "edited file X 30 times" and has no idea why. **linksee-memory keeps the WHY.**
|
|
89
91
|
|
|
90
|
-
It is a Model Context Protocol (MCP) server that gives any AI agent
|
|
92
|
+
It is a Model Context Protocol (MCP) server with **7 tools** that gives any AI agent structured memory + drift detection:
|
|
91
93
|
|
|
92
94
|
| | Mem0 / Letta / Zep | Claude Code auto-memory | linksee-memory |
|
|
93
95
|
|---|---|---|---|
|
|
94
96
|
| Cross-agent | △ (cloud) | ❌ Claude only | ✅ single SQLite file |
|
|
95
97
|
| 6-layer WHY structure | ❌ flat | ❌ flat markdown | ✅ goal / context / emotion / impl / caveat / learning |
|
|
98
|
+
| **Drift detection** | ❌ | ❌ | ✅ intent ↔ reality divergence tracking |
|
|
96
99
|
| File diff cache | ❌ | ❌ | ✅ AST-aware, 50-99% token savings on re-reads |
|
|
97
100
|
| Active forgetting | △ | ❌ | ✅ Ebbinghaus curve, caveat layer protected |
|
|
98
101
|
| Local-first / private | ❌ | ✅ | ✅ |
|
|
99
102
|
|
|
100
|
-
##
|
|
103
|
+
## Four pillars
|
|
101
104
|
|
|
102
105
|
1. **Token savings** via `read_smart` — sha256 + AST/heading/indent chunking. Re-reads return only diffs. **Measured 86% saved on a typical TS file edit, 99% saved on unchanged re-reads.**
|
|
103
106
|
2. **Cross-agent portability** — single SQLite file at `~/.linksee-memory/memory.db`. Same brain for Claude Code, Cursor, Windsurf, OpenAI Codex, Gemini CLI.
|
|
104
107
|
3. **WHY-first structured memory** — six explicit layers (`goal` / `context` / `emotion` / `implementation` / `caveat` / `learning`). Solves "flat fact memory is useless without goals".
|
|
108
|
+
4. **Drift detection** — declare decisions as anchors, then the engine automatically detects when committed reality diverges from stated intent. Think "Datadog for product decisions" — unaccounted divergences surface as drift, intentional evolution (recorded as supersede/fix) stays quiet.
|
|
109
|
+
|
|
110
|
+
## 🔍 Drift Detection — "Intent Datadog"
|
|
111
|
+
|
|
112
|
+
Most teams make decisions, then forget them. The agent from last week decided "we'll use FTS5 instead of vector search" — but this week a new session installs `pgvector` without knowing why that was rejected. **That's drift.** Not a bug. Not malice. Just forgotten context.
|
|
113
|
+
|
|
114
|
+
Linksee Memory's drift detection catches this:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
You: What's drifting right now?
|
|
118
|
+
Agent: [calls drift_status]
|
|
119
|
+
|
|
120
|
+
28 anchors: ⚪ 1 held · 🔵 27 aligned
|
|
121
|
+
|
|
122
|
+
Needs attention:
|
|
123
|
+
⚪ HELD — "Focus on 4 areas: Recipe layer, agent-native API,
|
|
124
|
+
Japanese market, Agent Insights"
|
|
125
|
+
↻ Reopens 2026-07-04
|
|
126
|
+
|
|
127
|
+
Everything else is aligned — no unaccounted divergence.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### How it works
|
|
131
|
+
|
|
132
|
+
1. **Declare** decisions as anchors: `declare_anchor({ kind: "decision", statement: "We use FTS5, not vector search", violation_signal: ["pgvector", "embedding"] })`
|
|
133
|
+
2. **The engine detects** when committed code reality diverges from these anchors
|
|
134
|
+
3. **State derivation** classifies each anchor:
|
|
135
|
+
- 🔴 **Drift** — reality diverges with no recorded resolution
|
|
136
|
+
- 🟡 **Review** — a soft signal awaits your decision
|
|
137
|
+
- ⚪ **Held** — you acknowledged the gap, parked it with a review date
|
|
138
|
+
- 🔵 **Aligned** — reality matches intent, or a recorded resolution explains the change
|
|
139
|
+
4. **Resolve** with `fix`, `supersede`, `acknowledge`, or `dismiss`
|
|
140
|
+
|
|
141
|
+
The **make-or-break rule**: a divergence accounted for by a recorded resolution (supersede/fix/acknowledge) is NOT drift. Only unaccounted gaps are flagged. This means intentional evolution stays quiet while silent abandonment gets caught.
|
|
142
|
+
|
|
143
|
+
### 4-species taxonomy
|
|
144
|
+
|
|
145
|
+
Anchors are classified into four species with different display formats:
|
|
146
|
+
|
|
147
|
+
| Species | Icon | Display Format | Example |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| Hypothesis | 🧪 | Decision Card (journal format) | "We'll launch English-first on HN" |
|
|
150
|
+
| Constraint | 🔒 | Rule (pass/fail checklist) | "All writes go through remember()" |
|
|
151
|
+
| Commitment | 🔁 | Heartbeat (alive/dead) | "Ship a new version every week" |
|
|
152
|
+
| Source of Truth | 📍 | Reference (stable anchor) | "MCP server runs on stdio, single SQLite" |
|
|
153
|
+
|
|
154
|
+
---
|
|
105
155
|
|
|
106
156
|
## Quick Start — One Command
|
|
107
157
|
|
|
@@ -127,7 +177,7 @@ Restart Claude Code, then just chat normally. Add **"Use Linksee"** to any promp
|
|
|
127
177
|
claude mcp add -s user linksee -- npx -y linksee-memory
|
|
128
178
|
```
|
|
129
179
|
|
|
130
|
-
Tools appear as `mcp__linksee__remember`, `mcp__linksee__recall`, `
|
|
180
|
+
Tools appear as `mcp__linksee__remember`, `mcp__linksee__recall`, `mcp__linksee__read_smart`.
|
|
131
181
|
|
|
132
182
|
**Install the skill (auto-invocation):**
|
|
133
183
|
|
|
@@ -248,28 +298,60 @@ All editors share the same `~/.linksee-memory/memory.db`. A decision made in Cla
|
|
|
248
298
|
|
|
249
299
|
Default: `~/.linksee-memory/memory.db`. Override with `LINKSEE_MEMORY_DIR` env var.
|
|
250
300
|
|
|
251
|
-
## What's new in v0.
|
|
301
|
+
## What's new in v0.8
|
|
302
|
+
|
|
303
|
+
| Feature | Detail |
|
|
304
|
+
|---|---|
|
|
305
|
+
| **4 drift detection tools** | `drift_status`, `check_decision`, `declare_anchor`, `resolve_drift` — agents can now query and act on intent ↔ reality divergence. The biggest gap in agent memory (decisions are forgotten across sessions) is now closed. |
|
|
306
|
+
| **Truth engine** | State derivation logic (drift/review/held/aligned) now lives in the MCP engine, not just the dashboard. Any MCP client can query drift status. |
|
|
307
|
+
| **4-species taxonomy** | Anchors classified as hypothesis/constraint/commitment/source_of_truth with species-appropriate display formats. |
|
|
308
|
+
| **Resolution priority** | When multiple resolutions exist for an anchor, the most recent one wins (prevents stale acknowledge from shadowing a newer fix). |
|
|
309
|
+
|
|
310
|
+
<details>
|
|
311
|
+
<summary>What's new in v0.7</summary>
|
|
312
|
+
|
|
313
|
+
| Feature | Detail |
|
|
314
|
+
|---|---|
|
|
315
|
+
| **3-tool unified surface** | 8 tools → 3: `remember` (create + update + delete), `recall` (search + file history + overview), `read_smart` (token-saving reads). Fewer tools = better cross-LLM consistency. Follows Context7's proven pattern. |
|
|
316
|
+
| **Auto-consolidate** | Consolidation runs automatically on server startup (non-blocking, 7-day threshold). No manual `consolidate()` calls needed. |
|
|
317
|
+
| **Deprecation guidance** | Old tool names (`forget`, `recall_file`, etc.) return specific migration examples instead of silent failures. |
|
|
318
|
+
| **"Use Linksee Memory" trigger** | Add "Use Linksee Memory" to any prompt to force memory recall — same adoption pattern as Context7. |
|
|
319
|
+
| **Claude Code Plugin** | `claude plugin add -- linksee-memory` — ships MCP server + auto-invocation skill in one install. |
|
|
320
|
+
|
|
321
|
+
</details>
|
|
322
|
+
|
|
323
|
+
<details>
|
|
324
|
+
<summary>What's new in v0.4</summary>
|
|
252
325
|
|
|
253
326
|
| Feature | Detail |
|
|
254
327
|
|---|---|
|
|
255
328
|
| **One-command setup** | `npx linksee-memory-setup` — registers MCP server, installs skill, configures auto-capture hook. One command instead of three. |
|
|
256
329
|
| **Structured memory v2** | 3-axis classification (altitude × type × state) for every memory. Auto-extraction from sessions produces machine-scannable JSON, not raw chat dumps. |
|
|
257
330
|
| **Precision recall guide** | SKILL.md now teaches agents HOW to write effective queries, WHEN to recall vs skip, and WHEN to proactively surface caveats before risky actions. |
|
|
258
|
-
| **
|
|
259
|
-
|
|
331
|
+
| **Five MCP Blocks** | Tools + Resources + Prompts + Sampling + Roots + Elicitation. Most MCP servers expose only Tools; linksee-memory implements all five primitives. |
|
|
332
|
+
|
|
333
|
+
</details>
|
|
334
|
+
|
|
335
|
+
## 7 Tools (v0.8)
|
|
336
|
+
|
|
337
|
+
### Memory tools
|
|
338
|
+
|
|
339
|
+
| Tool | What it does |
|
|
340
|
+
|---|---|
|
|
341
|
+
| `remember` | **Save / update / delete** memories. Auto-classifies into 6 layers. Modes: create (default), update (`memory_id` + fields), delete (`forget: true` + `memory_id`). |
|
|
342
|
+
| `recall` | **Search / file history / overview.** Modes: search (`query`), file history (`path`), entity overview (no params). FTS5 + heat × momentum ranking with `match_reasons`. |
|
|
343
|
+
| `read_smart` | **Token-saving file reader** with AST diff caching. First read = full content. Re-read unchanged = ~50 tokens. Re-read modified = changed chunks only. |
|
|
260
344
|
|
|
261
|
-
|
|
345
|
+
### Drift tools (v0.8.0)
|
|
262
346
|
|
|
263
|
-
| Tool |
|
|
347
|
+
| Tool | What it does |
|
|
264
348
|
|---|---|
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| `forget` | Explicit delete OR auto-sweep based on `forgettingRisk`. Pinned (`importance>=1.0`) and caveat-layer memories are always preserved. **v0.3.0** `interactive` flag asks the user via Elicitation before deleting a specific memory_id. |
|
|
272
|
-
| `consolidate` | Sleep-mode compression: cluster cold low-importance memories → protected learning-layer summary. Supports `dry_run` preview. **v0.3.0** `use_llm` flag asks the client LLM (Sampling) to rewrite cluster summaries into prose. |
|
|
349
|
+
| `drift_status` | **"What's drifting right now?"** Returns the truth map with 4-species classification (hypothesis/constraint/commitment/source_of_truth) and per-node state (🔴 drift / 🟡 review / ⚪ held / 🔵 aligned). |
|
|
350
|
+
| `check_decision` | **Deep-dive into a specific decision.** Returns the full context: what was decided, why, what reality says, pending candidates, and drift edges. |
|
|
351
|
+
| `declare_anchor` | **Record a decision as a truth-map anchor.** The drift detector checks these against committed reality. Supports v9 fields (domain, confidence, lifecycle, review_after). |
|
|
352
|
+
| `resolve_drift` | **Close the loop.** Record a resolution: `fix` (reality now matches), `supersede` (intent evolved), `acknowledge` (parking with review date), or `dismiss` (false positive). |
|
|
353
|
+
|
|
354
|
+
Previous versions exposed 3 tools — v0.8.0 added 4 drift tools that let agents query and act on product-level intent ↔ reality divergence. The memory tools are unchanged.
|
|
273
355
|
|
|
274
356
|
### CLI utilities
|
|
275
357
|
|
|
@@ -313,7 +395,7 @@ A single SQLite file (`better-sqlite3` + FTS5 trigram tokenizer for JP/EN) conta
|
|
|
313
395
|
- **Layer 4** — `events` (time-series log for heat / momentum computation)
|
|
314
396
|
- **Layer 5** — `file_snapshots` + `session_file_edits` (diff cache + conversation↔file linkage)
|
|
315
397
|
|
|
316
|
-
The conversation↔file linkage is the key. Every file edit captured by the Stop hook is stored alongside the **user message that drove the edit**. So `
|
|
398
|
+
The conversation↔file linkage is the key. Every file edit captured by the Stop hook is stored alongside the **user message that drove the edit**. So `recall({ path: "server.ts" })` returns "this file was edited 30 times across 3 days, and here are the actual user instructions that motivated each change".
|
|
317
399
|
|
|
318
400
|
## Why the design choices
|
|
319
401
|
|
|
@@ -324,17 +406,22 @@ The conversation↔file linkage is the key. Every file edit captured by the Stop
|
|
|
324
406
|
|
|
325
407
|
## Roadmap
|
|
326
408
|
|
|
327
|
-
- ✅
|
|
409
|
+
- ✅ 3-tool unified surface (remember / recall / read_smart) — v0.7.0
|
|
410
|
+
- ✅ Auto-consolidate on server startup — v0.7.0
|
|
411
|
+
- ✅ Claude Code Plugin (`claude plugin add -- linksee-memory`)
|
|
412
|
+
- ✅ Five MCP Blocks (Tools + Resources + Prompts + Sampling + Roots + Elicitation)
|
|
328
413
|
- ✅ Stop-hook auto-capture for Claude Code
|
|
329
414
|
- ✅ JP/EN trigram FTS5
|
|
330
415
|
- ✅ One-command setup (`npx linksee-memory-setup`)
|
|
331
416
|
- ✅ Structured memory v2 (3-axis classification: altitude × type × state)
|
|
332
|
-
- ✅ Precision recall guide + proactive caveat surfacing
|
|
333
417
|
- ✅ Cross-LLM: Claude Code, Cursor, Windsurf, OpenAI Codex, Gemini CLI
|
|
334
|
-
-
|
|
418
|
+
- ✅ Landing page ([linksee-site.vercel.app](https://linksee-site.vercel.app))
|
|
419
|
+
- ✅ Drift detection engine + 4 MCP drift tools — v0.8.0
|
|
420
|
+
- ✅ 4-species truth map (hypothesis/constraint/commitment/source_of_truth) — v0.8.0
|
|
421
|
+
- ✅ Dashboard with Decision Register visualization
|
|
422
|
+
- 🔮 Obsidian plugin (read truth map in your vault)
|
|
335
423
|
- 🔮 Vector search via `sqlite-vec` (already in deps, embedding backend pending)
|
|
336
424
|
- 🔮 Cross-device cloud sync (Pro tier)
|
|
337
|
-
- 🔮 Optional anonymized telemetry → MCP-quality intelligence layer
|
|
338
425
|
|
|
339
426
|
## Comparison with Claude Code auto-memory
|
|
340
427
|
|
|
@@ -454,21 +541,29 @@ recall({ query: "...", entity_name: "my-project", layer: "caveat" })
|
|
|
454
541
|
rm -rf ~/.linksee-memory # nuke everything; next run creates a fresh DB
|
|
455
542
|
```
|
|
456
543
|
|
|
457
|
-
Or delete individual memories via
|
|
544
|
+
Or delete individual memories via `remember({ forget: true, memory_id: <id> })`.
|
|
458
545
|
</details>
|
|
459
546
|
|
|
460
547
|
<details>
|
|
461
548
|
<summary><b>DB is getting large (>100 MB). How do I trim it?</b></summary>
|
|
462
549
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
```
|
|
467
|
-
Caveat and active-goal layers are always preserved. Consider scheduling a weekly run via cron / Task Scheduler.
|
|
550
|
+
Consolidation runs automatically on server startup (7-day threshold). It clusters old cold memories into compressed learning-layer summaries. Caveat and active-goal layers are always preserved.
|
|
551
|
+
|
|
552
|
+
If you want to force a manual consolidation, restart the MCP server — auto-consolidate triggers on every startup.
|
|
468
553
|
</details>
|
|
469
554
|
|
|
470
555
|
## FAQ
|
|
471
556
|
|
|
557
|
+
<details>
|
|
558
|
+
<summary><strong>What is drift detection and why do I need it?</strong></summary>
|
|
559
|
+
|
|
560
|
+
Drift = when your code reality silently diverges from what you decided. Example: Last week you decided "FTS5, not vector search" but this week a new agent session installs pgvector without knowing the history.
|
|
561
|
+
|
|
562
|
+
Linksee Memory tracks this by letting you declare decisions as "anchors" and then automatically checking committed code against them. The make-or-break rule: **intentional evolution (recorded as fix/supersede) stays quiet, while unaccounted gaps get flagged.** It's like Datadog but for product decisions instead of server metrics.
|
|
563
|
+
|
|
564
|
+
You don't need to use drift detection to benefit from linksee-memory — the 3 memory tools (remember/recall/read_smart) work independently. Drift tools are an additional layer for teams and solo devs managing multiple projects.
|
|
565
|
+
</details>
|
|
566
|
+
|
|
472
567
|
<details>
|
|
473
568
|
<summary><strong>How is this different from Mem0 / Letta / Zep?</strong></summary>
|
|
474
569
|
|
|
@@ -484,7 +579,7 @@ Three axes:
|
|
|
484
579
|
Claude Code's auto-memory is Claude-only (doesn't help if you switch to Cursor, OpenAI Codex, or Gemini CLI) and stores flat markdown with no structure. linksee-memory is the same local-first principle but:
|
|
485
580
|
- Works across Claude Code, Cursor, OpenAI Codex, Gemini CLI (shared SQLite)
|
|
486
581
|
- Structured 6-layer format makes recall explainable
|
|
487
|
-
-
|
|
582
|
+
- Auto-consolidation compresses cold memories on startup; caveats are permanently protected
|
|
488
583
|
</details>
|
|
489
584
|
|
|
490
585
|
<details>
|
|
@@ -509,7 +604,7 @@ The default is no sync — the SQLite file lives at `~/.linksee-memory/memory.db
|
|
|
509
604
|
|
|
510
605
|
Two mechanisms:
|
|
511
606
|
1. **Ebbinghaus forgetting**: cold low-importance memories decay naturally, eligible for auto-forget sweeps. `caveat` layer and memories with `importance ≥ 0.9` are always protected.
|
|
512
|
-
2.
|
|
607
|
+
2. **Auto-consolidation**: runs on every server startup (7-day threshold). Compresses clusters of cold low-importance memories by entity into a single `learning`-layer summary, then deletes the originals. No manual scheduling needed.
|
|
513
608
|
|
|
514
609
|
In practice a solo developer hits ~100MB after 6 months of heavy use. A year-old DB I tested with 80K memories still recalls in <10ms.
|
|
515
610
|
</details>
|
|
@@ -523,7 +618,7 @@ Yes — any MCP-compatible client works:
|
|
|
523
618
|
- **Cursor**: add to MCP settings in Cursor → Settings → Features → Model Context Protocol
|
|
524
619
|
- **OpenAI Codex**: `codex mcp add linksee -- npx -y linksee-memory` (or `~/.codex/config.toml` with `[mcp_servers.linksee]` block)
|
|
525
620
|
- **Gemini CLI**: add to `~/.gemini/settings.json` mcpServers section
|
|
526
|
-
- **ChatGPT (web/mobile app)**: stdio MCP not supported by the consumer app — requires Remote MCP server over HTTPS
|
|
621
|
+
- **ChatGPT (web/mobile app)**: stdio MCP not supported by the consumer app — requires Remote MCP server over HTTPS (not yet available).
|
|
527
622
|
- **Custom agent**: the MCP stdio protocol is documented at modelcontextprotocol.io
|
|
528
623
|
</details>
|
|
529
624
|
|
|
@@ -536,7 +631,7 @@ Yes — any MCP-compatible client works:
|
|
|
536
631
|
<details>
|
|
537
632
|
<summary><strong>How do I verify it's actually working?</strong></summary>
|
|
538
633
|
|
|
539
|
-
After install, in a new Claude session ask: *"Can you remember that I prefer TypeScript over JavaScript?"* Claude should confirm it called `mcp__linksee__remember` and stored this. Then in a **different session** ask: *"What languages do I prefer?"* It should recall via `mcp__linksee__recall` and return the preference with `match_reasons` showing why.
|
|
634
|
+
After install, in a new Claude session ask: *"Can you remember that I prefer TypeScript over JavaScript? Use Linksee Memory."* Claude should confirm it called `mcp__linksee__remember` and stored this. Then in a **different session** ask: *"What languages do I prefer? Use Linksee Memory."* It should recall via `mcp__linksee__recall` and return the preference with `match_reasons` showing why.
|
|
540
635
|
</details>
|
|
541
636
|
|
|
542
637
|
## Support
|
|
@@ -548,6 +643,70 @@ After install, in a new Claude session ask: *"Can you remember that I prefer Typ
|
|
|
548
643
|
|
|
549
644
|
## Changelog
|
|
550
645
|
|
|
646
|
+
### v0.8.0 — Drift Detection MCP Tools (2026-06-08)
|
|
647
|
+
|
|
648
|
+
**3 tools → 7 tools.** The biggest update since launch — agents can now detect, query, and resolve intent ↔ reality drift.
|
|
649
|
+
|
|
650
|
+
**New tools:**
|
|
651
|
+
- **`drift_status`** — returns the truth map with 4-species classification and per-node drift state
|
|
652
|
+
- **`check_decision`** — deep-dive into a single anchor: state, edges, pending candidates
|
|
653
|
+
- **`declare_anchor`** — record a decision/constraint/prohibition as a truth-map node (with v9 ProjectCoreNode fields)
|
|
654
|
+
- **`resolve_drift`** — close the feedback loop: fix / supersede / acknowledge / dismiss
|
|
655
|
+
|
|
656
|
+
**New engine module:**
|
|
657
|
+
- **`truth-engine.ts`** — state derivation logic migrated from the dashboard into the MCP engine. Any MCP client can now query drift status without a dashboard.
|
|
658
|
+
- **Resolution priority fix**: when multiple resolutions reference the same anchor, the most recent one wins (by `resolved_at` timestamp). Prevents a stale acknowledge from shadowing a newer fix.
|
|
659
|
+
- **4-species classification**: nodes classified by `decision_mode` into hypothesis / constraint / commitment / source_of_truth with display format guidance.
|
|
660
|
+
|
|
661
|
+
No breaking changes to existing memory tools. All 3 memory tools (remember, recall, read_smart) are unchanged.
|
|
662
|
+
|
|
663
|
+
### v0.7.2 — Recall ergonomics + auto-edge detection + classifier precision (2026-05-30)
|
|
664
|
+
|
|
665
|
+
Quality pass on v0.7.0 / v0.7.1 — sharper day-to-day agent UX and cleaner data for the dashboard:
|
|
666
|
+
|
|
667
|
+
- **`recall` token discipline**: drops the redundant `content_raw` from the response (parsed `content` was already there — it was a 2× duplicate), and actually enforces `max_tokens` by greedy assembly that measures real serialized size (was a flat ~100 tok/memory estimate). Adds `approx_tokens` to the response so the agent can see its budget usage. The same query that previously returned ~15,800 tokens for a 1200 budget now stays inside it.
|
|
668
|
+
- **`recall` precision**: near-duplicate memories — same entity + near-identical core text, e.g. the same message captured under both `goal` and `learning` — collapse to one in the result set. Composite weights adapt to query specificity: multi-term queries weight relevance higher so off-topic-but-pinned memories don't crowd narrow recalls.
|
|
669
|
+
- **Capture dedup (write side)**: `session-extractor` now produces AT MOST one memory per user turn, with priority `goal[first_intent] > caveat > decision > context`. A first-intent message containing decision words (e.g. "決めた" / "これで進めよう") is no longer double-saved as both `goal` and `learning`.
|
|
670
|
+
- **`memory_edges` auto-detection**: the previously-empty `memory_edges` table is now populated during the sleep-mode consolidation sweep. `detectMemoryEdges()` links a later DECISION memory to the most-recent earlier same-topic decision within an entity (chain, not clique) so the dashboard can render Pivot Chains. The default relation is `extends` — a same-topic later decision builds on, but does NOT deactivate, the earlier one. Explicit reversal markers (やめる / revert / instead of) produce `contradicts`; explicit replacement markers (の代わり / replaces / deprecate) produce `supersedes`. Prevents silent deactivation of still-valid decisions.
|
|
671
|
+
- **`inferType` / `inferState` precision**: chitchat acknowledgements ("そうだね" / "ありがとう"), pasted terminal/git/email content, and meta-noise no longer classify as `decision` — they return `note` / `open` before pattern matching. The learning-layer default → `decision` is gated by this guard. Real decisions (採用 / 決めた, even after an acknowledgement opener) survive.
|
|
672
|
+
|
|
673
|
+
No schema migration, no breaking API changes. Existing rows keep their stored content; the classifier improvements apply to new captures going forward.
|
|
674
|
+
|
|
675
|
+
### v0.7.1 — Review fixes (2026-05-29)
|
|
676
|
+
|
|
677
|
+
Based on Opus 4.7 design review of v0.7.0:
|
|
678
|
+
|
|
679
|
+
- **P0 — Required params guidance**: `remember` tool description now includes "REQUIRED PARAMS BY MODE" section so LLMs know exactly which fields are needed for create vs update vs delete.
|
|
680
|
+
- **P0 — Migration guidance**: Deprecated tool names (`forget`, `recall_file`, etc.) now return specific migration examples instead of generic errors.
|
|
681
|
+
- **P1 — recall path+query merge**: When both `path` and `query` are provided to `recall`, results from file history and memory search are merged into a single response.
|
|
682
|
+
- **P2 — Auto-consolidate safety**: Table existence check via `sqlite_master` before querying `consolidations` table, preventing errors on fresh databases.
|
|
683
|
+
|
|
684
|
+
### v0.7.0 — 3-Tool Unified Surface (2026-05-29)
|
|
685
|
+
|
|
686
|
+
**8 tools → 3 tools.** Following Context7's proven pattern of fewer tools = better cross-LLM consistency.
|
|
687
|
+
|
|
688
|
+
**Breaking change**: The following tools are removed from the MCP surface. Calling them returns a migration guide:
|
|
689
|
+
|
|
690
|
+
| Old tool | New equivalent |
|
|
691
|
+
|---|---|
|
|
692
|
+
| `forget` | `remember({ forget: true, memory_id: <id> })` |
|
|
693
|
+
| `update_memory` | `remember({ memory_id: <id>, content: "..." })` |
|
|
694
|
+
| `recall_file` | `recall({ path: "server.ts" })` |
|
|
695
|
+
| `list_entities` | `recall({})` (no params = entity overview) |
|
|
696
|
+
| `consolidate` | Auto-runs on server startup (7-day threshold) |
|
|
697
|
+
|
|
698
|
+
**New unified tools:**
|
|
699
|
+
- **`remember`** — create + update + delete in one tool. Mode is inferred from params.
|
|
700
|
+
- **`recall`** — search + file history + overview in one tool. Mode is inferred from params.
|
|
701
|
+
- **`read_smart`** — unchanged.
|
|
702
|
+
|
|
703
|
+
**Other changes:**
|
|
704
|
+
- Auto-consolidate on server startup (non-blocking `setTimeout`, 7-day threshold, `sqlite_master` safety check)
|
|
705
|
+
- Claude Code Plugin bundle (`claude plugin add -- linksee-memory`)
|
|
706
|
+
- Deprecation errors include specific migration examples
|
|
707
|
+
|
|
708
|
+
All internal handler functions are preserved — this is a surface change, not a logic rewrite.
|
|
709
|
+
|
|
551
710
|
### v0.2.0 — English-first launch readiness (2026-04-20)
|
|
552
711
|
|
|
553
712
|
Prepares the package for a broader (primarily English-speaking) audience on Reddit, Hacker News, and Anthropic Discord. No breaking API changes.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory-declare — declare / list / retire drift anchors (v8).
|
|
3
|
+
//
|
|
4
|
+
// The explicit write path for drift observability. Anchors declared here are clean
|
|
5
|
+
// by construction (declare-don't-mine): a human typed them. The bulk seeding script
|
|
6
|
+
// (curate the existing candidate pool) reuses curateAnchorFromMemory() from the lib.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// linksee-memory-declare list [--status active|retired] [--kind prohibition|decision|constraint]
|
|
10
|
+
// linksee-memory-declare add --kind <k> --statement "<text>" [--rationale "<text>"]
|
|
11
|
+
// [--affects "glob1,glob2"] [--terms "t1,t2"] [--violation "v1,v2"] [--tier human|explicit]
|
|
12
|
+
// linksee-memory-declare retire --id <n>
|
|
13
|
+
import { openDb, runMigrations } from '../db/migrate.js';
|
|
14
|
+
import { declareAnchor, listAnchors, retireAnchor, setNodeFields, getCurrentTruth, getAlertPolicy, setAlertPolicy, } from '../lib/drift-anchors.js';
|
|
15
|
+
function parseFlags(argv) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a.startsWith('--')) {
|
|
20
|
+
const key = a.slice(2);
|
|
21
|
+
const next = argv[i + 1];
|
|
22
|
+
if (next === undefined || next.startsWith('--')) {
|
|
23
|
+
out[key] = 'true';
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
out[key] = next;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function splitList(v) {
|
|
34
|
+
if (!v)
|
|
35
|
+
return [];
|
|
36
|
+
return v
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
function print(obj) {
|
|
42
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
43
|
+
}
|
|
44
|
+
function main() {
|
|
45
|
+
const [, , sub, ...rest] = process.argv;
|
|
46
|
+
const flags = parseFlags(rest);
|
|
47
|
+
const db = openDb();
|
|
48
|
+
runMigrations(db); // ensure drift tables exist when run standalone
|
|
49
|
+
try {
|
|
50
|
+
switch (sub) {
|
|
51
|
+
case 'add': {
|
|
52
|
+
const anchor = declareAnchor(db, {
|
|
53
|
+
kind: flags.kind,
|
|
54
|
+
statement: flags.statement ?? '',
|
|
55
|
+
rationale: flags.rationale,
|
|
56
|
+
affects: splitList(flags.affects),
|
|
57
|
+
detect_terms: splitList(flags.terms),
|
|
58
|
+
violation_signal: splitList(flags.violation),
|
|
59
|
+
tier: flags.tier || undefined,
|
|
60
|
+
});
|
|
61
|
+
// v9 ProjectCoreNode fields (minimal input — only what's given):
|
|
62
|
+
// --node-type --domain --mode --confidence --cadence --stale-days --applies --not-applies
|
|
63
|
+
const nf = {};
|
|
64
|
+
if (flags['node-type'])
|
|
65
|
+
nf.node_type = flags['node-type'];
|
|
66
|
+
if (flags.domain)
|
|
67
|
+
nf.domain = flags.domain;
|
|
68
|
+
if (flags.mode)
|
|
69
|
+
nf.decision_mode = flags.mode;
|
|
70
|
+
if (flags.confidence)
|
|
71
|
+
nf.confidence = Number(flags.confidence);
|
|
72
|
+
if (flags.cadence || flags['stale-days']) {
|
|
73
|
+
const cp = { enabled: true };
|
|
74
|
+
if (flags.cadence)
|
|
75
|
+
cp.cadence_days = Number(flags.cadence);
|
|
76
|
+
if (flags['stale-days'])
|
|
77
|
+
cp.stale_threshold_days = Number(flags['stale-days']);
|
|
78
|
+
nf.card_policy = cp;
|
|
79
|
+
}
|
|
80
|
+
if (flags.applies || flags['not-applies']) {
|
|
81
|
+
const vs = {};
|
|
82
|
+
if (flags.applies)
|
|
83
|
+
vs.applies_to = splitList(flags.applies);
|
|
84
|
+
if (flags['not-applies'])
|
|
85
|
+
vs.does_not_apply_to = splitList(flags['not-applies']);
|
|
86
|
+
nf.validity_scope = vs;
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(nf).length)
|
|
89
|
+
setNodeFields(db, anchor.id, nf);
|
|
90
|
+
print({ ok: true, declared: anchor, node_fields: Object.keys(nf).length ? nf : null });
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'retire': {
|
|
94
|
+
const id = Number(flags.id);
|
|
95
|
+
if (!Number.isFinite(id))
|
|
96
|
+
throw new Error('--id <n> required');
|
|
97
|
+
const ok = retireAnchor(db, id);
|
|
98
|
+
print({ ok, retired: ok ? id : null });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'list':
|
|
102
|
+
case undefined: {
|
|
103
|
+
const anchors = listAnchors(db, {
|
|
104
|
+
status: flags.status,
|
|
105
|
+
kind: flags.kind,
|
|
106
|
+
});
|
|
107
|
+
print({ ok: true, count: anchors.length, anchors });
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'truth': {
|
|
111
|
+
// ⑧ read_smart-style scoped read: active Current-Truth slice only.
|
|
112
|
+
const nodes = getCurrentTruth(db, { domain: flags.domain, decision_mode: flags.mode });
|
|
113
|
+
print({ ok: true, scope: { domain: flags.domain ?? 'all', decision_mode: flags.mode ?? 'all' }, count: nodes.length, current_truth: nodes });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'policy': {
|
|
117
|
+
const p = {};
|
|
118
|
+
if (flags['max-cards-per-day'])
|
|
119
|
+
p.max_cards_per_day = Number(flags['max-cards-per-day']);
|
|
120
|
+
if (flags['max-soft-per-week'])
|
|
121
|
+
p.max_soft_cards_per_week = Number(flags['max-soft-per-week']);
|
|
122
|
+
if (flags['min-soft-confidence'])
|
|
123
|
+
p.min_confidence_for_soft_card = Number(flags['min-soft-confidence']);
|
|
124
|
+
if (flags['two-sided'])
|
|
125
|
+
p.require_two_sided_evidence = flags['two-sided'] !== 'false';
|
|
126
|
+
const policy = Object.keys(p).length ? setAlertPolicy(db, p) : getAlertPolicy(db);
|
|
127
|
+
print({ ok: true, alert_policy: policy });
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`unknown subcommand "${sub}" — use: add | list | retire | truth | policy`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
print({ ok: false, error: err?.message ?? String(err) });
|
|
136
|
+
db.close();
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
db.close();
|
|
140
|
+
}
|
|
141
|
+
if (import.meta.url === `file://${process.argv[1]}` ||
|
|
142
|
+
process.argv[1]?.endsWith('declare-anchor.ts') ||
|
|
143
|
+
process.argv[1]?.endsWith('declare-anchor.js')) {
|
|
144
|
+
main();
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=declare-anchor.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory-detect — run the drift detector (照合: declared intent vs. actual reality).
|
|
3
|
+
//
|
|
4
|
+
// The manual-trigger entry point for drift detection (the cadence question). Mirrors
|
|
5
|
+
// declare-anchor.ts; purely additive — touches only drift_edges (the feature's own table) and
|
|
6
|
+
// ONLY when --persist is passed. SAFE BY DEFAULT: a bare run is a dry-run (reads only, no
|
|
7
|
+
// writes), so you can preview drift before committing it to the view. Pass --persist to write
|
|
8
|
+
// the edges that /drift renders. No embedding layer: matching is lexical/glob/trigram-FTS only.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// linksee-memory-detect # dry-run — preview drift, writes NOTHING (default)
|
|
12
|
+
// linksee-memory-detect --persist # write contradicts/absent edges into drift_edges
|
|
13
|
+
// linksee-memory-detect --stale-days 30 # override absence staleness gate (default 14)
|
|
14
|
+
// linksee-memory-detect --threshold 0.5 # override emit threshold (default 0.5)
|
|
15
|
+
import { openDb, runMigrations } from '../db/migrate.js';
|
|
16
|
+
import { detectDrift, detectFileViolations } from '../lib/drift-detection.js';
|
|
17
|
+
function parseFlags(argv) {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a.startsWith('--')) {
|
|
22
|
+
const key = a.slice(2);
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (next === undefined || next.startsWith('--')) {
|
|
25
|
+
out[key] = 'true';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
out[key] = next;
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function print(obj) {
|
|
36
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
37
|
+
}
|
|
38
|
+
function main() {
|
|
39
|
+
const [, , ...rest] = process.argv;
|
|
40
|
+
const flags = parseFlags(rest);
|
|
41
|
+
const persist = flags.persist === 'true';
|
|
42
|
+
const staleDays = flags['stale-days'] !== undefined ? Number(flags['stale-days']) : undefined;
|
|
43
|
+
const emitThreshold = flags.threshold !== undefined ? Number(flags.threshold) : undefined;
|
|
44
|
+
if (staleDays !== undefined && !Number.isFinite(staleDays))
|
|
45
|
+
throw new Error('--stale-days <n> must be a number');
|
|
46
|
+
if (emitThreshold !== undefined && !Number.isFinite(emitThreshold))
|
|
47
|
+
throw new Error('--threshold <n> must be a number');
|
|
48
|
+
const db = openDb();
|
|
49
|
+
runMigrations(db); // ensure drift tables exist when run standalone
|
|
50
|
+
try {
|
|
51
|
+
const res = detectDrift(db, { dryRun: !persist, staleDays, emitThreshold });
|
|
52
|
+
const fres = detectFileViolations(db, { dryRun: !persist, emitThreshold });
|
|
53
|
+
print({
|
|
54
|
+
ok: true,
|
|
55
|
+
mode: persist ? 'PERSISTED' : 'DRY RUN (no writes — pass --persist to write)',
|
|
56
|
+
persisted: res.persisted,
|
|
57
|
+
anchorsScanned: res.anchorsScanned,
|
|
58
|
+
editsScanned: res.editsScanned,
|
|
59
|
+
// v1 — edit-snippet scan (what a captured edit's snippet contained)
|
|
60
|
+
editSnippetScan: {
|
|
61
|
+
contradicts: res.contradicts,
|
|
62
|
+
absent: res.absent,
|
|
63
|
+
edgesEmitted: res.edgesEmitted,
|
|
64
|
+
byAnchorHits: res.byAnchor.filter((b) => b.contradicts > 0 || b.absent > 0),
|
|
65
|
+
samples: res.samples.slice(0, 6),
|
|
66
|
+
},
|
|
67
|
+
// v2 — current-file scan (live file:line against violation_signal)
|
|
68
|
+
currentFileScan: {
|
|
69
|
+
filesScanned: fres.filesScanned,
|
|
70
|
+
anchorsCapped: fres.anchorsCapped,
|
|
71
|
+
contradicts: fres.contradicts,
|
|
72
|
+
edgesEmitted: fres.edgesEmitted,
|
|
73
|
+
byAnchorHits: fres.byAnchor,
|
|
74
|
+
samples: fres.samples.slice(0, 14),
|
|
75
|
+
},
|
|
76
|
+
totalEdgesEmitted: res.edgesEmitted + fres.edgesEmitted,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
print({ ok: false, error: err?.message ?? String(err) });
|
|
81
|
+
db.close();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
db.close();
|
|
85
|
+
}
|
|
86
|
+
if (import.meta.url === `file://${process.argv[1]}` ||
|
|
87
|
+
process.argv[1]?.endsWith('detect-drift.ts') ||
|
|
88
|
+
process.argv[1]?.endsWith('detect-drift.js')) {
|
|
89
|
+
main();
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=detect-drift.js.map
|