claude-mem-lite 6.2.0 → 6.4.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.
@@ -209,26 +209,84 @@ try {
209
209
  useDevServer = existsSync(devServer) && lstatSync(devServer).isSymbolicLink();
210
210
  } catch {}
211
211
 
212
+ // The MCP server opens the DB while it is being imported, so a forward-incompat store
213
+ // (schema.mjs's "DB schema is vN but this binary supports up to vN-1") throws right here
214
+ // and kills the process before the stdio handshake. All the host can say about that is
215
+ // `-32000 Connection closed`, which names nothing — measured 2026-09-08, a full day of it
216
+ // with the real cause visible only in a JSONL file the user has no reason to open.
217
+ //
218
+ // stderr is the one channel a launcher still has at that point. It reaches the plugin's own
219
+ // log rather than the transcript, so this is a diagnosis for whoever goes looking, not a
220
+ // substitute for the SessionStart notice — which is why both exist.
221
+ async function importServerOrExplain(run, { dev = false } = {}) {
222
+ try {
223
+ await run();
224
+ } catch (e) {
225
+ // The classifier is loaded INSIDE its own try and any failure rethrows the ORIGINAL
226
+ // error. Importing it unconditionally destroyed `e`: this path exists to diagnose an
227
+ // install whose files are missing (issue #15), lib/schema-skew.mjs is a brand-new file,
228
+ // and resolveLaunchEntry can serve the server from dataDir while `../lib/…` still
229
+ // resolves against ROOT. Proven by review — with the module moved aside the process died
230
+ // naming ERR_MODULE_NOT_FOUND for the classifier while the real boot failure never
231
+ // appeared anywhere in the output.
232
+ let skewMod;
233
+ try {
234
+ skewMod = await import('../lib/schema-skew.mjs');
235
+ } catch {
236
+ throw e;
237
+ }
238
+ if (!skewMod.isSchemaSkewError(e)) throw e;
239
+ let shape = { managed: false, activePluginVersion: null };
240
+ try {
241
+ ({ ...shape } = await import('../lib/install-shape.mjs').then((m) =>
242
+ m.detectInstallShape({ installDir: dataDir }),
243
+ ));
244
+ } catch {
245
+ /* shape unknown → schemaSkewRemedy answers 'unknown', which is its job */
246
+ }
247
+ const skew = skewMod.schemaSkewFromError(e) || { dbVersion: null, binaryVersion: null };
248
+ process.stderr.write(
249
+ skewMod.formatSchemaSkewNotice({
250
+ dbVersion: skew.dbVersion,
251
+ binaryVersion: skew.binaryVersion,
252
+ // `dev` is passed because the useDevServer branch IS the dev install by definition —
253
+ // omitting it told a checkout to `npm i -g` over its own working tree. `root: ROOT`
254
+ // so a mixed managed+plugin machine gets the remedy for the tree that is behind.
255
+ remedy: skewMod.schemaSkewRemedy({
256
+ managed: shape.managed,
257
+ activePluginVersion: shape.activePluginVersion,
258
+ dev,
259
+ root: ROOT,
260
+ }),
261
+ codeHome: ROOT,
262
+ }) + '\n',
263
+ );
264
+ process.exit(1);
265
+ }
266
+ }
267
+
212
268
  if (useDevServer) {
213
- await import(pathToFileURL(devServer).href);
269
+ await importServerOrExplain(() => import(pathToFileURL(devServer).href), { dev: true });
214
270
  } else {
215
271
  // Preflight: detect incomplete primary install (issue #15) — if relative
216
272
  // imports referenced by server.mjs are missing on disk, fall back to the
217
273
  // hook-update.mjs-maintained ~/.claude-mem-lite/ copy when healthy, or exit
218
274
  // with a clear repair command instead of a Node ERR_MODULE_NOT_FOUND stack.
219
275
  const { resolveLaunchEntry } = await import('./launch-preflight.mjs');
220
- try {
221
- const entry = resolveLaunchEntry({
222
- primaryRoot: ROOT,
223
- fallbackRoot: dataDir,
224
- warn: (msg) => process.stderr.write(msg + '\n'),
225
- });
226
- await import(pathToFileURL(entry.path).href);
227
- } catch (e) {
228
- if (e.code === 'INSTALL_INCOMPLETE') {
229
- process.stderr.write(e.message + '\n');
230
- process.exit(1);
276
+ await importServerOrExplain(async () => {
277
+ try {
278
+ const entry = resolveLaunchEntry({
279
+ primaryRoot: ROOT,
280
+ fallbackRoot: dataDir,
281
+ warn: (msg) => process.stderr.write(msg + '\n'),
282
+ });
283
+ await import(pathToFileURL(entry.path).href);
284
+ } catch (e) {
285
+ if (e.code === 'INSTALL_INCOMPLETE') {
286
+ process.stderr.write(e.message + '\n');
287
+ process.exit(1);
288
+ }
289
+ throw e;
231
290
  }
232
- throw e;
233
- }
291
+ });
234
292
  }
@@ -39,6 +39,7 @@ import {
39
39
  import { injectedIdsFileName, mergeInjectedMarker } from '../lib/injected-ids.mjs';
40
40
  import { getDeferredByIds } from '../lib/deferred-work.mjs';
41
41
  import { recordHookError } from '../lib/hook-telemetry.mjs';
42
+ import { isSchemaSkewError, schemaSkewFromError, shouldRecordSkew } from '../lib/schema-skew.mjs';
42
43
 
43
44
  import { DAY_MS } from '../lib/time-constants.mjs';
44
45
  import { envNumber } from '../lib/env-number.mjs';
@@ -773,6 +774,25 @@ async function main() {
773
774
  // A failed DB open silently kills EVERY prompt-time injection while `stats`
774
775
  // reads zero errors (audit 2026-08-14 M-5) — record before the mandatory
775
776
  // swallow. Exact blindness class of the 2026-08-13 pre-recall:db-open outage.
777
+ //
778
+ // Schema skew is the one member of that family worth deduplicating: it persists until
779
+ // the user installs newer code, so it repeats on EVERY prompt. This face opens the DB
780
+ // itself rather than through hook-shared's openDb, so it needs the gate explicitly —
781
+ // it contributed 15 of one measured day's 727 identical lines, i.e. the flood was ~98%
782
+ // closed and not closed. Shared implementation, deliberately: a second copy of a
783
+ // dedup rule is this repo's twin-drift class.
784
+ if (isSchemaSkewError(e)) {
785
+ let project = '';
786
+ try {
787
+ project = inferProject();
788
+ } catch {
789
+ /* total — the marker degrades to one shared file, never a throw */
790
+ }
791
+ if (shouldRecordSkew(RUNTIME_DIR, project, schemaSkewFromError(e))) {
792
+ recordHookError('ups:db-open', e, RUNTIME_DIR);
793
+ }
794
+ return;
795
+ }
776
796
  recordHookError('ups:db-open', e, RUNTIME_DIR);
777
797
  return;
778
798
  }
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) {
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',
@@ -131,6 +136,7 @@ export const SOURCE_FILES = [
131
136
  // Missing from the manifest → an updated install ships a doctor that throws
132
137
  // ERR_MODULE_NOT_FOUND on the command users run when something is already wrong.
133
138
  'lib/install-shape.mjs',
139
+ 'lib/schema-skew.mjs',
134
140
  // Single-envelope stdout for hook processes — imported by hook.mjs. Claude Code
135
141
  // parses hook stdout as ONE JSON document; missing from the manifest → an updated
136
142
  // install throws ERR_MODULE_NOT_FOUND on every hook fire.
package/tool-schemas.mjs CHANGED
@@ -256,7 +256,15 @@ export const memGetSchema = {
256
256
  };
257
257
 
258
258
  export const memDeleteSchema = {
259
- ids: coerceIntArray.pipe(z.array(z.number().int()).min(1).max(50)).describe('Observation IDs to delete'),
259
+ // `.nonoptional()` is not a runtime change — zod already rejected an omitted `ids` here.
260
+ // It is what keeps the field in the PUBLISHED JSON Schema's `required` array: zod 4's
261
+ // toJSONSchema({io:'input'}) reads a ZodPipe's input side as accepting `undefined` and
262
+ // drops the key, so the advertised contract said optional while the server said required.
263
+ // See tests/tool-schemas.test.mjs, which grades every field against runtime ground truth.
264
+ ids: coerceIntArray
265
+ .pipe(z.array(z.number().int()).min(1).max(50))
266
+ .nonoptional()
267
+ .describe('Observation IDs to delete'),
260
268
  confirm: coerceBool.describe('false=preview what will be deleted, true=execute deletion'),
261
269
  };
262
270
 
@@ -433,7 +441,8 @@ export const memMaintainSchema = {
433
441
  };
434
442
 
435
443
  export const memUpdateSchema = {
436
- id: coerceInt.pipe(z.number().int().positive()).describe('Observation ID to update'),
444
+ // `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
445
+ id: coerceInt.pipe(z.number().int().positive()).nonoptional().describe('Observation ID to update'),
437
446
  // CLI parity (cmdUpdate): empty/whitespace title would render as `(untitled)`
438
447
  // in every listing — reject here like the CLI does, instead of persisting it.
439
448
  title: z
@@ -541,6 +550,10 @@ export const memDeferDropSchema = {
541
550
  coerceInt.pipe(z.number().int().positive()),
542
551
  z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
543
552
  ])
553
+ // `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
554
+ // This is the CORE tool of the three — it ships in tools/list, so the drift was visible
555
+ // to every agent, and the tool's own description already calls the reason "required".
556
+ .nonoptional()
544
557
  .describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
545
558
  reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
546
559
  project: z.string().optional().describe('Project name (default: inferred from CWD)'),