sessionmem 1.4.0 → 1.5.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +186 -41
  3. package/dist/adapters/diagnostics.js +109 -0
  4. package/dist/adapters/generic.js +6 -2
  5. package/dist/adapters/global/antigravity.js +4 -1
  6. package/dist/adapters/global/claudeCode.js +19 -4
  7. package/dist/adapters/global/codex.js +7 -6
  8. package/dist/adapters/global/qcoder.js +4 -1
  9. package/dist/adapters/ide/cline.js +3 -0
  10. package/dist/adapters/ide/cursor.js +3 -0
  11. package/dist/adapters/ide/installer.js +17 -5
  12. package/dist/adapters/ide/windsurf.js +3 -0
  13. package/dist/adapters/installedHosts.js +51 -0
  14. package/dist/cli/commands/doctor.js +146 -0
  15. package/dist/cli/commands/entities.js +27 -0
  16. package/dist/cli/commands/history.js +23 -0
  17. package/dist/cli/commands/ingestHook.js +4 -78
  18. package/dist/cli/commands/install.js +34 -56
  19. package/dist/cli/commands/scan.js +16 -0
  20. package/dist/cli/commands/search.js +1 -0
  21. package/dist/cli/commands/team.js +7 -3
  22. package/dist/cli/commands/ui.js +67 -0
  23. package/dist/cli/commands/watch.js +48 -0
  24. package/dist/cli/index.js +42 -1
  25. package/dist/cli/projectId.js +1 -2
  26. package/dist/cli/transcriptSweep.js +182 -0
  27. package/dist/core/api/contracts.js +1 -0
  28. package/dist/core/api/memoryCoreService.js +8 -0
  29. package/dist/core/api/sessionLifecycleService.js +13 -1
  30. package/dist/core/capture/toolEvent.js +82 -0
  31. package/dist/core/extract/factClaims.js +97 -0
  32. package/dist/core/extract/knowledge.js +127 -0
  33. package/dist/core/retrieve/retrieveMemories.js +20 -4
  34. package/dist/core/schema/migrations/012_transcript_checkpoints.sql +4 -0
  35. package/dist/core/schema/migrations/013_fts_porter.sql +32 -0
  36. package/dist/core/schema/migrations/014_memory_entities.sql +19 -0
  37. package/dist/core/schema/migrations/015_memory_supersessions.sql +41 -0
  38. package/dist/core/schema/runMigrations.js +46 -0
  39. package/dist/core/storage/knowledgeRepo.js +67 -0
  40. package/dist/core/storage/memoryRepo.js +45 -12
  41. package/dist/core/storage/memorySearchRepo.js +30 -20
  42. package/dist/core/storage/supersessionRepo.js +101 -0
  43. package/dist/core/summarize/summaryShape.js +1 -1
  44. package/dist/ui/assets.js +181 -0
  45. package/dist/ui/server.js +147 -0
  46. package/docs/REGISTRY_LISTING.md +88 -0
  47. package/docs/architecture.md +27 -3
  48. package/docs/benchmark.md +105 -33
  49. package/docs/migration.md +2 -0
  50. package/docs/troubleshooting.md +52 -4
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0] - 2026-10-18
9
+
10
+ ### Added
11
+
12
+ - `sessionmem scan` to import existing Claude Code transcripts into memory, and
13
+ `sessionmem watch` to capture new and updated sessions continuously. Both are
14
+ idempotent across reruns and restarts and work fully offline. ([#45])
15
+ - Automatic host detection during `sessionmem install`, and a `sessionmem doctor`
16
+ command that checks the store, hooks, adapter wiring, and recall with fix
17
+ hints for each failure. ([#46])
18
+ - Deterministic entity and relation extraction during capture and scan, a
19
+ `sessionmem entities` command, and entity overlap as a recall ranking
20
+ signal. ([#48])
21
+ - Deterministic supersession handling: memories that replace an earlier fact are
22
+ linked with history, and recall prefers current facts while replaced ones stay
23
+ retrievable explicitly. ([#49])
24
+ - `sessionmem ui`, a local read-only web viewer for memories, entities,
25
+ relations, and supersession history, served on localhost with no external
26
+ services. ([#50])
27
+ - COMPARISON.md, a cited comparison with claude-mem, Mem0, and Zep, plus a draft
28
+ MCP registry listing. ([#51])
29
+ - A reproducible offline LoCoMo evaluation: committed corpus with provenance and
30
+ checksums, and a runner reporting hit rate, precision, recall, and latency
31
+ with all network access blocked. ([#44])
32
+
33
+ ### Changed
34
+
35
+ - Retrieval now ranks with SQLite FTS5 BM25 and Porter stemming over memory
36
+ content and summaries. Measured on the committed LoCoMo evaluation, top-5 hit
37
+ rate improved from 1.44% to 61.95%, and to 63.92% with entity overlap. ([#47], [#48])
38
+ - The release workflow skips the npm publish when the version already exists on
39
+ the registry and creates the GitHub release with notes from this
40
+ changelog. ([#43])
41
+
8
42
  ## [1.4.0] - 2026-10-16
9
43
 
10
44
  ### Added
@@ -68,6 +102,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
68
102
  errors and findings fail the job; added scanner dependency updates and
69
103
  Dependabot cooldowns. ([#32])
70
104
 
105
+ [1.5.0]: https://github.com/catfish-1234/sessionmem/compare/679b8954f0b49f197985c40da47f181c00060cd9...v1.5.0
106
+ [#43]: https://github.com/catfish-1234/sessionmem/pull/43
107
+ [#44]: https://github.com/catfish-1234/sessionmem/pull/44
108
+ [#45]: https://github.com/catfish-1234/sessionmem/pull/45
109
+ [#46]: https://github.com/catfish-1234/sessionmem/pull/46
110
+ [#47]: https://github.com/catfish-1234/sessionmem/pull/47
111
+ [#48]: https://github.com/catfish-1234/sessionmem/pull/48
112
+ [#49]: https://github.com/catfish-1234/sessionmem/pull/49
113
+ [#50]: https://github.com/catfish-1234/sessionmem/pull/50
114
+ [#51]: https://github.com/catfish-1234/sessionmem/pull/51
71
115
  [1.4.0]: https://github.com/catfish-1234/sessionmem/compare/a73f503123b1d33c35407ce66403f281ba3451ac...679b8954f0b49f197985c40da47f181c00060cd9
72
116
  [#22]: https://github.com/catfish-1234/sessionmem/pull/22
73
117
  [#23]: https://github.com/catfish-1234/sessionmem/pull/23
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <a href="https://github.com/catfish-1234/sessionmem/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
10
10
  </p>
11
11
 
12
- **Project memory between sessions, stored on your machine. Startup context used 85.6% fewer tokens than full session history in the included benchmark.**
12
+ **Project memory between sessions, stored on your machine.**
13
13
 
14
14
  ```
15
15
  New session. Claude starts fresh.
@@ -49,16 +49,25 @@ Run this in your project folder after configuring your AI tool (Claude Code, Cur
49
49
 
50
50
  ```bash
51
51
  sessionmem install
52
- # From a plain terminal, choose the host explicitly when needed:
52
+ # Optional: select one host explicitly, including in automation:
53
53
  sessionmem install --adapter claude-code
54
54
  # Other names: cursor, cline, codex, windsurf, antigravity, qcoder, generic
55
55
  ```
56
56
 
57
- This initializes the database and, for supported adapters:
57
+ `install` detects installed hosts from local executables, including from a plain
58
+ terminal, and configures every host found. Automatic discovery currently supports
59
+ Claude Code on `PATH` or in its standard native install locations. Use
60
+ `--adapter <name>` for the other adapters or to override discovery. The existing
61
+ `SESSIONMEM_ADAPTER` environment override also works; the flag takes precedence.
62
+ If no supported host is found, the command says so and exits nonzero before
63
+ creating files. Discovery and configuration work fully offline.
64
+
65
+ This initializes the database and, for each selected adapter:
58
66
 
59
67
  - Registers `sessionmem` with your AI tool's MCP host so the host can start it automatically.
60
68
  - Creates `~/.sessionmem/config.json` with secret redaction enabled and a 90-day retention window, if the file doesn't already exist.
61
69
  - Adds instructions to the selected host's guidance file, such as `~/.claude/CLAUDE.md` for Claude Code or `~/.codex/AGENTS.md` for Codex. You can re-run the command without duplicating those instructions.
70
+ - Installs Claude Code's SessionStart, SessionEnd, and PostToolUse hooks and prints what was configured per host. If a host fails, the remaining hosts are still attempted and the command exits nonzero.
62
71
 
63
72
  ### 3. Restart your AI tool
64
73
 
@@ -73,10 +82,12 @@ After restarting:
73
82
  - In Claude Code, the installed PostToolUse, SessionEnd, and SessionStart hooks capture supported tool events, summarize them, and load prior memories automatically.
74
83
  - Other MCP hosts use sessionmem's tools and installed instructions. Automatic capture and session lifecycle calls depend on the host or agent. MCP registration alone does not install Claude Code hooks there.
75
84
 
76
- Check that the local database is accessible:
85
+ Check the installation offline:
77
86
 
78
87
  ```bash
79
- sessionmem ping
88
+ sessionmem doctor
89
+ # For an explicitly selected host:
90
+ sessionmem doctor --adapter claude-code
80
91
  ```
81
92
 
82
93
  ---
@@ -139,36 +150,21 @@ The SQLite file is easy to inspect, back up, or delete, and you can use it acros
139
150
 
140
151
  ## Benchmark results
141
152
 
142
- These numbers come from `npm run benchmark` (`scripts/benchmark.mjs`). It runs the production retrieval and injection code against fixed, synthetic test data without network calls. See [`docs/benchmark.md`](docs/benchmark.md) for the full report and reproduction steps.
143
-
144
- ### Token savings
145
-
146
- > **~85.6% reduction** in tokens compared to carrying full session history.
147
-
148
- | | Tokens |
149
- |---|---|
150
- | Full session history (baseline) | 1,587 |
151
- | What sessionmem injects at the start of your next session | 228 |
152
-
153
- In this benchmark, about 1,600 tokens of session history become roughly 230 tokens of decisions, warnings, and facts for the next session.
154
-
155
- ### Retrieval accuracy
153
+ `npm run benchmark` evaluates conversational retrieval on the committed LoCoMo
154
+ corpus: 10 conversations, 272 sessions, 5,882 turns, and 1,527 scored questions.
155
+ It captures dialogue through the public `storeMemory` API and asks the original
156
+ questions through `retrieveMemories`, fully offline with no API keys.
156
157
 
157
- > **100% hit-rate**: all 10 test queries retrieved the expected memory.
158
-
159
- | Metric | Result |
160
- |---|---|
161
- | Hit-rate (10 curated queries) | 100.0% |
162
- | Recall | 100.0% |
163
- | Precision | 33.3% |
164
-
165
- Each query returns 3 candidate memories, with one scored as the expected match. That gives 33.3% precision even when the expected memory is found every time. The other two candidates may provide context, but are not scored as matches. These results apply to the 10 curated queries; see the report for the embedding model's limits.
166
-
167
- The benchmark is deterministic and reproducible:
158
+ The evaluation reports hit rate, precision, recall, complete-evidence retrieval,
159
+ and latency. Production retrieval uses offline BM25 ranking with English stemming;
160
+ conversational questions test recall beyond exact copies of memory text.
161
+ See [benchmark results](docs/benchmark.md) and the
162
+ [methodology, dataset license, and metric definitions](benchmarks/README.md).
168
163
 
169
164
  ```bash
170
- npm run build # benchmark imports the compiled code from dist/
171
- npm run benchmark # regenerates docs/benchmark.md
165
+ npm run build
166
+ npm run benchmark
167
+ npm run benchmark -- --output benchmarks/results/locomo.json
172
168
  ```
173
169
 
174
170
  ---
@@ -213,16 +209,22 @@ See [`docs/architecture.md`](docs/architecture.md) for the technical overview.
213
209
 
214
210
  | Command | What it does |
215
211
  |---------|--------------|
216
- | `sessionmem install [--adapter <name>]` | Register sessionmem with the current MCP host and write default config. |
212
+ | `sessionmem install [--adapter <name>]` | Detect installed hosts, register sessionmem with each, and write default config; the flag selects one host. |
213
+ | `sessionmem doctor [--adapter <name>]` | Check the store, schema, hooks, MCP wiring, and canary recall offline; print PASS/FAIL and repair hints. |
217
214
  | `sessionmem uninstall [--adapter <name>] [--purge]` | Remove sessionmem from the host. `--purge` also deletes the local database. |
218
215
  | `sessionmem run` | Start the MCP server. |
216
+ | `sessionmem ui [--port N]` | Open the local read-only web viewer (default port 3777). |
219
217
  | `sessionmem ping` | Check the local version and database reachability. |
220
- | `sessionmem search <query> [--limit <n>]` | Search memories by semantic query (limit 1–100, default 10). |
218
+ | `sessionmem search <query> [--limit <n>] [--include-history]` | Search current memories by text query (limit 1–100, default 10). Include superseded facts with `--include-history`. |
221
219
  | `sessionmem list` | List up to 200 recent memories for the current project; warns when truncated. |
220
+ | `sessionmem entities [--project <id>] [--json]` | List extracted entities, typed relations, and their source memory IDs. Defaults to the current project. |
221
+ | `sessionmem history [--project <id>] [--json]` | List supersession chains with old/new values, source memory IDs, reasons, and timestamps. |
222
222
  | `sessionmem show <id>` | Show memory details (content capped at 2000 characters; use export for full content). |
223
223
  | `sessionmem forget <id> [--force]` | Delete a memory by ID. |
224
224
  | `sessionmem export [path]` | Export all current-project memories to JSON (default `~/.sessionmem/export-YYYY-MM-DD.json`). |
225
225
  | `sessionmem import <path> [--merge]` | Import up to 1000 valid records from UTF-8 JSON into the current project. Existing IDs are preserved, including with `--merge`; invalid records are skipped with warnings. |
226
+ | `sessionmem scan [--transcripts-dir <path>]` | Import existing Claude Code JSONL transcripts into local session memories. Reports files found, sessions imported, memories created, skipped files, and errors. |
227
+ | `sessionmem watch [--once] [--interval <seconds>] [--transcripts-dir <path>]` | Capture new or updated Claude Code transcripts. Polls every 5 seconds by default; `--once` runs a single sweep. |
226
228
  | `sessionmem stats` | Show memory statistics for the current project. |
227
229
  | `sessionmem savings [--json]` | Show token savings from compression and injection, with percentage. |
228
230
  | `sessionmem redact-scan [--apply]` | Scan stored memories for secrets; `--apply` redacts in place. |
@@ -253,9 +255,139 @@ for Windows paths. Imports accept CRLF and a UTF-8 BOM; convert other encodings
253
255
 
254
256
  `SESSIONMEM_PROJECT_ID` pins the project bucket (useful when moving a repository);
255
257
  otherwise it is derived from the repository root or working directory.
256
- `SESSIONMEM_ADAPTER` selects the install/uninstall host unless `--adapter` is set.
258
+ `SESSIONMEM_ADAPTER` selects the install/doctor/uninstall host unless `--adapter` is set.
257
259
  `SESSIONMEM_DEBUG=1` enables ingest-hook diagnostics on stderr.
258
260
 
261
+ ### Inspect memories in your browser
262
+
263
+ ```bash
264
+ sessionmem ui
265
+ sessionmem ui --port 8080
266
+ sessionmem ui --port 0 # Pick an available port
267
+ ```
268
+
269
+ The command prints its URL and opens your default browser. It binds only to
270
+ `127.0.0.1`, needs no authentication, and serves all assets locally using Node's
271
+ built-in HTTP server. No external services, CDNs, or additional runtime dependencies
272
+ are used. If browser launch is unavailable, open the printed URL manually. Press
273
+ Ctrl-C to stop the server and close its database connection.
274
+
275
+ The viewer starts on the current project, with a selector for other projects in
276
+ the same store. Browse saved memories, search through the existing recall path,
277
+ inspect extracted entities and relations with their source memories, and follow
278
+ supersession chains with old/new values, source IDs, assertion times, detection
279
+ times, and partial/full status. Search returns up to 50 results and defaults to
280
+ current facts; select **Include superseded facts in search** to recall history.
281
+ Browsing lists all saved records, including superseded and expired ones, in pages
282
+ of 50. Source IDs open full memory details. Entity visibility and history links
283
+ use the same rules as `sessionmem entities` and `sessionmem history`.
284
+
285
+ The database is opened read-only. Browsing and on-demand search do not increment
286
+ access counts, migrate the schema, or change memories. Only HTTP GET and HEAD are
287
+ accepted; no write, delete, or edit routes are exposed. Responses are not cached,
288
+ and cross-origin browser requests and unexpected Host headers are rejected. Run
289
+ `sessionmem ping` first to initialize a missing store or upgrade an older schema.
290
+ The usual `SESSIONMEM_DB_PATH` and `SESSIONMEM_PROJECT_ID` overrides
291
+ apply. Anyone with access to this computer can read the local viewer while it is
292
+ running; stop it when finished inspecting private memories.
293
+
294
+ ### Import existing sessions and watch for new ones
295
+
296
+ When a fact changes, sessionmem can link its replacement locally. For example, “I use VS Code” followed by “I switched to Neovim” supersedes the earlier editor choice. Recall excludes fully superseded memories by default. `retrieveMemories` accepts the optional `includeHistory: true`; `sessionmem search --include-history` searches earlier facts too. `sessionmem history` shows what replaced what. The old memory stays available through show, list, and export.
297
+
298
+ Detection uses deterministic entity/relation rules, shared purpose words, a small tool-category vocabulary, and explicit negation or replacement phrases. It needs no API or model. Unfamiliar wording can be missed, and ambiguous changes can be misclassified. A memory with independent claims remains searchable while any claim is current; history labels these partial supersessions. See [supersession details](docs/architecture.md#contradiction-and-supersession).
299
+
300
+ Run a scan to capture Claude Code sessions from before sessionmem was installed:
301
+
302
+ ```bash
303
+ sessionmem scan
304
+ sessionmem watch --once
305
+ sessionmem watch --interval 10
306
+ ```
307
+
308
+ Both commands recursively read `~/.claude/projects/**/*.jsonl`. Use
309
+ `--transcripts-dir "/path/to/projects"` to select another transcript tree. They
310
+ work fully offline without API keys and always use the local summarizer, even
311
+ if cloud summarization is enabled elsewhere.
312
+
313
+ Transcript user/assistant text and supported tool uses (Bash, Edit, Write,
314
+ MultiEdit) pass through the existing event-ingestion, redaction, and session-summary
315
+ pipeline. Each session gets one summary memory, including short sessions with
316
+ only one event. Updated sessions refresh that summary. Project buckets come from
317
+ each transcript's `cwd`, using its repository root when available; the directory
318
+ where you run scan/watch does not select the project. `SESSIONMEM_PROJECT_ID`
319
+ can explicitly pin all imports to one bucket. Transcripts without a working
320
+ directory or capturable events are skipped; nested subagent files get separate
321
+ session summaries.
322
+
323
+ Re-running either command does not duplicate memories or transcript events.
324
+ File checkpoints live in the selected memory database, and unchanged files are
325
+ skipped across restarts. Stable event IDs also make interrupted imports safe to
326
+ retry. Partial or malformed JSONL lines are reported and retried on later sweeps;
327
+ valid events in the same file can still be captured. A single sweep exits with
328
+ status 1 if it encounters errors. The daemon keeps running and retries errors.
329
+
330
+ `watch` prints its first sweep and subsequent imports or errors. It runs in the
331
+ foreground until SIGINT or SIGTERM. For a background process:
332
+
333
+ ```bash
334
+ nohup sessionmem watch > sessionmem-watch.log 2>&1 &
335
+ ```
336
+
337
+ For macOS launchd, save this as
338
+ `~/Library/LaunchAgents/io.sessionmem.watch.plist`. Replace the executable path
339
+ with the output of `command -v sessionmem`, and adjust PATH for your Node install:
340
+
341
+ ```xml
342
+ <?xml version="1.0" encoding="UTF-8"?>
343
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
344
+ <plist version="1.0">
345
+ <dict>
346
+ <key>Label</key><string>io.sessionmem.watch</string>
347
+ <key>ProgramArguments</key>
348
+ <array><string>/opt/homebrew/bin/sessionmem</string><string>watch</string></array>
349
+ <key>EnvironmentVariables</key>
350
+ <dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
351
+ <key>RunAtLoad</key><true/>
352
+ <key>KeepAlive</key><true/>
353
+ </dict>
354
+ </plist>
355
+ ```
356
+
357
+ ```bash
358
+ launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/io.sessionmem.watch.plist
359
+ # Stop and unload:
360
+ launchctl bootout "gui/$(id -u)/io.sessionmem.watch"
361
+ ```
362
+
363
+ For a Linux systemd user service, save this as
364
+ `~/.config/systemd/user/sessionmem-watch.service`. Replace the executable path
365
+ and adjust PATH for your Node install:
366
+
367
+ ```ini
368
+ [Unit]
369
+ Description=Capture Claude Code transcripts with sessionmem
370
+
371
+ [Service]
372
+ ExecStart=/usr/local/bin/sessionmem watch
373
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
374
+ Restart=on-failure
375
+ RestartSec=5
376
+
377
+ [Install]
378
+ WantedBy=default.target
379
+ ```
380
+
381
+ ```bash
382
+ systemctl --user daemon-reload
383
+ systemctl --user enable --now sessionmem-watch.service
384
+ journalctl --user -u sessionmem-watch.service -f
385
+ # Stop and disable:
386
+ systemctl --user disable --now sessionmem-watch.service
387
+ ```
388
+
389
+ These are optional examples; `sessionmem install` does not register a daemon.
390
+
259
391
  ---
260
392
 
261
393
  ## Privacy, secrets, and your data
@@ -296,13 +428,13 @@ Old decisions and duplicate facts can accumulate in a memory store, causing the
296
428
  sessionmem retention prune --force # actually deletes
297
429
  ```
298
430
 
299
- 2. **Importance-weighted ranking**: retrieval ranks memories by semantic relevance, recency, and importance. Older, less important notes rank lower even before they are pruned.
431
+ 2. **Importance-weighted ranking**: retrieval ranks memories by offline BM25 lexical relevance, entity overlap, recency, and importance. Older, less important notes rank lower even before they are pruned.
300
432
 
301
433
  3. **Token-budgeted injection**: only the highest-ranked memories are loaded, trimmed to a small token budget (see [benchmarks](#benchmark-results)). The context stays within that budget as the store grows.
302
434
 
303
435
  4. **Conflict resolution in team mode**: merging teammate memories uses last-write-wins by id to avoid stale duplicates and keeps the higher importance score so a warning isn't downgraded.
304
436
 
305
- The benchmark above found every expected memory across its 10 queries (100% hit-rate and recall) with ranking and trimming enabled.
437
+ The [conversational benchmark](benchmarks/README.md) measures retrieval quality separately from retention and startup token budgets.
306
438
 
307
439
  Export your memories before pruning if you want to keep a copy:
308
440
 
@@ -359,7 +491,7 @@ Other tools that implement the [Model Context Protocol](https://modelcontextprot
359
491
  ## Further documentation
360
492
 
361
493
  - [Architecture](docs/architecture.md): how the core engine, adapters, CLI, and SQLite storage fit together.
362
- - [Benchmark](docs/benchmark.md): full token-reduction and retrieval-accuracy report, and how to reproduce it.
494
+ - [Benchmark](docs/benchmark.md): conversational retrieval results and reproduction steps.
363
495
  - [Privacy and retention](docs/privacy-and-retention.md): secret redaction, retention pruning, and config.
364
496
  - [Team mode](docs/team-mode.md): shared-path team memory.
365
497
  - [Cloud summarization](docs/cloud-summarization.md): the opt-in cloud summarization path.
@@ -372,13 +504,26 @@ Other tools that implement the [Model Context Protocol](https://modelcontextprot
372
504
 
373
505
  For install failures, adapter issues, missing session data, or `better-sqlite3` native-build problems, see [`docs/troubleshooting.md`](docs/troubleshooting.md).
374
506
 
375
- To refresh the installation and check for stored session data:
507
+ Start with offline diagnostics:
376
508
 
377
509
  ```bash
378
- sessionmem install # idempotent; Claude Code also installs all three hooks
379
- sessionmem stats # memories, sessions, and session_events for the current project
510
+ sessionmem doctor
511
+ sessionmem doctor --adapter claude-code # explicit host override
380
512
  ```
381
513
 
514
+ `doctor` reports a separate PASS or FAIL for the memory store, schema, hook
515
+ definitions shipped with the current version, configured MCP command, and a
516
+ canary write/recall. It exits nonzero if any check fails and prints fix hints,
517
+ including exact commands for safe repairs. It does not create a missing store or
518
+ apply migrations. Its temporary canary is isolated from your projects and rolled
519
+ back even on failure. The MCP check starts the installed local command and verifies
520
+ its version, tools, and a stats response; it does not require a running agent.
521
+
522
+ Use `sessionmem ping` to initialize a missing store or apply pending schema
523
+ migrations, or `sessionmem install --adapter claude-code` to refresh Claude Code's
524
+ MCP registration and hooks. Run repairs with the same `SESSIONMEM_DB_PATH` as
525
+ doctor when using a custom store.
526
+
382
527
  If `stats` shows `sessions: 0` after real work, see [“0 sessions” / no session data recorded](docs/troubleshooting.md#0-sessions--no-session-data-recorded). Memories are keyed to the **repository root**, so every directory inside one repo shares a bucket; outside a repo the working directory itself is the key.
383
528
 
384
529
  To check database access and memory counts:
@@ -0,0 +1,109 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ import { IDEInstaller, isManagedClaudeHook } from "./ide/installer.js";
6
+ import { resolveDbPath } from "../cli/context.js";
7
+ function object(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value)
9
+ ? value
10
+ : {};
11
+ }
12
+ export function checkHooks(installation) {
13
+ const expected = installation.hooks;
14
+ if (!expected)
15
+ return "This host uses MCP tools; no lifecycle hooks are required";
16
+ const config = IDEInstaller.parseJsonc(readFileSync(expected.path, "utf8"));
17
+ if (config.disableAllHooks === true)
18
+ throw new Error("Claude Code has disableAllHooks enabled");
19
+ const hooks = object(config.hooks);
20
+ for (const spec of expected.entries) {
21
+ const entries = Array.isArray(hooks[spec.event]) ? hooks[spec.event] : [];
22
+ const managed = entries.flatMap((value) => {
23
+ const entry = object(value);
24
+ return Array.isArray(entry.hooks) ? entry.hooks.filter((value) => isManagedClaudeHook(object(value).command, spec.command)).map((hook) => ({ entry, hook: object(hook) })) : [];
25
+ });
26
+ const matches = managed.filter(({ entry, hook }) => {
27
+ if (entry.matcher !== spec.matcher || entry.async === true)
28
+ return false;
29
+ return hook.type === "command" && hook.command === spec.command && hook.async !== true;
30
+ });
31
+ if (managed.length !== 1 || matches.length !== 1)
32
+ throw new Error(`${spec.event} hook is missing, duplicated, or differs from the current definition`);
33
+ }
34
+ return `SessionStart, SessionEnd, PostToolUse match current hook definitions (${expected.path})`;
35
+ }
36
+ /** Check the installed launch entry before executing the known local command. */
37
+ export function readMcpLaunch(installation) {
38
+ const expected = installation.mcp;
39
+ const content = readFileSync(expected.path, "utf8");
40
+ let entry;
41
+ if (expected.format === "toml") {
42
+ // The Codex installer writes this small TOML section with JSON-compatible
43
+ // quoted values. Refuse unfamiliar syntax rather than claiming it works.
44
+ const section = content.match(/^\s*\[mcp_servers\.sessionmem\][^\S\r\n]*\r?\n([\s\S]*?)(?=^\s*\[|$(?![\s\S]))/m)?.[1];
45
+ if (!section)
46
+ throw new Error("sessionmem MCP section is missing");
47
+ if (/^\s*\[mcp_servers\.sessionmem\./m.test(content) || /^\s*(?:env|env_vars|cwd|url)\s*=/m.test(section)) {
48
+ throw new Error("Custom Codex launch settings require manual verification; doctor supports the installer-generated command and args");
49
+ }
50
+ if (/^\s*enabled\s*=\s*false\s*$/m.test(section))
51
+ throw new Error("sessionmem MCP server is disabled");
52
+ entry = {
53
+ command: JSON.parse(section.match(/^\s*command\s*=\s*(.+)$/m)?.[1] ?? "null"),
54
+ args: JSON.parse(section.match(/^\s*args\s*=\s*(.+)$/m)?.[1] ?? "null"),
55
+ };
56
+ }
57
+ else {
58
+ const config = IDEInstaller.parseJsonc(content);
59
+ entry = object(object(config.mcpServers).sessionmem);
60
+ }
61
+ if (entry.disabled === true)
62
+ throw new Error("sessionmem MCP server is disabled");
63
+ if (entry.type !== undefined && entry.type !== "stdio")
64
+ throw new Error("sessionmem MCP server must use the local stdio transport");
65
+ if (entry.command !== expected.command || JSON.stringify(entry.args) !== JSON.stringify(expected.args)) {
66
+ throw new Error(`sessionmem MCP launch entry differs from the current definition (${expected.path})`);
67
+ }
68
+ if (entry.env !== undefined && (entry.env === null || typeof entry.env !== "object" || Array.isArray(entry.env))) {
69
+ throw new Error("sessionmem MCP environment must be an object");
70
+ }
71
+ const env = object(entry.env);
72
+ if (Object.values(env).some((value) => typeof value !== "string")) {
73
+ throw new Error("sessionmem MCP environment values must be strings");
74
+ }
75
+ return { command: expected.command, args: expected.args, env: env };
76
+ }
77
+ export async function probeMcpWiring(launch, dbPath, version) {
78
+ if (launch.env?.SESSIONMEM_DB_PATH && resolve(resolveDbPath(launch.env.SESSIONMEM_DB_PATH)) !== resolve(dbPath)) {
79
+ throw new Error("MCP configuration selects a different SESSIONMEM_DB_PATH; run doctor with that path");
80
+ }
81
+ const inherited = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
82
+ const client = new Client({ name: "sessionmem-doctor", version });
83
+ const transport = new StdioClientTransport({
84
+ command: launch.command,
85
+ args: launch.args,
86
+ env: { ...inherited, ...launch.env, SESSIONMEM_DB_PATH: dbPath },
87
+ stderr: "ignore",
88
+ });
89
+ try {
90
+ await client.connect(transport, { timeout: 5000 });
91
+ const server = client.getServerVersion();
92
+ if (server?.name !== "sessionmem" || server.version !== version) {
93
+ throw new Error(`MCP server version does not match sessionmem ${version}`);
94
+ }
95
+ const tools = await client.listTools({}, { timeout: 5000 });
96
+ for (const name of ["stats", "storeMemory", "retrieveMemories"]) {
97
+ if (!tools.tools.some((tool) => tool.name === name))
98
+ throw new Error(`MCP tool ${name} is missing`);
99
+ }
100
+ const result = await client.callTool({ name: "stats", arguments: {} }, undefined, { timeout: 5000 });
101
+ const payload = Array.isArray(result.content) ? object(result.content.find((part) => object(part).type === "text")) : {};
102
+ if (result.isError || typeof payload.text !== "string" || !JSON.parse(payload.text).ok) {
103
+ throw new Error("MCP stats tool failed");
104
+ }
105
+ }
106
+ finally {
107
+ await transport.close();
108
+ }
109
+ }
@@ -69,6 +69,7 @@ const TOOL_DEFINITIONS = [
69
69
  mode: retrieveMemoriesRequestSchema.shape.mode.describe("'auto' for the standard startup context-load path. 'on-demand' for an explicit mid-session retrieval triggered by a specific task or question."),
70
70
  depth: retrieveMemoriesRequestSchema.shape.depth.describe("'default' for standard semantic search. 'deep' for a broader sweep that surfaces less-similar memories — use when the topic is new or unfamiliar."),
71
71
  tags: retrieveMemoriesRequestSchema.shape.tags.describe("Optional tag filter. When provided, only return memories that have ALL listed tags."),
72
+ includeHistory: retrieveMemoriesRequestSchema.shape.includeHistory.describe("Include superseded facts for historical questions. Omit for current facts only."),
72
73
  },
73
74
  },
74
75
  {
@@ -209,18 +210,21 @@ export class GenericMCPAdapter {
209
210
  guidanceTargets() {
210
211
  return [join(process.cwd(), "AGENTS.md")];
211
212
  }
213
+ installation() {
214
+ return { mcp: { path: join(process.cwd(), ".mcp.json"), command: "sessionmem", args: ["run"] } };
215
+ }
212
216
  /**
213
217
  * Fallback for hosts that aren't specifically detected: register sessionmem
214
218
  * in a project-local `.mcp.json` (the de-facto generic MCP config format).
215
219
  */
216
220
  async install() {
217
- const configPath = join(process.cwd(), ".mcp.json");
221
+ const configPath = this.installation().mcp.path;
218
222
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
219
223
  "run",
220
224
  ]);
221
225
  }
222
226
  async uninstall() {
223
- const configPath = join(process.cwd(), ".mcp.json");
227
+ const configPath = this.installation().mcp.path;
224
228
  return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
225
229
  }
226
230
  async call(_method, _request) {
@@ -11,13 +11,16 @@ import { IDEInstaller } from "../ide/installer.js";
11
11
  const ANTIGRAVITY_MCP_CONFIG = [".gemini", "config", "mcp_config.json"];
12
12
  export class AntigravityAdapter extends GenericMCPAdapter {
13
13
  name = "Antigravity";
14
+ installation() {
15
+ return { mcp: { path: join(homedir(), ...ANTIGRAVITY_MCP_CONFIG), command: "sessionmem", args: ["run"] } };
16
+ }
14
17
  // Capabilities inherited from GenericMCPAdapter (tools only).
15
18
  /** Antigravity reads AGENTS.md-style guidance from its global config dir. */
16
19
  guidanceTargets() {
17
20
  return [join(homedir(), ".antigravity", "AGENTS.md")];
18
21
  }
19
22
  async install() {
20
- const configPath = join(homedir(), ...ANTIGRAVITY_MCP_CONFIG);
23
+ const configPath = this.installation().mcp.path;
21
24
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
22
25
  "run",
23
26
  ]);
@@ -21,15 +21,30 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
21
21
  settingsPath() {
22
22
  return join(homedir(), ".claude", "settings.json");
23
23
  }
24
+ installation() {
25
+ const isWindows = process.platform === "win32";
26
+ return {
27
+ mcp: {
28
+ path: join(homedir(), ".claude.json"),
29
+ command: isWindows ? "cmd" : "sessionmem",
30
+ args: isWindows ? ["/c", "sessionmem", "run"] : ["run"],
31
+ },
32
+ hooks: {
33
+ path: this.settingsPath(),
34
+ entries: [
35
+ { event: "SessionStart", command: SESSIONMEM_HOOK_COMMAND },
36
+ { event: "SessionEnd", command: SESSIONMEM_SESSION_END_HOOK_COMMAND },
37
+ { event: "PostToolUse", command: SESSIONMEM_POST_TOOL_HOOK_COMMAND, matcher: SESSIONMEM_POST_TOOL_MATCHER },
38
+ ],
39
+ },
40
+ };
41
+ }
24
42
  async install() {
25
- const configPath = join(homedir(), ".claude.json");
26
43
  // On Windows, the globally-installed `sessionmem` bin is a `.cmd` shim.
27
44
  // Claude Code spawns MCP servers WITHOUT a shell, and `.cmd` shims require
28
45
  // cmd.exe to execute, so a bare `command: "sessionmem"` fails to start.
29
46
  // Route through `cmd /c sessionmem run` so the shim is resolved correctly.
30
- const isWindows = process.platform === "win32";
31
- const command = isWindows ? "cmd" : "sessionmem";
32
- const args = isWindows ? ["/c", "sessionmem", "run"] : ["run"];
47
+ const { path: configPath, command, args } = this.installation().mcp;
33
48
  const mcpOk = await IDEInstaller.injectMcpConfig(configPath, "sessionmem", command, args);
34
49
  // Register a SessionStart hook so prior memories are injected automatically
35
50
  // at the start of EVERY Claude Code session. This is the deterministic
@@ -12,6 +12,9 @@ args = ["run"]
12
12
  `;
13
13
  export class CodexAdapter extends GenericMCPAdapter {
14
14
  name = "Codex";
15
+ installation() {
16
+ return { mcp: { path: join(homedir(), ".codex", "config.toml"), command: "sessionmem", args: ["run"], format: "toml" } };
17
+ }
15
18
  // Capabilities inherited from GenericMCPAdapter (tools only).
16
19
  /** Codex reads AGENTS.md; the global one lives in ~/.codex/AGENTS.md. */
17
20
  guidanceTargets() {
@@ -20,21 +23,19 @@ export class CodexAdapter extends GenericMCPAdapter {
20
23
  async install() {
21
24
  try {
22
25
  const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
23
- const configPath = join(homedir(), ".codex", "config.toml");
26
+ const configPath = this.installation().mcp.path;
24
27
  const existing = existsSync(configPath)
25
28
  ? readFileSync(configPath, "utf-8")
26
29
  : "";
27
- // Idempotent: skip if the sessionmem server section already exists.
28
- // Anchor on line start so a commented/substring mention doesn't match.
29
- if (existing.split("\n").some((line) => line.trimStart().startsWith("[mcp_servers.sessionmem]")))
30
- return true;
30
+ // Replace our section so re-install also repairs stale launch commands.
31
+ const withoutServer = existing.replace(/^\s*\[mcp_servers\.sessionmem\][^\S\r\n]*\r?\n[\s\S]*?(?=^\s*\[|$(?![\s\S]))/m, "");
31
32
  const dir = dirname(configPath);
32
33
  if (!existsSync(dir)) {
33
34
  mkdirSync(dir, { recursive: true });
34
35
  }
35
36
  // Append the TOML block, trimming trailing whitespace to keep a single
36
37
  // blank-line separator before our section.
37
- const updated = existing.trimEnd() + "\n" + CODEX_MCP_BLOCK;
38
+ const updated = withoutServer.trimEnd() + "\n" + CODEX_MCP_BLOCK;
38
39
  writeFileSync(configPath, updated, "utf-8");
39
40
  return true;
40
41
  }
@@ -17,13 +17,16 @@ import { IDEInstaller } from "../ide/installer.js";
17
17
  // directory spelling, or wire it through the UI-managed store if no file exists.
18
18
  export class QCoderAdapter extends GenericMCPAdapter {
19
19
  name = "QCoder";
20
+ installation() {
21
+ return { mcp: { path: join(homedir(), ".qcoder", "config.json"), command: "sessionmem", args: ["run"] } };
22
+ }
20
23
  // Capabilities inherited from GenericMCPAdapter (tools only).
21
24
  /** QCoder reads AGENTS.md-style guidance from its global config dir. */
22
25
  guidanceTargets() {
23
26
  return [join(homedir(), ".qcoder", "AGENTS.md")];
24
27
  }
25
28
  async install() {
26
- const configPath = join(homedir(), ".qcoder", "config.json");
29
+ const configPath = this.installation().mcp.path;
27
30
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
28
31
  "run",
29
32
  ]);
@@ -59,6 +59,9 @@ export class ClineAdapter extends GenericMCPAdapter {
59
59
  // expects to read.
60
60
  return IDEInstaller.injectMcpConfig(this.configPath, "sessionmem", "sessionmem", ["run"], { disabled: false, autoApprove: [] });
61
61
  }
62
+ installation() {
63
+ return { mcp: { path: this.configPath, command: "sessionmem", args: ["run"] } };
64
+ }
62
65
  async uninstall() {
63
66
  return IDEInstaller.removeMcpConfig(this.configPath, "sessionmem");
64
67
  }