claude-mem-lite 6.3.0 → 6.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.
package/mem-cli.mjs CHANGED
@@ -799,7 +799,7 @@ function renderObsRows(db, ids, requestedFields) {
799
799
  }
800
800
  parts.push(lines.join('\n'));
801
801
  }
802
- return { text: parts.join('\n\n'), count: rows.length };
802
+ return { text: parts.join('\n\n'), count: rows.length, foundIds: rows.map((r) => r.id) };
803
803
  }
804
804
 
805
805
  function renderSessionRows(db, ids) {
@@ -819,7 +819,7 @@ function renderSessionRows(db, ids) {
819
819
  }
820
820
  parts.push(lines.join('\n'));
821
821
  }
822
- return { text: parts.join('\n\n'), count: rows.length };
822
+ return { text: parts.join('\n\n'), count: rows.length, foundIds: rows.map((r) => r.id) };
823
823
  }
824
824
 
825
825
  // The CLI's established labels for the prompt/event detail faces. Sharing the FIELD SET
@@ -850,7 +850,7 @@ function renderPromptRows(db, ids) {
850
850
  }
851
851
  parts.push(lines.join('\n'));
852
852
  }
853
- return { text: parts.join('\n\n'), count: rows.length };
853
+ return { text: parts.join('\n\n'), count: rows.length, foundIds: rows.map((r) => r.id) };
854
854
  }
855
855
 
856
856
  function renderEventRows(db, ids) {
@@ -867,7 +867,7 @@ function renderEventRows(db, ids) {
867
867
  }
868
868
  parts.push(lines.join('\n'));
869
869
  }
870
- return { text: parts.join('\n\n'), count: rows.length };
870
+ return { text: parts.join('\n\n'), count: rows.length, foundIds: rows.map((r) => r.id) };
871
871
  }
872
872
 
873
873
  function cmdGet(db, args) {
@@ -954,34 +954,21 @@ function cmdGet(db, args) {
954
954
  );
955
955
  }
956
956
  }
