sessionmem 1.5.0 → 1.7.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/CHANGELOG.md +43 -0
- package/README.md +148 -79
- package/dist/cli/autostart/index.js +246 -0
- package/dist/cli/commands/autostart.js +24 -0
- package/dist/cli/commands/config.js +1 -0
- package/dist/cli/commands/doctor.js +28 -0
- package/dist/cli/commands/install.js +10 -0
- package/dist/cli/commands/uninstall.js +4 -0
- package/dist/cli/commands/watch.js +14 -2
- package/dist/cli/index.js +16 -7
- package/dist/cli/transcriptSweep.js +103 -143
- package/dist/cli/transcripts/claudeCode.js +62 -0
- package/dist/cli/transcripts/codex.js +111 -0
- package/dist/cli/transcripts/cursor.js +295 -0
- package/dist/cli/transcripts/index.js +118 -0
- package/dist/cli/transcripts/types.js +15 -0
- package/dist/cli/watchCheckpoint.js +35 -0
- package/dist/cli/watchLog.js +23 -0
- package/dist/core/api/contracts.js +3 -0
- package/dist/core/api/memoryCoreService.js +37 -1
- package/dist/core/capture/toolEvent.js +14 -2
- package/dist/core/config/policyConfig.js +3 -0
- package/dist/core/embed/localSemanticEmbedder.js +62 -0
- package/dist/core/retrieve/retrieveMemories.js +18 -2
- package/dist/core/schema/migrations/016_semantic_embeddings.sql +12 -0
- package/dist/core/storage/memorySearchRepo.js +11 -0
- package/dist/core/storage/semanticSearchRepo.js +56 -0
- package/dist/core/storage/sessionEventsRepo.js +12 -3
- package/docs/architecture.md +13 -1
- package/docs/benchmark.md +9 -0
- package/docs/local-semantic.md +56 -0
- package/docs/research/automatic-capture.md +62 -0
- package/docs/troubleshooting.md +5 -0
- package/package.json +21 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.7.0] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Automatic transcript capture at login: per-user login services on macOS,
|
|
15
|
+
Linux, and Windows keep `sessionmem watch` running after install, so new
|
|
16
|
+
sessions are captured without a manual command. ([#54])
|
|
17
|
+
- Optional local MiniLM retrieval complements FTS5 with learned semantic
|
|
18
|
+
candidates. Installed model files are verified locally; unavailable inference
|
|
19
|
+
falls back to BM25. The feature defaults off. See [setup](docs/local-semantic.md).
|
|
20
|
+
([#55])
|
|
21
|
+
- Pinned competitor source reviews, local raw-turn LoCoMo comparisons, complete
|
|
22
|
+
result archives, and per-question retrieval regressions. Measured on LoCoMo:
|
|
23
|
+
hit rate 63.92% to 66.47%, precision 14.05% to 14.77%, recall 57.45% to
|
|
24
|
+
59.76%. See [measured comparison](COMPARISON.md). ([#55])
|
|
25
|
+
|
|
26
|
+
## [1.6.0] - 2026-10-18
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Multi-host transcript imports: `sessionmem scan` and `sessionmem watch` now
|
|
31
|
+
sweep Claude Code, Codex CLI, and Cursor histories by default, with a
|
|
32
|
+
separate parser per host and an explicit unsupported-format error when a
|
|
33
|
+
claimed host's records cannot be parsed. `--host` selects one host.
|
|
34
|
+
([#52], [#53])
|
|
35
|
+
- Codex CLI support: dated rollout discovery, project and session identity from
|
|
36
|
+
session metadata, resume-file grouping with cross-file deduplication,
|
|
37
|
+
sub-agent session separation, native tool-call events, and filtering of
|
|
38
|
+
encrypted agent payloads and injected machine blocks. ([#52])
|
|
39
|
+
- Cursor support: chat history read from Cursor's SQLite storage through
|
|
40
|
+
private snapshot copies opened read-only, covering both composer index
|
|
41
|
+
layouts, with per-composer checkpoints and live-update handling. ([#53])
|
|
42
|
+
- `sessionmem doctor` now checks transcript discovery for each supported
|
|
43
|
+
host. ([#52], [#53])
|
|
44
|
+
|
|
8
45
|
## [1.5.0] - 2026-10-18
|
|
9
46
|
|
|
10
47
|
### Added
|
|
@@ -102,6 +139,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
139
|
errors and findings fail the job; added scanner dependency updates and
|
|
103
140
|
Dependabot cooldowns. ([#32])
|
|
104
141
|
|
|
142
|
+
[1.7.0]: https://github.com/catfish-1234/sessionmem/compare/v1.6.0...v1.7.0
|
|
143
|
+
[1.6.0]: https://github.com/catfish-1234/sessionmem/compare/v1.5.0...v1.6.0
|
|
144
|
+
[#54]: https://github.com/catfish-1234/sessionmem/pull/54
|
|
145
|
+
[#55]: https://github.com/catfish-1234/sessionmem/pull/55
|
|
105
146
|
[1.5.0]: https://github.com/catfish-1234/sessionmem/compare/679b8954f0b49f197985c40da47f181c00060cd9...v1.5.0
|
|
106
147
|
[#43]: https://github.com/catfish-1234/sessionmem/pull/43
|
|
107
148
|
[#44]: https://github.com/catfish-1234/sessionmem/pull/44
|
|
@@ -112,6 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
112
153
|
[#49]: https://github.com/catfish-1234/sessionmem/pull/49
|
|
113
154
|
[#50]: https://github.com/catfish-1234/sessionmem/pull/50
|
|
114
155
|
[#51]: https://github.com/catfish-1234/sessionmem/pull/51
|
|
156
|
+
[#52]: https://github.com/catfish-1234/sessionmem/pull/52
|
|
157
|
+
[#53]: https://github.com/catfish-1234/sessionmem/pull/53
|
|
115
158
|
[1.4.0]: https://github.com/catfish-1234/sessionmem/compare/a73f503123b1d33c35407ce66403f281ba3451ac...679b8954f0b49f197985c40da47f181c00060cd9
|
|
116
159
|
[#22]: https://github.com/catfish-1234/sessionmem/pull/22
|
|
117
160
|
[#23]: https://github.com/catfish-1234/sessionmem/pull/23
|
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ WITH sessionmem:
|
|
|
29
29
|
|
|
30
30
|
`sessionmem` is an MCP server that stores decisions, warnings, and project context across sessions. Claude Code hooks capture supported tool events and load relevant memories automatically. Other hosts use its MCP tools and installed instructions; session automation depends on the host. It works with Claude Code, Cursor, Cline, Codex, Windsurf, and other [MCP](https://modelcontextprotocol.io) hosts.
|
|
31
31
|
|
|
32
|
+
Default retrieval uses SQLite FTS5 BM25 and entity overlap. An optional
|
|
33
|
+
[local MiniLM model](docs/local-semantic.md) adds semantic candidates. Weights are
|
|
34
|
+
installed separately; queries make no network calls and fall back to BM25 when
|
|
35
|
+
local inference is unavailable. See [measured comparisons](COMPARISON.md) for
|
|
36
|
+
quality gains, missed questions, and latency costs.
|
|
37
|
+
|
|
32
38
|
By default, everything stays on your machine. You don't need an account or a cloud service. Data only leaves your computer if you enable sharing or cloud summarization.
|
|
33
39
|
|
|
34
40
|
---
|
|
@@ -62,12 +68,25 @@ Claude Code on `PATH` or in its standard native install locations. Use
|
|
|
62
68
|
If no supported host is found, the command says so and exits nonzero before
|
|
63
69
|
creating files. Discovery and configuration work fully offline.
|
|
64
70
|
|
|
65
|
-
This initializes the database and
|
|
71
|
+
This initializes the database and configures the selected adapters:
|
|
66
72
|
|
|
67
73
|
- Registers `sessionmem` with your AI tool's MCP host so the host can start it automatically.
|
|
68
74
|
- Creates `~/.sessionmem/config.json` with secret redaction enabled and a 90-day retention window, if the file doesn't already exist.
|
|
69
75
|
- 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
76
|
- 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.
|
|
77
|
+
- Enables and starts one per-user login service for `sessionmem watch`. Re-running install updates that registration in place and restarts capture. Capture continues after closing the terminal and starts again at login.
|
|
78
|
+
|
|
79
|
+
Login capture uses a LaunchAgent in `~/Library/LaunchAgents` on macOS, a
|
|
80
|
+
`systemd --user` service on Linux, and a Scheduled Task with a user logon trigger
|
|
81
|
+
on Windows. Linux requires systemd and an available user service manager; macOS
|
|
82
|
+
requires a GUI login session. If registration or startup fails, install exits
|
|
83
|
+
nonzero, preserves completed setup, and prints a repair hint. After fixing the
|
|
84
|
+
OS service error, run `sessionmem autostart enable`.
|
|
85
|
+
|
|
86
|
+
The watcher captures existing and updated Claude Code, Codex CLI, and Cursor
|
|
87
|
+
history offline, without API keys. It starts immediately after install; no
|
|
88
|
+
manual scan or watch process is required. Hosts outside these three can use MCP
|
|
89
|
+
memory tools, but do not have a transcript reader.
|
|
71
90
|
|
|
72
91
|
### 3. Restart your AI tool
|
|
73
92
|
|
|
@@ -80,7 +99,8 @@ another format or location, copy its server entry into that host’s MCP config.
|
|
|
80
99
|
After restarting:
|
|
81
100
|
|
|
82
101
|
- In Claude Code, the installed PostToolUse, SessionEnd, and SessionStart hooks capture supported tool events, summarize them, and load prior memories automatically.
|
|
83
|
-
-
|
|
102
|
+
- Codex and Cursor use the auto-started watcher for transcript capture. Install does not wire lifecycle capture hooks for these hosts. Their MCP tools and installed instructions provide recall and explicit memory operations.
|
|
103
|
+
- Other MCP hosts use sessionmem's tools and installed instructions; automatic transcript capture requires a supported transcript reader.
|
|
84
104
|
|
|
85
105
|
Check the installation offline:
|
|
86
106
|
|
|
@@ -209,9 +229,10 @@ See [`docs/architecture.md`](docs/architecture.md) for the technical overview.
|
|
|
209
229
|
|
|
210
230
|
| Command | What it does |
|
|
211
231
|
|---------|--------------|
|
|
212
|
-
| `sessionmem install [--adapter <name>]` | Detect installed hosts,
|
|
213
|
-
| `sessionmem doctor [--adapter <name>]` | Check the store, schema, hooks, MCP wiring, and canary recall offline; print PASS/FAIL and repair hints. |
|
|
214
|
-
| `sessionmem
|
|
232
|
+
| `sessionmem install [--adapter <name>]` | Detect installed hosts, configure integrations, write default config, and enable login capture; the flag selects one host. |
|
|
233
|
+
| `sessionmem doctor [--adapter <name>]` | Check the store, schema, auto-start, binary paths, capture freshness, transcript discovery, hooks, MCP wiring, and canary recall offline; print PASS/FAIL and repair hints. |
|
|
234
|
+
| `sessionmem autostart enable\|disable\|status` | Start login capture, stop and unregister it, or inspect its registration and binary paths. |
|
|
235
|
+
| `sessionmem uninstall [--adapter <name>] [--purge]` | Stop and unregister login capture, then remove sessionmem from the selected host. `--purge` also deletes the local database. |
|
|
215
236
|
| `sessionmem run` | Start the MCP server. |
|
|
216
237
|
| `sessionmem ui [--port N]` | Open the local read-only web viewer (default port 3777). |
|
|
217
238
|
| `sessionmem ping` | Check the local version and database reachability. |
|
|
@@ -223,8 +244,8 @@ See [`docs/architecture.md`](docs/architecture.md) for the technical overview.
|
|
|
223
244
|
| `sessionmem forget <id> [--force]` | Delete a memory by ID. |
|
|
224
245
|
| `sessionmem export [path]` | Export all current-project memories to JSON (default `~/.sessionmem/export-YYYY-MM-DD.json`). |
|
|
225
246
|
| `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
|
|
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. |
|
|
247
|
+
| `sessionmem scan [--host <host>] [--transcripts-dir <path>]` | Import existing Claude Code, Codex CLI, and Cursor transcripts into local session memories. Reports files found, sessions imported, memories created, skips, and errors. |
|
|
248
|
+
| `sessionmem watch [--once] [--interval <seconds>] [--host <host>] [--transcripts-dir <path>]` | Capture new or updated Claude Code, Codex CLI, and Cursor transcripts. Polls every 5 seconds by default; `--once` runs a single sweep. |
|
|
228
249
|
| `sessionmem stats` | Show memory statistics for the current project. |
|
|
229
250
|
| `sessionmem savings [--json]` | Show token savings from compression and injection, with percentage. |
|
|
230
251
|
| `sessionmem redact-scan [--apply]` | Scan stored memories for secrets; `--apply` redacts in place. |
|
|
@@ -297,96 +318,140 @@ When a fact changes, sessionmem can link its replacement locally. For example,
|
|
|
297
318
|
|
|
298
319
|
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
320
|
|
|
300
|
-
|
|
321
|
+
Install starts capture for existing history and new sessions automatically. For a manual import or a foreground diagnostic run:
|
|
301
322
|
|
|
302
323
|
```bash
|
|
303
324
|
sessionmem scan
|
|
304
325
|
sessionmem watch --once
|
|
305
326
|
sessionmem watch --interval 10
|
|
327
|
+
sessionmem scan --host cursor
|
|
306
328
|
```
|
|
307
329
|
|
|
308
|
-
Both commands
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
Both commands sweep all detected transcript histories by default:
|
|
331
|
+
|
|
332
|
+
- Claude Code: `~/.claude/projects/**/*.jsonl`.
|
|
333
|
+
- Codex CLI: `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`.
|
|
334
|
+
- Cursor: `User/globalStorage/state.vscdb` under
|
|
335
|
+
`~/Library/Application Support/Cursor` on macOS, `~/.config/Cursor` on Linux,
|
|
336
|
+
or `%APPDATA%/Cursor` on Windows.
|
|
337
|
+
|
|
338
|
+
Missing history directories are allowed. Use `--transcripts-dir "/path/to/history"`
|
|
339
|
+
to select another transcript tree; each JSONL file's host is detected from its records.
|
|
340
|
+
For Cursor, the override accepts its `User` directory or a `state.vscdb` path.
|
|
341
|
+
Use `--host cursor`, `--host codex`, or `--host claude-code` to select one host,
|
|
342
|
+
including when using an override. A Cursor User directory or `.vscdb` file is
|
|
343
|
+
also detected without `--host`. Files that do not match a supported format, or
|
|
344
|
+
the selected host, produce an explicit `unsupported format` error.
|
|
345
|
+
|
|
346
|
+
Both commands work offline without API keys and always use the local summarizer,
|
|
347
|
+
even if cloud summarization is enabled elsewhere. `sessionmem doctor` checks
|
|
348
|
+
transcript discovery for all three hosts, parses JSONL samples, and validates
|
|
349
|
+
Cursor's composer indexes and message rows when history is present.
|
|
350
|
+
|
|
351
|
+
Transcript user/assistant text, supported Claude tool uses (Bash, Edit, Write,
|
|
352
|
+
MultiEdit), and Codex function calls pass through the existing event-ingestion,
|
|
353
|
+
redaction, and session-summary pipeline. Codex tool events retain the native tool
|
|
354
|
+
name and `call_id`. Encrypted `agent_message` payloads and obvious injected machine
|
|
355
|
+
blocks, including plugin catalogues and environment context, are excluded. Each
|
|
356
|
+
session gets one summary memory, including short sessions with only one event.
|
|
357
|
+
Updated sessions refresh that summary. Project buckets come from
|
|
358
|
+
each Claude transcript's `cwd`, Codex `session_meta.payload.cwd`, or Cursor's
|
|
359
|
+
workspace path, using the repository root when available; the directory where you run scan/watch does not
|
|
360
|
+
select the project. `SESSIONMEM_PROJECT_ID` can explicitly pin all imports to one
|
|
361
|
+
bucket. Transcripts without capturable events are skipped. Claude transcripts
|
|
362
|
+
without a working directory are also skipped; Codex session metadata must contain
|
|
363
|
+
an ID and working directory. Nested subagent files get separate session summaries.
|
|
364
|
+
Summary memories record `sourceAdapter` as `claude-code`, `codex`, or `cursor`
|
|
365
|
+
according to the transcript host. Cursor session IDs come from `composerId`.
|
|
366
|
+
|
|
367
|
+
Cursor stores messages in `cursorDiskKV` bubble rows in its global SQLite
|
|
368
|
+
database. The importer supports workspace `composer.composerData` indexes
|
|
369
|
+
(Cursor 2.6 and earlier) and the central `composer.composerHeaders` index
|
|
370
|
+
(Cursor 3.0 and later). It captures up to 4,000 characters of user/assistant text
|
|
371
|
+
per bubble and excludes tool-only bubbles and internal tool/code fields. Missing
|
|
372
|
+
tables, index keys, or required conversation data produce an explicit
|
|
373
|
+
`unsupported format for cursor` error.
|
|
374
|
+
|
|
375
|
+
Cursor databases are never written. Each sweep copies the database and its WAL
|
|
376
|
+
to a private temporary directory, checking for concurrent changes during the
|
|
377
|
+
copy. SQLite opens the copy read-only, using immutable mode when URI support is
|
|
378
|
+
available and there is no WAL to replay. Cursor's live database is never opened by SQLite. Busy
|
|
379
|
+
copies are reported and retried. Each sweep reads a fresh snapshot; per-composer
|
|
380
|
+
checkpoints skip unchanged messages, and streamed bubble updates refresh the
|
|
381
|
+
existing event without duplicating it.
|
|
382
|
+
|
|
383
|
+
Codex resumes create new rollout files. Files with the same session ID are grouped,
|
|
384
|
+
and replayed events are deduplicated across files, sweeps, and restarts. A Codex
|
|
385
|
+
sub-agent keeps its own `session_meta.payload.id`, even when `parent_thread_id`
|
|
386
|
+
points to a parent session.
|
|
322
387
|
|
|
323
388
|
Re-running either command does not duplicate memories or transcript events.
|
|
324
|
-
File checkpoints live in the selected memory database, and unchanged
|
|
325
|
-
skipped across restarts.
|
|
389
|
+
File and composer checkpoints live in the selected memory database, and unchanged
|
|
390
|
+
files or composers are skipped across restarts. For Cursor, `files found` counts
|
|
391
|
+
global databases, and `skipped` counts unchanged or empty composers.
|
|
392
|
+
Stable event IDs also make interrupted imports safe to
|
|
326
393
|
retry. Partial or malformed JSONL lines are reported and retried on later sweeps;
|
|
327
394
|
valid events in the same file can still be captured. A single sweep exits with
|
|
328
395
|
status 1 if it encounters errors. The daemon keeps running and retries errors.
|
|
329
396
|
|
|
330
|
-
`watch` prints its first sweep and subsequent imports or errors.
|
|
331
|
-
foreground until SIGINT or SIGTERM.
|
|
397
|
+
`watch` prints its first sweep and subsequent imports or errors. A manual run
|
|
398
|
+
stays in the foreground until SIGINT or SIGTERM. Login capture uses the same
|
|
399
|
+
watcher with an absolute Node executable and absolute sessionmem entry point;
|
|
400
|
+
it does not need a shell, a TTY, or your interactive PATH.
|
|
332
401
|
|
|
333
|
-
|
|
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
|
-
```
|
|
402
|
+
Control login capture explicitly:
|
|
356
403
|
|
|
357
404
|
```bash
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
405
|
+
sessionmem autostart status
|
|
406
|
+
sessionmem autostart disable
|
|
407
|
+
sessionmem autostart enable
|
|
361
408
|
```
|
|
362
409
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
410
|
+
`enable` updates one registration and starts capture immediately. `disable` stops
|
|
411
|
+
the registered watcher and removes its login registration, preserving memories.
|
|
412
|
+
The next `sessionmem install` enables it again. This control affects the login
|
|
413
|
+
watcher; Claude Code hooks and explicit MCP memory tools remain active.
|
|
414
|
+
|
|
415
|
+
Registrations:
|
|
416
|
+
|
|
417
|
+
| OS | Registration |
|
|
418
|
+
| --- | --- |
|
|
419
|
+
| macOS | `~/Library/LaunchAgents/io.sessionmem.watch.plist` |
|
|
420
|
+
| Linux | `~/.config/systemd/user/sessionmem-watch.service` |
|
|
421
|
+
| Windows | Scheduled Task `sessionmem-watch-<user-home-hash>`, for the current user only |
|
|
422
|
+
|
|
423
|
+
Capture output and failures are appended with timestamps to `logs/watch.log`
|
|
424
|
+
beside the selected database (default `~/.sessionmem/logs/watch.log`). The
|
|
425
|
+
watcher also writes `memories.db.watch-checkpoint.json` beside the default store
|
|
426
|
+
after each completed sweep. Doctor checks this checkpoint's age, capture errors,
|
|
427
|
+
and transcript checkpoint count; unchanged history and an empty history are
|
|
428
|
+
healthy when the watcher continues completing sweeps. A checkpoint older than
|
|
429
|
+
two minutes fails the default 5-second watcher check. For a custom polling
|
|
430
|
+
interval, the limit is the greater of two minutes and three intervals.
|
|
431
|
+
|
|
432
|
+
`SESSIONMEM_DB_PATH` selected at install or enable is saved as an absolute
|
|
433
|
+
watcher argument. Run enable with that variable again to switch stores. The
|
|
434
|
+
policy config remains `~/.sessionmem/config.json`. After moving sessionmem or
|
|
435
|
+
changing Node versions, run `sessionmem autostart enable` to update the absolute
|
|
436
|
+
paths. For failures before the watcher can open its log, inspect `launchctl
|
|
437
|
+
print gui/<uid>/io.sessionmem.watch`, `journalctl --user -u
|
|
438
|
+
sessionmem-watch.service`, or Windows Task Scheduler's task history.
|
|
439
|
+
|
|
440
|
+
### Uninstall
|
|
380
441
|
|
|
381
442
|
```bash
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# Stop and disable:
|
|
386
|
-
systemctl --user disable --now sessionmem-watch.service
|
|
443
|
+
sessionmem uninstall --adapter claude-code
|
|
444
|
+
# Also delete the selected memory database:
|
|
445
|
+
sessionmem uninstall --adapter claude-code --purge
|
|
387
446
|
```
|
|
388
447
|
|
|
389
|
-
|
|
448
|
+
Uninstall stops capture and removes the login registration before removing MCP
|
|
449
|
+
configuration, Claude hooks, and agent guidance for the selected host. It keeps
|
|
450
|
+
the database, config and logs by default. Use the same `SESSIONMEM_DB_PATH` as
|
|
451
|
+
install when selecting a custom database to purge. If stopping or unregistering
|
|
452
|
+
the watcher fails, uninstall reports the OS error and leaves the database intact.
|
|
453
|
+
For multiple installed hosts, repeat uninstall with each adapter to remove its
|
|
454
|
+
MCP configuration and guidance.
|
|
390
455
|
|
|
391
456
|
---
|
|
392
457
|
|
|
@@ -511,9 +576,11 @@ sessionmem doctor
|
|
|
511
576
|
sessionmem doctor --adapter claude-code # explicit host override
|
|
512
577
|
```
|
|
513
578
|
|
|
514
|
-
`doctor` reports a separate PASS or FAIL for the memory store, schema,
|
|
515
|
-
|
|
516
|
-
|
|
579
|
+
`doctor` reports a separate PASS or FAIL for the memory store, schema, transcript
|
|
580
|
+
discovery for Claude Code, Codex and Cursor, login auto-start registration,
|
|
581
|
+
registered binary paths, watcher capture checkpoint freshness, hook definitions
|
|
582
|
+
shipped with the current version, configured MCP command, and a canary
|
|
583
|
+
write/recall. It exits nonzero if any check fails and prints fix hints,
|
|
517
584
|
including exact commands for safe repairs. It does not create a missing store or
|
|
518
585
|
apply migrations. Its temporary canary is isolated from your projects and rolled
|
|
519
586
|
back even on failure. The MCP check starts the installed local command and verifies
|
|
@@ -521,7 +588,9 @@ its version, tools, and a stats response; it does not require a running agent.
|
|
|
521
588
|
|
|
522
589
|
Use `sessionmem ping` to initialize a missing store or apply pending schema
|
|
523
590
|
migrations, or `sessionmem install --adapter claude-code` to refresh Claude Code's
|
|
524
|
-
MCP registration and hooks.
|
|
591
|
+
MCP registration and hooks. Use `sessionmem autostart enable` to refresh the
|
|
592
|
+
login service and absolute binary paths; inspect `logs/watch.log` beside the
|
|
593
|
+
database when capture fails. Run repairs with the same `SESSIONMEM_DB_PATH` as
|
|
525
594
|
doctor when using a custom store.
|
|
526
595
|
|
|
527
596
|
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.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, userInfo } from "os";
|
|
5
|
+
import { dirname, isAbsolute, posix, win32 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveDbPath } from "../context.js";
|
|
8
|
+
export const LABEL = "io.sessionmem.watch";
|
|
9
|
+
export const UNIT = "sessionmem-watch.service";
|
|
10
|
+
export const xml = (value) => value.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
11
|
+
const unxml = (value) => value.replace(/&(amp|lt|gt|quot|apos);/g, c => ({ "&": "&", "<": "<", ">": ">", """: '"', "'": "'" })[c]);
|
|
12
|
+
const systemdQuote = (value) => JSON.stringify(value).replace(/%/g, "%%").replace(/\$/g, () => "$$");
|
|
13
|
+
// All generated Windows arguments are quoted. Double backslashes before quotes
|
|
14
|
+
// and at the closing quote according to CommandLineToArgvW rules.
|
|
15
|
+
export const windowsQuote = (value) => `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
16
|
+
export function renderPlist(launch, log, home) {
|
|
17
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
18
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
19
|
+
<plist version="1.0"><dict>
|
|
20
|
+
<key>Label</key><string>${LABEL}</string>
|
|
21
|
+
<key>ProgramArguments</key><array>${[launch.command, ...launch.args].map(v => `<string>${xml(v)}</string>`).join("")}</array>
|
|
22
|
+
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
23
|
+
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
24
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
25
|
+
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
26
|
+
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
27
|
+
</dict></plist>
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
export function renderSystemd(launch, log, home) {
|
|
31
|
+
return `[Unit]
|
|
32
|
+
Description=Capture agent transcripts with sessionmem
|
|
33
|
+
StartLimitIntervalSec=0
|
|
34
|
+
|
|
35
|
+
[Service]
|
|
36
|
+
Type=simple
|
|
37
|
+
ExecStart=${[launch.command, ...launch.args].map(systemdQuote).join(" ")}
|
|
38
|
+
WorkingDirectory=${home.replace(/%/g, "%%")}
|
|
39
|
+
Restart=always
|
|
40
|
+
RestartSec=10
|
|
41
|
+
StandardInput=null
|
|
42
|
+
StandardOutput=append:${log.replace(/%/g, "%%")}
|
|
43
|
+
StandardError=append:${log.replace(/%/g, "%%")}
|
|
44
|
+
|
|
45
|
+
[Install]
|
|
46
|
+
WantedBy=default.target
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
export function renderTask(launch, home, user) {
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
52
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(user)}</UserId></LogonTrigger></Triggers>
|
|
53
|
+
<Principals><Principal id="Author"><UserId>${xml(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
54
|
+
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>255</Count></RestartOnFailure></Settings>
|
|
55
|
+
<Actions Context="Author"><Exec><Command>${xml(launch.command)}</Command><Arguments>${xml(launch.args.map(windowsQuote).join(" "))}</Arguments><WorkingDirectory>${xml(home)}</WorkingDirectory></Exec></Actions>
|
|
56
|
+
</Task>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
export function decodeCommandOutput(value) {
|
|
60
|
+
// schtasks XML may use UTF-16 even when stdout is a pipe.
|
|
61
|
+
return (value[0] === 0xff && value[1] === 0xfe) || value[1] === 0
|
|
62
|
+
? value.toString("utf16le").replace(/^\uFEFF/, "") : value.toString("utf8").replace(/^\uFEFF/, "");
|
|
63
|
+
}
|
|
64
|
+
export const runCommand = (command, args) => {
|
|
65
|
+
const result = spawnSync(command, args, { windowsHide: true, timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
|
|
66
|
+
if (result.error)
|
|
67
|
+
throw new Error(`${command}: ${result.error.message}`);
|
|
68
|
+
return { status: result.status ?? 1, stdout: decodeCommandOutput(result.stdout), stderr: decodeCommandOutput(result.stderr) };
|
|
69
|
+
};
|
|
70
|
+
function checked(run, command, args) {
|
|
71
|
+
const result = run(command, args);
|
|
72
|
+
if (result.status !== 0)
|
|
73
|
+
throw new Error(`${command} ${args.join(" ")}: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function save(path, content) {
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
writeFileSync(path, content, { mode: 0o600 });
|
|
79
|
+
}
|
|
80
|
+
function strings(value) {
|
|
81
|
+
return [...value.matchAll(/<string>([\s\S]*?)<\/string>/g)].map(m => unxml(m[1]));
|
|
82
|
+
}
|
|
83
|
+
function launchFromArgs(args) {
|
|
84
|
+
if (args.length < 3 || args[2] !== "watch")
|
|
85
|
+
throw new Error("Unrecognized watcher command; run sessionmem autostart enable");
|
|
86
|
+
return { command: args[0], args: args.slice(1) };
|
|
87
|
+
}
|
|
88
|
+
export function invalidLaunchPaths(launch) {
|
|
89
|
+
return [launch.command, launch.args[0]].filter((path, index) => {
|
|
90
|
+
try {
|
|
91
|
+
if (!path || (!isAbsolute(path) && !win32.isAbsolute(path)) || !statSync(path).isFile())
|
|
92
|
+
return true;
|
|
93
|
+
accessSync(path, index === 0 && process.platform !== "win32" ? constants.X_OK : constants.R_OK);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function createAutostart(options = {}) {
|
|
102
|
+
const platform = options.platform ?? process.platform;
|
|
103
|
+
const home = options.home ?? homedir();
|
|
104
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
105
|
+
const selectedDb = options.dbPath ?? resolveDbPath();
|
|
106
|
+
if (selectedDb === ":memory:")
|
|
107
|
+
throw new Error("Login capture requires a persistent database");
|
|
108
|
+
const dbPath = paths.resolve(selectedDb);
|
|
109
|
+
const data = paths.dirname(dbPath);
|
|
110
|
+
const log = paths.join(data, "logs", "watch.log");
|
|
111
|
+
// npm's sessionmem binary is a JS entry point. Pin both it and Node, avoiding
|
|
112
|
+
// /usr/bin/env, npm shims and interactive shell/version-manager setup at login.
|
|
113
|
+
const launch = {
|
|
114
|
+
command: paths.resolve(options.nodePath ?? process.execPath),
|
|
115
|
+
args: [paths.resolve(options.cliPath ?? fileURLToPath(new URL("../index.js", import.meta.url))), "watch", "--db-path", dbPath, "--log-file", log],
|
|
116
|
+
};
|
|
117
|
+
const run = options.run ?? runCommand;
|
|
118
|
+
const invoke = (command, args) => checked(run, command, args);
|
|
119
|
+
const prepare = () => {
|
|
120
|
+
if ([home, launch.command, ...launch.args].some(value => /[\r\n\0]/.test(value)))
|
|
121
|
+
throw new Error("Login service paths cannot contain line breaks or NUL");
|
|
122
|
+
const invalid = invalidLaunchPaths(launch);
|
|
123
|
+
if (invalid.length)
|
|
124
|
+
throw new Error(`Cannot read or execute installed binary: ${invalid.join(", ")}`);
|
|
125
|
+
mkdirSync(paths.dirname(log), { recursive: true });
|
|
126
|
+
};
|
|
127
|
+
if (platform === "darwin") {
|
|
128
|
+
const file = paths.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
129
|
+
const domain = `gui/${options.uid ?? userInfo().uid}`;
|
|
130
|
+
const target = `${domain}/${LABEL}`;
|
|
131
|
+
const loaded = () => {
|
|
132
|
+
const result = run("launchctl", ["print", target]);
|
|
133
|
+
if (result.status === 0)
|
|
134
|
+
return true;
|
|
135
|
+
if (/could not find service|no such process/i.test(result.stderr))
|
|
136
|
+
return false;
|
|
137
|
+
throw new Error(`launchctl print: ${result.stderr || result.stdout}`);
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
enable() {
|
|
141
|
+
prepare();
|
|
142
|
+
if (loaded())
|
|
143
|
+
invoke("launchctl", ["bootout", target]);
|
|
144
|
+
save(file, renderPlist(launch, log, home));
|
|
145
|
+
invoke("launchctl", ["enable", target]);
|
|
146
|
+
invoke("launchctl", ["bootstrap", domain, file]);
|
|
147
|
+
},
|
|
148
|
+
disable() {
|
|
149
|
+
if (loaded())
|
|
150
|
+
invoke("launchctl", ["bootout", target]);
|
|
151
|
+
rmSync(file, { force: true });
|
|
152
|
+
},
|
|
153
|
+
status() {
|
|
154
|
+
if (!existsSync(file))
|
|
155
|
+
return { registered: false, detail: `No LaunchAgent (${file})` };
|
|
156
|
+
const content = readFileSync(file, "utf8");
|
|
157
|
+
const args = strings(content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "");
|
|
158
|
+
return { registered: loaded(), launch: launchFromArgs(args), detail: file };
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (platform === "linux") {
|
|
163
|
+
const file = paths.join(home, ".config", "systemd", "user", UNIT);
|
|
164
|
+
return {
|
|
165
|
+
enable() {
|
|
166
|
+
prepare();
|
|
167
|
+
save(file, renderSystemd(launch, log, home));
|
|
168
|
+
invoke("systemctl", ["--user", "daemon-reload"]);
|
|
169
|
+
invoke("systemctl", ["--user", "enable", UNIT]);
|
|
170
|
+
invoke("systemctl", ["--user", "restart", UNIT]);
|
|
171
|
+
},
|
|
172
|
+
disable() {
|
|
173
|
+
if (!existsSync(file))
|
|
174
|
+
return;
|
|
175
|
+
invoke("systemctl", ["--user", "disable", "--now", UNIT]);
|
|
176
|
+
rmSync(file, { force: true });
|
|
177
|
+
invoke("systemctl", ["--user", "daemon-reload"]);
|
|
178
|
+
},
|
|
179
|
+
status() {
|
|
180
|
+
if (!existsSync(file))
|
|
181
|
+
return { registered: false, detail: `No user service (${file})` };
|
|
182
|
+
const value = readFileSync(file, "utf8").match(/^ExecStart=(.+)$/m)?.[1] ?? "";
|
|
183
|
+
const args = [...value.matchAll(/"(?:\\.|[^"\\])*"/g)].map(m => JSON.parse(m[0]).replace(/%%/g, "%").replace(/\$\$/g, "$"));
|
|
184
|
+
const result = run("systemctl", ["--user", "is-enabled", UNIT]);
|
|
185
|
+
if (result.status !== 0 && !["disabled", "masked", "not-found"].includes(result.stdout.trim()))
|
|
186
|
+
throw new Error(`systemctl --user is-enabled: ${result.stderr || result.stdout}`);
|
|
187
|
+
return { registered: result.status === 0, launch: launchFromArgs(args), detail: `${file} (${result.stdout.trim()})` };
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (platform === "win32") {
|
|
192
|
+
// Task names are machine-wide; one stable name per user's home avoids
|
|
193
|
+
// overwriting another user's task on shared Windows machines.
|
|
194
|
+
const name = `sessionmem-watch-${createHash("sha256").update(home.toLowerCase()).digest("hex").slice(0, 12)}`;
|
|
195
|
+
const file = paths.join(data, "autostart-task.xml");
|
|
196
|
+
const query = () => {
|
|
197
|
+
const result = run("schtasks.exe", ["/Query", "/TN", name, "/XML", "/HRESULT"]);
|
|
198
|
+
if ((result.status >>> 0) === 0x80070002)
|
|
199
|
+
return undefined; // ERROR_FILE_NOT_FOUND
|
|
200
|
+
if (result.status !== 0)
|
|
201
|
+
throw new Error(`schtasks /Query: ${result.stderr || result.stdout || result.status}`);
|
|
202
|
+
return result.stdout;
|
|
203
|
+
};
|
|
204
|
+
const stop = () => {
|
|
205
|
+
const result = run("schtasks.exe", ["/End", "/TN", name, "/HRESULT"]);
|
|
206
|
+
// Numeric HRESULTs avoid matching localized Windows error messages.
|
|
207
|
+
if (result.status !== 0 && (result.status >>> 0) !== 0x8004130b)
|
|
208
|
+
throw new Error(`schtasks /End: ${result.stderr || result.stdout || result.status}`);
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
enable() {
|
|
212
|
+
prepare();
|
|
213
|
+
const identity = invoke("whoami.exe", ["/USER", "/FO", "CSV", "/NH"]).stdout;
|
|
214
|
+
const user = identity.match(/"(S-1-[0-9-]+)"/)?.[1];
|
|
215
|
+
if (!user)
|
|
216
|
+
throw new Error("whoami returned no user SID for the logon task");
|
|
217
|
+
if (query() !== undefined)
|
|
218
|
+
stop();
|
|
219
|
+
save(file, renderTask(launch, home, user));
|
|
220
|
+
invoke("schtasks.exe", ["/Create", "/TN", name, "/XML", file, "/F"]);
|
|
221
|
+
invoke("schtasks.exe", ["/Run", "/TN", name]);
|
|
222
|
+
},
|
|
223
|
+
disable() {
|
|
224
|
+
if (query() !== undefined) {
|
|
225
|
+
stop();
|
|
226
|
+
invoke("schtasks.exe", ["/Delete", "/TN", name, "/F"]);
|
|
227
|
+
}
|
|
228
|
+
rmSync(file, { force: true });
|
|
229
|
+
},
|
|
230
|
+
status() {
|
|
231
|
+
const content = query();
|
|
232
|
+
if (content === undefined)
|
|
233
|
+
return { registered: false, detail: `No Scheduled Task (${name})` };
|
|
234
|
+
const command = unxml(content.match(/<Command>([\s\S]*?)<\/Command>/)?.[1] ?? "");
|
|
235
|
+
const args = unxml(content.match(/<Arguments>([\s\S]*?)<\/Arguments>/)?.[1] ?? "");
|
|
236
|
+
// The first argument is an absolute .js path, which cannot contain quotes
|
|
237
|
+
// on Windows. The remaining arguments are only needed for display.
|
|
238
|
+
const cli = args.match(/^"([^"]+)"\s+"watch"(?:\s|$)/)?.[1];
|
|
239
|
+
if (!command || !cli)
|
|
240
|
+
throw new Error("Unrecognized Scheduled Task action; run sessionmem autostart enable");
|
|
241
|
+
return { registered: !/<Enabled>false<\/Enabled>/.test(content), launch: { command, args: [cli, "watch"] }, detail: name };
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Login auto-start is unsupported on ${platform}`);
|
|
246
|
+
}
|