sessionmem 1.6.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 CHANGED
@@ -5,6 +5,24 @@ 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
+
8
26
  ## [1.6.0] - 2026-10-18
9
27
 
10
28
  ### Added
@@ -121,7 +139,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
121
139
  errors and findings fail the job; added scanner dependency updates and
122
140
  Dependabot cooldowns. ([#32])
123
141
 
142
+ [1.7.0]: https://github.com/catfish-1234/sessionmem/compare/v1.6.0...v1.7.0
124
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
125
146
  [1.5.0]: https://github.com/catfish-1234/sessionmem/compare/679b8954f0b49f197985c40da47f181c00060cd9...v1.5.0
126
147
  [#43]: https://github.com/catfish-1234/sessionmem/pull/43
127
148
  [#44]: https://github.com/catfish-1234/sessionmem/pull/44
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, for each selected adapter:
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
- - 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.
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, register sessionmem with each, and write default config; the flag selects one host. |
213
- | `sessionmem doctor [--adapter <name>]` | Check the store, schema, transcript discovery, hooks, MCP wiring, and canary recall offline; print PASS/FAIL and repair hints. |
214
- | `sessionmem uninstall [--adapter <name>] [--purge]` | Remove sessionmem from the host. `--purge` also deletes the local database. |
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. |
@@ -297,7 +318,7 @@ 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
- Run a scan to capture Claude Code, Codex CLI, and Cursor sessions from before sessionmem was installed:
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
@@ -373,66 +394,64 @@ retry. Partial or malformed JSONL lines are reported and retried on later sweeps
373
394
  valid events in the same file can still be captured. A single sweep exits with
374
395
  status 1 if it encounters errors. The daemon keeps running and retries errors.
375
396
 
376
- `watch` prints its first sweep and subsequent imports or errors. It runs in the
377
- foreground until SIGINT or SIGTERM. For a background process:
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.
378
401
 
379
- ```bash
380
- nohup sessionmem watch > sessionmem-watch.log 2>&1 &
381
- ```
382
-
383
- For macOS launchd, save this as
384
- `~/Library/LaunchAgents/io.sessionmem.watch.plist`. Replace the executable path
385
- with the output of `command -v sessionmem`, and adjust PATH for your Node install:
386
-
387
- ```xml
388
- <?xml version="1.0" encoding="UTF-8"?>
389
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
390
- <plist version="1.0">
391
- <dict>
392
- <key>Label</key><string>io.sessionmem.watch</string>
393
- <key>ProgramArguments</key>
394
- <array><string>/opt/homebrew/bin/sessionmem</string><string>watch</string></array>
395
- <key>EnvironmentVariables</key>
396
- <dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
397
- <key>RunAtLoad</key><true/>
398
- <key>KeepAlive</key><true/>
399
- </dict>
400
- </plist>
401
- ```
402
+ Control login capture explicitly:
402
403
 
403
404
  ```bash
404
- launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/io.sessionmem.watch.plist
405
- # Stop and unload:
406
- launchctl bootout "gui/$(id -u)/io.sessionmem.watch"
405
+ sessionmem autostart status
406
+ sessionmem autostart disable
407
+ sessionmem autostart enable
407
408
  ```
408
409
 
409
- For a Linux systemd user service, save this as
410
- `~/.config/systemd/user/sessionmem-watch.service`. Replace the executable path
411
- and adjust PATH for your Node install:
412
-
413
- ```ini
414
- [Unit]
415
- Description=Capture Claude Code, Codex, and Cursor transcripts with sessionmem
416
-
417
- [Service]
418
- ExecStart=/usr/local/bin/sessionmem watch
419
- Environment=PATH=/usr/local/bin:/usr/bin:/bin
420
- Restart=on-failure
421
- RestartSec=5
422
-
423
- [Install]
424
- WantedBy=default.target
425
- ```
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
426
441
 
427
442
  ```bash
428
- systemctl --user daemon-reload
429
- systemctl --user enable --now sessionmem-watch.service
430
- journalctl --user -u sessionmem-watch.service -f
431
- # Stop and disable:
432
- 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
433
446
  ```
434
447
 
435
- These are optional examples; `sessionmem install` does not register a daemon.
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.
436
455
 
437
456
  ---
438
457
 
@@ -558,9 +577,10 @@ sessionmem doctor --adapter claude-code # explicit host override
558
577
  ```
559
578
 
560
579
  `doctor` reports a separate PASS or FAIL for the memory store, schema, transcript
561
- discovery for Claude Code and Codex, hook definitions shipped with the current
562
- version, configured MCP command, and a
563
- canary write/recall. It exits nonzero if any check fails and prints fix hints,
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,
564
584
  including exact commands for safe repairs. It does not create a missing store or
565
585
  apply migrations. Its temporary canary is isolated from your projects and rolled
566
586
  back even on failure. The MCP check starts the installed local command and verifies
@@ -568,7 +588,9 @@ its version, tools, and a stats response; it does not require a running agent.
568
588
 
569
589
  Use `sessionmem ping` to initialize a missing store or apply pending schema
570
590
  migrations, or `sessionmem install --adapter claude-code` to refresh Claude Code's
571
- MCP registration and hooks. Run repairs with the same `SESSIONMEM_DB_PATH` as
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
572
594
  doctor when using a custom store.
573
595
 
574
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 => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
11
+ const unxml = (value) => value.replace(/&(amp|lt|gt|quot|apos);/g, c => ({ "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&apos;": "'" })[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
+ }
@@ -0,0 +1,24 @@
1
+ import { createAutostart, invalidLaunchPaths } from "../autostart/index.js";
2
+ export function autostartCommand(action, service) {
3
+ if (!["enable", "disable", "status"].includes(action))
4
+ throw new Error("Use sessionmem autostart enable, disable, or status");
5
+ const autostart = service ?? createAutostart();
6
+ if (action === "status") {
7
+ const status = autostart.status();
8
+ console.log(`Auto-start ${status.registered ? "enabled" : "disabled"}: ${status.detail}`);
9
+ if (status.launch) {
10
+ console.log(`Binary: ${status.launch.command}\nEntry point: ${status.launch.args[0]}`);
11
+ const invalid = invalidLaunchPaths(status.launch);
12
+ if (invalid.length) {
13
+ console.error(`Invalid binary path: ${invalid.join(", ")}. Run sessionmem autostart enable.`);
14
+ process.exitCode = 1;
15
+ }
16
+ }
17
+ return;
18
+ }
19
+ if (action === "enable")
20
+ autostart.enable();
21
+ else
22
+ autostart.disable();
23
+ console.log(`Auto-start ${action === "enable" ? "enabled and started" : "disabled and stopped"}`);
24
+ }
@@ -42,6 +42,7 @@ const CONFIG_KEYS = {
42
42
  retentionDays: { field: "retentionDays", coerce: coerceRetentionDays },
43
43
  redactionEnabled: { field: "redactionEnabled", coerce: coerceBool },
44
44
  injectionCap: { field: "injectionCap", coerce: coerceInjectionCap },
45
+ semanticSearch: { field: "semanticSearch", coerce: coerceBool },
45
46
  };
46
47
  function resolvePath(options) {
47
48
  return options?.configPath ?? configFilePath();
@@ -1,3 +1,5 @@
1
+ import { createAutostart, invalidLaunchPaths } from "../autostart/index.js";
2
+ import { inspectWatchCheckpoint } from "../watchCheckpoint.js";
1
3
  import BetterSqlite3 from "better-sqlite3";
2
4
  import { randomUUID } from "node:crypto";
3
5
  import { createRequire } from "node:module";
@@ -54,6 +56,23 @@ export async function doctorCommand(options, overrides = {}) {
54
56
  report("Agent hosts", false, message(err), "Install a supported host or select one with sessionmem doctor --adapter <name>");
55
57
  }
56
58
  const dbPath = resolveDbPath(overrides.dbPath);
59
+ const captureFix = "Run sessionmem autostart enable; inspect logs/watch.log beside the database and OS service logs, then run sessionmem doctor";
60
+ try {
61
+ const registration = createAutostart({ dbPath }).status();
62
+ report("Auto-start registration", registration.registered, registration.detail, registration.registered ? undefined : captureFix);
63
+ const invalid = registration.launch ? invalidLaunchPaths(registration.launch) : ["No registered binary"];
64
+ report("Auto-start binary", invalid.length === 0, invalid.length ? invalid.join(", ") : "Registered Node binary and sessionmem entry point are readable", invalid.length ? "Reinstall sessionmem if needed, then run sessionmem autostart enable" : undefined);
65
+ }
66
+ catch (err) {
67
+ report("Auto-start registration", false, message(err), captureFix);
68
+ report("Auto-start binary", false, "Cannot inspect the registration", captureFix);
69
+ }
70
+ try {
71
+ report("Watcher capture", true, inspectWatchCheckpoint(dbPath));
72
+ }
73
+ catch (err) {
74
+ report("Watcher capture", false, message(err), captureFix);
75
+ }
57
76
  for (const host of TRANSCRIPT_HOSTS) {
58
77
  try {
59
78
  report(`${host} transcripts`, true, await inspectTranscriptDiscovery(host));
@@ -1,3 +1,4 @@
1
+ import { createAutostart } from "../autostart/index.js";
1
2
  import { existsSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { homedir } from "os";
@@ -87,6 +88,15 @@ export async function installCommand(options, contextOverrides) {
87
88
  console.log("✓ Claude Code: SessionStart, SessionEnd, PostToolUse hooks configured (~/.claude/settings.json)");
88
89
  }
89
90
  }
91
+ try {
92
+ createAutostart({ dbPath: dbPath }).enable();
93
+ console.log("Auto-start enabled and started; capture logs are in logs/watch.log beside the database");
94
+ }
95
+ catch (err) {
96
+ console.error(`Auto-start setup failed: ${err instanceof Error ? err.message : String(err)}`);
97
+ console.error("Fix the OS login service error, then run sessionmem autostart enable. Run sessionmem doctor to check capture.");
98
+ failed = true;
99
+ }
90
100
  if (failed)
91
101
  process.exit(1);
92
102
  console.log("✓ sessionmem ready — restart your editor/agent so it picks up the MCP server");
@@ -1,3 +1,4 @@
1
+ import { createAutostart } from "../autostart/index.js";
1
2
  import { existsSync, rmSync } from "fs";
2
3
  import { join } from "path";
3
4
  import { homedir } from "os";
@@ -28,6 +29,9 @@ export async function uninstallCommand(options = {}) {
28
29
  console.error(err instanceof Error ? err.message : String(err));
29
30
  process.exit(1);
30
31
  }
32
+ // Stop capture before removing integrations or purging an open database.
33
+ createAutostart({ dbPath: options.dbPath }).disable();
34
+ console.log("Auto-start removed and watcher stopped");
31
35
  if (!adapter.uninstall) {
32
36
  console.error(`${adapter.name} does not support automated uninstall. Remove sessionmem from your MCP config manually.`);
33
37
  process.exit(1);
@@ -1,3 +1,5 @@
1
+ import { openWatchLog } from "../watchLog.js";
2
+ import { saveWatchCheckpoint } from "../watchCheckpoint.js";
1
3
  import { setTimeout } from "node:timers/promises";
2
4
  import { createCliContext } from "../context.js";
3
5
  import { printSweepSummary, sweepTranscripts } from "../transcriptSweep.js";
@@ -6,16 +8,20 @@ export async function watchCommand(options = {}, ctx) {
6
8
  if (!Number.isFinite(interval) || interval <= 0 || interval > 86400) {
7
9
  throw new Error("--interval must be a number greater than 0 and at most 86400 seconds");
8
10
  }
9
- const context = ctx ?? createCliContext();
11
+ const closeLog = openWatchLog(options.logFile);
12
+ let context;
10
13
  const controller = new AbortController();
11
14
  const stop = () => controller.abort();
12
15
  process.once("SIGINT", stop);
13
16
  process.once("SIGTERM", stop);
14
17
  let firstSweep = true;
15
18
  try {
19
+ console.log(`sessionmem watch starting (pid ${process.pid}, interval ${interval}s)`);
20
+ context = ctx ?? createCliContext({ dbPath: options.dbPath });
16
21
  do {
17
22
  try {
18
23
  const summary = await sweepTranscripts(context, options);
24
+ saveWatchCheckpoint(context, summary, interval);
19
25
  if (firstSweep || options.once || summary.sessionsImported || summary.errors) {
20
26
  printSweepSummary("watch", summary);
21
27
  }
@@ -39,10 +45,16 @@ export async function watchCommand(options = {}, ctx) {
39
45
  }
40
46
  } while (!controller.signal.aborted);
41
47
  }
48
+ catch (error) {
49
+ console.error(`[sessionmem] watch failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
50
+ throw error;
51
+ }
42
52
  finally {
43
53
  process.removeListener("SIGINT", stop);
44
54
  process.removeListener("SIGTERM", stop);
45
55
  if (!ctx)
46
- context.db.close();
56
+ context?.db.close();
57
+ console.log("sessionmem watch stopped");
58
+ closeLog();
47
59
  }
48
60
  }