957
- if (bySrc.obs.length > 0) {
958
- const s = renderObsRows(db, bySrc.obs, requestedFields);
959
- if (s) {
960
- sections.push(s.text);
961
- totalFound += s.count;
957
+ // Per-bucket misses are collected as they happen, not inferred afterwards, so the note
958
+ // below can name an id with the prefix the caller typed.
959
+ const missing = [];
960
+ const takeSection = (result, ids, prefix) => {
961
+ if (result) {
962
+ sections.push(result.text);
963
+ totalFound += result.count;
962
964
  }
963
- }
964
- if (bySrc.session.length > 0) {
965
- const s = renderSessionRows(db, bySrc.session);
966
- if (s) {
967
- sections.push(s.text);
968
- totalFound += s.count;
969
- }
970
- }
971
- if (bySrc.prompt.length > 0) {
972
- const s = renderPromptRows(db, bySrc.prompt);
973
- if (s) {
974
- sections.push(s.text);
975
- totalFound += s.count;
976
- }
977
- }
978
- if (bySrc.event.length > 0) {
979
- const s = renderEventRows(db, bySrc.event);
980
- if (s) {
981
- sections.push(s.text);
982
- totalFound += s.count;
983
- }
984
- }
965
+ const found = new Set(result?.foundIds ?? []);
966
+ missing.push(...ids.filter((id) => !found.has(id)).map((id) => `${prefix}${id}`));
967
+ };
968
+ if (bySrc.obs.length > 0) takeSection(renderObsRows(db, bySrc.obs, requestedFields), bySrc.obs, '#');
969
+ if (bySrc.session.length > 0) takeSection(renderSessionRows(db, bySrc.session), bySrc.session, 'S#');
970
+ if (bySrc.prompt.length > 0) takeSection(renderPromptRows(db, bySrc.prompt), bySrc.prompt, 'P#');
971
+ if (bySrc.event.length > 0) takeSection(renderEventRows(db, bySrc.event), bySrc.event, 'E#');
985
972
 
986
973
  if (totalFound === 0) {
987
974
  // Deferred-only request that found nothing — the source-probe below is
@@ -1011,6 +998,15 @@ function cmdGet(db, args) {
1011
998
  }
1012
999
 
1013
1000
  out(sections.join('\n\n'));
1001
+
1002
+ // Partial misses were silent here while every sibling reported them: mem_get appends
1003
+ // "Note: ID(s) … not found.", CLI `delete` prints "not found and will be skipped", and the
1004
+ // total-miss branch above fails loudly. So `get 12,13,14` returning two records read as a
1005
+ // complete answer, and nothing said which id was absent. stderr, not stdout: `get` output
1006
+ // is piped, and the D#/unparseable notices above already use that channel.
1007
+ if (missing.length > 0) {
1008
+ process.stderr.write(`[mem] Note: ID(s) ${missing.join(', ')} not found\n`);
1009
+ }
1014
1010
  }
1015
1011
 
1016
1012
  function cmdTimeline(db, args) {
@@ -1193,7 +1189,7 @@ function cmdSave(db, args) {
1193
1189
  if (text === null) return;
1194
1190
  if (!text.trim()) {
1195
1191
  fail(
1196
- '[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42] [--supersedes 8754,E#10524] — content may also be passed via --text/--content "<text>"',
1192
+ '[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42] [--supersedes 8754,E#10524] [--force] — content may also be passed via --text/--content "<text>"',
1197
1193
  );
1198
1194
  return;
1199
1195
  }
@@ -1306,6 +1302,9 @@ function cmdSave(db, args) {
1306
1302
  files: saveFiles,
1307
1303
  lesson_learned: rawLesson,
1308
1304
  supersedes: supersedesIds || undefined,
1305
+ // Opt-in only. The 5-minute near-duplicate window stays the default; this is for a
1306
+ // caller who read "Skipped: similar to existing #N" and means the second one.
1307
+ force: flags.force === true || flags.force === 'true',
1309
1308
  },
1310
1309
  { closesTokens, project },
1311
1310
  ));
@@ -3142,6 +3141,8 @@ Commands:
3142
3141
  --project P Project name
3143
3142
  --files f1,f2 Comma-separated file paths
3144
3143
  --lesson T Lesson learned (≤500 chars; alias: --lesson-learned)
3144
+ --force Save even if it looks like a near-duplicate of something
3145
+ saved in the last 5 minutes (that guard is on by default)
3145
3146
  --closes-deferred 1,D#42 Close deferred items in same transaction
3146
3147
 
3147
3148
  defer <action> First-class deferred work (v2.70+)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.3.0",
3
+ "version": "6.5.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "6.3.0",
9
+ "version": "6.5.0",
10
10
  "os": [
11
11
  "darwin",
12
12
  "linux",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.3.0",
3
+ "version": "6.5.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -24,6 +24,7 @@
24
24
  "format": "prettier --write \"**/*.{mjs,js}\"",
25
25
  "format:check": "prettier --check \"**/*.{mjs,js}\"",
26
26
  "dead-code": "knip",
27
+ "validate:manifests": "node scripts/validate-plugin-manifests.mjs",
27
28
  "audit:metrics": "node scripts/audit-metrics.mjs --md",
28
29
  "audit:baseline": "node scripts/audit-metrics.mjs --run-tests --md",
29
30
  "test": "vitest run",
@@ -89,6 +90,7 @@
89
90
  "lib/err-sampler.mjs",
90
91
  "lib/hook-telemetry.mjs",
91
92
  "lib/resolve-data-dir.mjs",
93
+ "lib/data-paths.mjs",
92
94
  "lib/export-columns.mjs",
93
95
  "lib/file-intel.mjs",
94
96
  "lib/reread-guard.mjs",
@@ -103,6 +105,8 @@
103
105
  "lib/binding-probe.mjs",
104
106
  "lib/install-shape.mjs",
105
107
  "lib/schema-skew.mjs",
108
+ "lib/db-unusable.mjs",
109
+ "lib/record-once.mjs",
106
110
  "lib/hook-stdin.mjs",
107
111
  "lib/plugin-key.mjs",
108
112
  "lib/hook-stdout.mjs",
package/schema.mjs CHANGED
@@ -7,22 +7,21 @@ import { homedir } from 'os';
7
7
  import { join } from 'path';
8
8
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, chmodSync } from 'fs';
9
9
  import { OBS_FTS_COLUMNS, debugCatch } from './utils.mjs';
10
- import { resolveDataDir } from './lib/resolve-data-dir.mjs';
11
10
  // Imported, never re-declared: a hand-copied marker string is this repo's twin-drift
12
11
  // class, and every consumer of the forward-incompat throw keys on this exact value.
13
12
  // schema-skew.mjs imports nothing local, so this closes no cycle.
14
13
  import { SCHEMA_SKEW_CODE } from './lib/schema-skew.mjs';
15
-
16
- // DATA location — DB, managed resources, registry DB, runtime/. Honors
17
- // CLAUDE_MEM_DIR so users can relocate state to a larger/faster volume.
18
- export const DB_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
19
- export const DB_PATH = join(DB_DIR, 'claude-mem-lite.db');
20
- // CODE / install location — server.mjs, hook.mjs, cli.mjs, package.json live
21
- // here. ALWAYS homedir-rooted: Claude Code's settings.json + MCP registration
22
- // bake ABSOLUTE paths to server.mjs/hooks, so the code must NOT follow the
23
- // CLAUDE_MEM_DIR relocation env var (mirrors install.mjs INSTALL_DIR). Equals
24
- // DB_DIR when CLAUDE_MEM_DIR is unset — the common, non-relocated case.
25
- export const CODE_DIR = join(homedir(), '.claude-mem-lite');
14
+ import { isFtsCorruptionError } from './lib/db-unusable.mjs';
15
+
16
+ // The three location constants now live in lib/data-paths.mjs — a leaf module with no
17
+ // package imports — and are re-exported here so every existing importer is unchanged.
18
+ // This file statically imports better-sqlite3, so holding a path constant here made the
19
+ // native driver a load-time dependency of anything that wanted one; that is what put the
20
+ // Ed25519-verified repair path out of reach on a tree with no node_modules. Imported AND
21
+ // re-exported (not `export … from`) because schema.mjs uses DB_DIR / DB_PATH itself.
22
+ // See lib/data-paths.mjs and tests/repair-path-no-native-dep.test.mjs.
23
+ import { DB_DIR, DB_PATH, CODE_DIR } from './lib/data-paths.mjs';
24
+ export { DB_DIR, DB_PATH, CODE_DIR };
26
25
 
27
26
  // Increment when schema changes (tables, columns, indexes, FTS, migrations)
28
27
  //
@@ -1269,16 +1268,10 @@ export function isDbCorruptionError(err) {
1269
1268
  return /SQLITE_CORRUPT|SQLITE_NOTADB|malformed|not a database|disk image/i.test(text);
1270
1269
  }
1271
1270
 
1272
- /**
1273
- * Whether an error is a damaged FTS5 INDEX rather than a damaged database file. SQLite
1274
- * reports these as SQLITE_CORRUPT_VTAB, whose message text is the same
1275
- * "database disk image is malformed" the file-level faults use — so the code is the only
1276
- * thing that separates them, and matching on message alone is what conflated them.
1277
- * The remedy is rebuildFTS, which the FTS content lets us do losslessly.
1278
- */
1279
- export function isFtsCorruptionError(err) {
1280
- return /SQLITE_CORRUPT_VTAB/i.test(`${err?.code || ''}`);
1281
- }
1271
+ // Definition moved to lib/db-unusable.mjs (v6.5.0) so the hook path's own classifier and this
1272
+ // one cannot drift; re-exported here because four call sites and a test import it from
1273
+ // schema.mjs. Same pattern as DB_DIR / DB_PATH above.
1274
+ export { isFtsCorruptionError };
1282
1275
 
1283
1276
  /**
1284
1277
  * ensureDb with corruption-gated WAL recovery. Was inlined in server.mjs only,
@@ -31,6 +31,7 @@ import { spawn, spawnSync } from 'node:child_process';
31
31
  import { dirname, join, isAbsolute } from 'node:path';
32
32
  import { fileURLToPath, pathToFileURL } from 'node:url';
33
33
  import { homedir } from 'node:os';
34
+ import { createHash } from 'node:crypto';
34
35
 
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
37
  const INSTALL_DIR = join(__dirname, '..');
@@ -65,7 +66,16 @@ const DATA_DIR = MEM_DIR && isAbsolute(MEM_DIR) ? MEM_DIR : join(homedir(), '.cl
65
66
  // `tests/runtime-dir-single-home.test.mjs` asserts this file still carries the rule.
66
67
  const RUNTIME_DIR = join(DATA_DIR, 'runtime'); // runtime-dir:stays-put — serves swap-in-progress only; HOOK_RUNTIME_DIR carries the hook markers
67
68
  const HOOK_RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || RUNTIME_DIR;
68
- const HEAL_MARKER = join(HOOK_RUNTIME_DIR, 'hook-launcher-lastheal');
69
+ // Per CODE HOME, not per machine. The runtime dir is data-dir-relative and therefore SHARED
70
+ // by every install shape on this box — a plugin cache version, a managed ~/.claude-mem-lite,
71
+ // a dev checkout. With one global name, a failed heal attempt for root A silenced root B's
72
+ // heal for the next six hours, and the two are repaired by different commands. The suffix is
73
+ // derived from INSTALL_DIR, which is what `cli.mjs repair` below actually acts on.
74
+ //
75
+ // node:crypto only — the pure-`node:` charter above still holds. No migration: a pre-6.4.0
76
+ // unsuffixed marker is simply ignored, which costs at most one extra heal attempt once.
77
+ const INSTALL_KEY = createHash('sha256').update(INSTALL_DIR).digest('hex').slice(0, 12);
78
+ const HEAL_MARKER = join(HOOK_RUNTIME_DIR, `hook-launcher-lastheal-${INSTALL_KEY}`);
69
79
  const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
70
80
  // Observable breakage state: written when the launcher degrades a broken install
71
81
  // to exit 0, cleared once the install is confirmed healthy. `doctor` reads it so
@@ -89,6 +99,14 @@ const BROKEN_MARKER = join(HOOK_RUNTIME_DIR, 'hook-launcher-broken');
89
99
  // SESSION-START only — never on the per-tool hot path, where an npm run would
90
100
  // stall the user's edit.
91
101
  // Marker dir: HOOK_RUNTIME_DIR (see its definition above for why it is override-aware).
102
+ // BOTH stay machine-wide, unlike HEAL_MARKER above, and the difference is the SUBJECT of the
103
+ // repair. HEAL_MARKER gates `cli.mjs repair`, which fixes THIS install dir. This pair gates
104
+ // the native-binding rebuild, and `install.mjs rebuildBinding()` iterates
105
+ // `shape.runtimeRoots` — "Every code home on this machine, not just the one this file sits
106
+ // in", as its own comment puts it, which is also what install.mjs tells the user. Keying it
107
+ // per code home would let N homes each spawn npm within one 6h window for a repair that
108
+ // already covered all of them. A first cut of this change did exactly that; pre-ship review
109
+ // caught that the justifying comment was false for the command it gates.
92
110
  const NB_BROKEN_MARKER = join(HOOK_RUNTIME_DIR, 'native-binding-broken');
93
111
  const NB_HEAL_MARKER = join(HOOK_RUNTIME_DIR, 'native-binding-lastheal');
94
112
  // Literal, not imported: the pure-`node:` charter above forbids importing lib/
@@ -111,10 +129,13 @@ const CLI_REPAIR = `node ${join(INSTALL_DIR, 'cli.mjs')} repair`;
111
129
 
112
130
  // Last-resort recovery string for users whose `cli.mjs repair` path
113
131
  // itself failed (install.mjs missing / repair errored / retry still drifting).
114
- // Duplicated in install.mjs::repair() catch; both are reachable when local
115
- // scripts are broken, so neither can import a shared constant.
132
+ // Duplicated from install.mjs::MANUAL_TARBALL_FALLBACK — the pure-`node:` charter above
133
+ // forbids importing it, and this path is reachable exactly when local scripts are broken.
134
+ // Pinned to that constant by tests/manual-fallback-sync.test.mjs, which also fails if a
135
+ // fifth surface starts hardcoding its own. Resolves the latest RELEASE tag rather than
136
+ // `/tarball` (the default branch, i.e. unreleased WIP).
116
137
  const TARBALL_FALLBACK =
117
- 'T=$(mktemp -d) && curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/tarball | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install';
138
+ 'T=$(mktemp -d) && U=$(curl -sL https://api.github.com/repos/sdsrss/claude-mem-lite/releases/latest | grep -o \'"tarball_url"[^,]*\' | cut -d\'"\' -f4) && curl -sL "$U" | tar xz -C "$T" --strip-components=1 && node "$T/install.mjs" install';
118
139
 
119
140
  const [, , entryArg, ...rest] = process.argv;
120
141
  if (!entryArg) {
package/server.mjs CHANGED
@@ -7,7 +7,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { truncate, typeIcon, inferProject, fmtDate, debugLog, debugCatch } from './utils.mjs';
9
9
  import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
10
- import { ensureDbWithWalRecovery, DB_PATH, DB_DIR } from './schema.mjs';
10
+ import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
11
+ // schema.mjs already imports this module for SCHEMA_SKEW_CODE, so it is in the graph before
12
+ // the DB is touched — a static import here adds no cold-start cost.
13
+ import {
14
+ isSchemaSkewError,
15
+ schemaSkewFromError,
16
+ schemaSkewRemedy,
17
+ formatSchemaSkewNotice,
18
+ } from './lib/schema-skew.mjs';
11
19
  import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
12
20
  import { searchObservationsHybrid } from './search-engine.mjs';
13
21
  import {
@@ -153,6 +161,54 @@ try {
153
161
  info: (m) => debugLog('INFO', 'server', m),
154
162
  });
155
163
  } catch (err) {
164
+ // Schema skew gets the same treatment as the CLI (mem-cli.mjs) and for the same reason:
165
+ // the raw message ends in `npm i -g claude-mem-lite@latest`, which repairs nothing on the
166
+ // plugin-cache install that actually hits this.
167
+ //
168
+ // This branch has to live HERE rather than in scripts/launch.mjs, which already knows how
169
+ // to format the notice. server.mjs opens the DB while it is being imported, so the throw
170
+ // is caught by this block and `process.exit(1)` runs before the launcher's own catch can
171
+ // ever see it — the launcher's handler is unreachable for this one error. Re-throwing
172
+ // instead would fix the plugin path and break the npm one, because install.mjs registers
173
+ // `claude mcp add ... -- node <SERVER_PATH>`, launching this file with no launcher above
174
+ // it to catch anything. Emitting from here covers both, and the launcher's copy stays as
175
+ // the handler for failures that happen before this module is reached.
176
+ if (isSchemaSkewError(err)) {
177
+ const skew = schemaSkewFromError(err) || { dbVersion: null, binaryVersion: null };
178
+ let shape = { managed: false, activePluginVersion: null };
179
+ let dev = false;
180
+ try {
181
+ const [shapeMod, updateMod] = await Promise.all([
182
+ import('./lib/install-shape.mjs'),
183
+ import('./hook-update.mjs'),
184
+ ]);
185
+ // CODE_DIR, not DB_DIR: `hasManagedCodeInstall` looks for server.mjs + hook.mjs inside
186
+ // whatever it is handed, and DB_DIR follows CLAUDE_MEM_DIR. Handing it the relocated DATA
187
+ // dir reports `managed: false` for a machine that has a managed install, so the remedy
188
+ // came out as "could not identify this install" or, with any plugin cache present, the
189
+ // plugin commands printed under a line naming ~/.claude-mem-lite. The three sibling call
190
+ // sites (mem-cli.mjs, hook.mjs, scripts/launch.mjs) all pass the CODE dir.
191
+ shape = shapeMod.detectInstallShape({ installDir: CODE_DIR });
192
+ dev = updateMod.isDevMode();
193
+ } catch {
194
+ /* shape unknown → schemaSkewRemedy answers 'unknown', which is its job */
195
+ }
196
+ const codeHome = process.env.CLAUDE_PLUGIN_ROOT || import.meta.dirname;
197
+ console.error(
198
+ formatSchemaSkewNotice({
199
+ dbVersion: skew.dbVersion,
200
+ binaryVersion: skew.binaryVersion,
201
+ remedy: schemaSkewRemedy({
202
+ managed: shape.managed,
203
+ activePluginVersion: shape.activePluginVersion,
204
+ dev,
205
+ root: codeHome,
206
+ }),
207
+ codeHome,
208
+ }),
209
+ );
210
+ process.exit(1);
211
+ }
156
212
  // Fatal: log and exit with descriptive message (Claude Code shows stderr)
157
213
  console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
158
214
  if (err.walRecoveryAttempted) {
@@ -1082,6 +1138,8 @@ server.registerTool(
1082
1138
  files: args.files || [],
1083
1139
  lesson_learned: clampSaveText(args.lesson_learned, SAVE_TEXT_LIMITS.lesson_learned),
1084
1140
  supersedes: args.supersedes,
1141
+ // Opt-in only — the near-duplicate window stays the default. CLI parity: `--force`.
1142
+ force: args.force === true,
1085
1143
  },
1086
1144
  { closesTokens: args.closes_deferred, project },
1087
1145
  ));
package/source-files.mjs CHANGED
@@ -55,6 +55,11 @@ export const SOURCE_FILES = [
55
55
  // AND hook scripts (pre-tool-recall / post-tool-recall) — ship it
56
56
  // or auto-update leaves schema + every hook with ERR_MODULE_NOT_FOUND on each fire.
57
57
  'lib/resolve-data-dir.mjs',
58
+ // DB_DIR / DB_PATH / CODE_DIR. Statically imported by schema.mjs (which re-exports all
59
+ // three) and by hook-update.mjs, which takes them from HERE so the verified repair path
60
+ // stays loadable without better-sqlite3. Missing from the manifest → auto-update leaves
61
+ // schema.mjs and the repair path with ERR_MODULE_NOT_FOUND on every fire.
62
+ 'lib/data-paths.mjs',
58
63
  // lib/ — statically imported by hook-llm.mjs (activity) + hook-handoff.mjs (git-state, task-reader);
59
64
  // dynamically imported by hook.mjs (startup-dashboard) + mem-cli.mjs (doctor-benchmark, plan-reader).
60
65
  'lib/activity.mjs',
@@ -132,6 +137,8 @@ export const SOURCE_FILES = [
132
137
  // ERR_MODULE_NOT_FOUND on the command users run when something is already wrong.
133
138
  'lib/install-shape.mjs',
134
139
  'lib/schema-skew.mjs',
140
+ 'lib/db-unusable.mjs',
141
+ 'lib/record-once.mjs',
135
142
  // Single-envelope stdout for hook processes — imported by hook.mjs. Claude Code
136
143
  // parses hook stdout as ONE JSON document; missing from the manifest → an updated
137
144
  // install throws ERR_MODULE_NOT_FOUND on every hook fire.