claude-mem-lite 3.96.0 → 3.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
package/hook-handoff.mjs CHANGED
@@ -2,11 +2,25 @@
2
2
  // Extracted for testability — hook.mjs has module-level side effects
3
3
 
4
4
  import { basename } from 'path';
5
- import { truncate, extractMatchKeywords, tokenizeHandoff, isSpecificTerm, scrubSecrets, LOW_SIGNAL_TITLE, EDIT_TOOLS, isMetaTriggerPrompt, notLowSignalTitleClause, neutralizeContextDelimiters } from './utils.mjs';
5
+ import {
6
+ truncate,
7
+ extractMatchKeywords,
8
+ tokenizeHandoff,
9
+ isSpecificTerm,
10
+ scrubSecrets,
11
+ LOW_SIGNAL_TITLE,
12
+ EDIT_TOOLS,
13
+ isMetaTriggerPrompt,
14
+ notLowSignalTitleClause,
15
+ neutralizeContextDelimiters,
16
+ } from './utils.mjs';
6
17
  import { scrubRecord } from './lib/scrub-record.mjs';
7
18
  import {
8
- HANDOFF_EXPIRY_CLEAR, HANDOFF_EXPIRY_EXIT, HANDOFF_ANCHOR_MAX_AGE,
9
- HANDOFF_MATCH_THRESHOLD, CONTINUE_KEYWORDS,
19
+ HANDOFF_EXPIRY_CLEAR,
20
+ HANDOFF_EXPIRY_EXIT,
21
+ HANDOFF_ANCHOR_MAX_AGE,
22
+ HANDOFF_MATCH_THRESHOLD,
23
+ CONTINUE_KEYWORDS,
10
24
  } from './hook-shared.mjs';
11
25
  // T10d: import the whole module (not a named export) so tests can spy on
12
26
  // gitStateModule.readGitState via vi.spyOn. Named-import bindings are
@@ -43,17 +57,25 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
43
57
  // unfiltered query (identical to pre-D#26 behavior).
44
58
  const ccScope = scopeSessionId && scopeSessionId !== sessionId ? scopeSessionId : null;
45
59
  const prompts = ccScope
46
- ? db.prepare(`
60
+ ? db
61
+ .prepare(
62
+ `
47
63
  SELECT prompt_text FROM user_prompts
48
64
  WHERE content_session_id = ? AND (cc_session_id = ? OR cc_session_id IS NULL)
49
65
  ORDER BY prompt_number ASC LIMIT 5
50
- `).all(sessionId, ccScope)
51
- : db.prepare(`
66
+ `,
67
+ )
68
+ .all(sessionId, ccScope)
69
+ : db
70
+ .prepare(
71
+ `
52
72
  SELECT prompt_text FROM user_prompts
53
73
  WHERE content_session_id = ?
54
74
  ORDER BY prompt_number ASC LIMIT 5
55
- `).all(sessionId);
56
- if (prompts.length === 0) return; // Empty session — nothing to hand off
75
+ `,
76
+ )
77
+ .all(sessionId);
78
+ if (prompts.length === 0) return; // Empty session — nothing to hand off
57
79
 
58
80
  // Filter prompts whose only content is workflow/control language ("继续",
59
81
  // "提交代码", "/exit", etc.). Storing them verbatim into working_on creates
@@ -62,26 +84,30 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
62
84
  // the project's most recent importance≥3 non-low-signal observation as the
63
85
  // carry-forward anchor — that's the closest durable signal of "what was
64
86
  // being worked on at a higher level than this session".
65
- const subjectPrompts = prompts.filter(p => !isMetaTriggerPrompt(p.prompt_text));
87
+ const subjectPrompts = prompts.filter((p) => !isMetaTriggerPrompt(p.prompt_text));
66
88
  const sourcePrompts = subjectPrompts.length > 0 ? subjectPrompts : prompts;
67
89
 
68
90
  const seen = new Set();
69
- const uniquePrompts = sourcePrompts.filter(p => {
91
+ const uniquePrompts = sourcePrompts.filter((p) => {
70
92
  const t = truncate(p.prompt_text, 200);
71
93
  if (seen.has(t)) return false;
72
94
  seen.add(t);
73
95
  return true;
74
96
  });
75
- let workingOn = uniquePrompts.map(p => truncate(p.prompt_text, 200)).join(' → ');
97
+ let workingOn = uniquePrompts.map((p) => truncate(p.prompt_text, 200)).join(' → ');
76
98
 
77
99
  if (subjectPrompts.length === 0) {
78
- const fallback = db.prepare(`
100
+ const fallback = db
101
+ .prepare(
102
+ `
79
103
  SELECT title FROM observations
80
104
  WHERE project = ? AND ${liveObsFilterSql('')}
81
105
  AND COALESCE(importance, 1) >= 3
82
106
  AND ${notLowSignalTitleClause('')}
83
107
  ORDER BY created_at_epoch DESC LIMIT 1
84
- `).get(project);
108
+ `,
109
+ )
110
+ .get(project);
85
111
  if (fallback?.title) {
86
112
  workingOn = `(carry-forward subject) ${truncate(fallback.title, 180)}`;
87
113
  }
@@ -96,21 +122,29 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
96
122
  // concurrent same-project sessions whose windows overlap can still co-attribute a few rows.
97
123
  let ccWindowStart = null;
98
124
  if (ccScope) {
99
- const w = db.prepare(`
125
+ const w = db
126
+ .prepare(
127
+ `
100
128
  SELECT MIN(created_at_epoch) AS startEpoch FROM user_prompts
101
129
  WHERE content_session_id = ? AND cc_session_id = ?
102
- `).get(sessionId, ccScope);
130
+ `,
131
+ )
132
+ .get(sessionId, ccScope);
103
133
  if (typeof w?.startEpoch === 'number') ccWindowStart = w.startEpoch;
104
134
  }
105
135
  const obsWindowClause = ccWindowStart !== null ? 'AND created_at_epoch >= ?' : '';
106
136
  const obsWindowParams = ccWindowStart !== null ? [ccWindowStart] : [];
107
137
 
108
138
  // 2. Completed — from observations (include narrative for richer handoff)
109
- const completed = db.prepare(`
139
+ const completed = db
140
+ .prepare(
141
+ `
110
142
  SELECT title, type, narrative FROM observations
111
143
  WHERE memory_session_id = ? AND COALESCE(compressed_into, 0) = 0 ${obsWindowClause}
112
144
  ORDER BY created_at_epoch DESC LIMIT 15
113
- `).all(sessionId, ...obsWindowParams);
145
+ `,
146
+ )
147
+ .all(sessionId, ...obsWindowParams);
114
148
 
115
149
  // 3. Recent activity — episode snapshot + full session edit history from narratives.
116
150
  // Keep only entries that represent in-flight work (file edits) or outright failures
@@ -121,9 +155,13 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
121
155
  if (episodeSnapshot?.entries) {
122
156
  const seenDescs = new Set();
123
157
  const pendingDescs = episodeSnapshot.entries
124
- .filter(e => e.isError || EDIT_TOOLS.has(e.tool))
125
- .map(e => e.desc)
126
- .filter(d => { if (seenDescs.has(d)) return false; seenDescs.add(d); return true; });
158
+ .filter((e) => e.isError || EDIT_TOOLS.has(e.tool))
159
+ .map((e) => e.desc)
160
+ .filter((d) => {
161
+ if (seenDescs.has(d)) return false;
162
+ seenDescs.add(d);
163
+ return true;
164
+ });
127
165
  if (pendingDescs.length > 0) unfinished = pendingDescs.join('; ');
128
166
  }
129
167
 
@@ -141,19 +179,19 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
141
179
  // '\n'-join collapsed the whole task list into one unreadable multi-line bullet.
142
180
  unfinished = tasks
143
181
  .slice(0, 5)
144
- .map(t => `[${t.status}] ${t.title}`)
182
+ .map((t) => `[${t.status}] ${t.title}`)
145
183
  .join(UNFINISHED_ENTRY_SEP);
146
184
  }
147
- } catch { /* task reader is best-effort; never block handoff */ }
185
+ } catch {
186
+ /* task reader is best-effort; never block handoff */
187
+ }
148
188
  }
149
189
 
150
190
  // Enrich unfinished with full session edit history from observation narratives.
151
191
  // Since handoff is UPSERT (max 2 rows per project), storing more data is free.
152
192
  // Always use \n---\n separator so extractUnfinishedSummary can distinguish
153
193
  // pending work (before separator) from narrative history (after separator).
154
- const narratives = completed
155
- .filter(c => c.narrative)
156
- .map(c => c.narrative);
194
+ const narratives = completed.filter((c) => c.narrative).map((c) => c.narrative);
157
195
  if (narratives.length > 0) {
158
196
  const editHistory = narratives.join('\n');
159
197
  unfinished += '\n---\n' + editHistory;
@@ -161,16 +199,30 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
161
199
 
162
200
  // 4. Key files — from episode snapshot + observations
163
201
  const fileSet = new Set();
164
- const isValidFile = f => f && f.length > 2 && f.includes('/') && f.indexOf('/', 1) !== -1
165
- && !f.startsWith('/dev/') && !f.startsWith('/proc/') && !f.startsWith('/tmp/');
166
- if (episodeSnapshot?.files) episodeSnapshot.files.filter(isValidFile).forEach(f => fileSet.add(f));
167
- const obsFiles = db.prepare(`
202
+ const isValidFile = (f) =>
203
+ f &&
204
+ f.length > 2 &&
205
+ f.includes('/') &&
206
+ f.indexOf('/', 1) !== -1 &&
207
+ !f.startsWith('/dev/') &&
208
+ !f.startsWith('/proc/') &&
209
+ !f.startsWith('/tmp/');
210
+ if (episodeSnapshot?.files) episodeSnapshot.files.filter(isValidFile).forEach((f) => fileSet.add(f));
211
+ const obsFiles = db
212
+ .prepare(
213
+ `
168
214
  SELECT files_modified FROM observations
169
215
  WHERE memory_session_id = ? AND files_modified IS NOT NULL ${obsWindowClause}
170
216
  ORDER BY created_at_epoch DESC LIMIT 10
171
- `).all(sessionId, ...obsWindowParams);
217
+ `,
218
+ )
219
+ .all(sessionId, ...obsWindowParams);
172
220
  for (const row of obsFiles) {
173
- try { JSON.parse(row.files_modified).filter(isValidFile).forEach(f => fileSet.add(f)); } catch {}
221
+ try {
222
+ JSON.parse(row.files_modified)
223
+ .filter(isValidFile)
224
+ .forEach((f) => fileSet.add(f));
225
+ } catch {}
174
226
  }
175
227
 
176
228
  // 5. Key decisions — high importance observations (skip low-signal degraded titles).
@@ -182,15 +234,21 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
182
234
  // retracted decision rendered there is indistinguishable from live policy. The
183
235
  // carry-forward fallback at the top of this function already filters the same column;
184
236
  // this is the sibling that did not.
185
- const decisions = db.prepare(`
237
+ const decisions = db
238
+ .prepare(
239
+ `
186
240
  SELECT title FROM observations
187
241
  WHERE memory_session_id = ? AND COALESCE(importance, 1) >= 2
188
242
  AND ${liveObsFilterSql('')} ${obsWindowClause}
189
243
  ORDER BY created_at_epoch DESC LIMIT 10
190
- `).all(sessionId, ...obsWindowParams).filter(d => d.title && !LOW_SIGNAL_TITLE.test(d.title)).slice(0, 5);
244
+ `,
245
+ )
246
+ .all(sessionId, ...obsWindowParams)
247
+ .filter((d) => d.title && !LOW_SIGNAL_TITLE.test(d.title))
248
+ .slice(0, 5);
191
249
 
192
250
  // 6. Match keywords
193
- const allText = [workingOn, ...completed.map(c => c.title).filter(Boolean), unfinished].join(' ');
251
+ const allText = [workingOn, ...completed.map((c) => c.title).filter(Boolean), unfinished].join(' ');
194
252
  const keywords = extractMatchKeywords(allText, [...fileSet]);
195
253
 
196
254
  // T10d: capture HEAD sha so detectContinuationIntent can anchor on it later.
@@ -198,7 +256,9 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
198
256
  let gitShaAtHandoff = null;
199
257
  try {
200
258
  gitShaAtHandoff = gitStateModule.readGitState({ cwd: process.cwd() }).headSha || null;
201
- } catch { /* swallow — handoff must still persist */ }
259
+ } catch {
260
+ /* swallow — handoff must still persist */
261
+ }
202
262
 
203
263
  // UPSERT keyed on (project, type, session_id) — parallel sessions coexist.
204
264
  // Same session re-writing its own handoff (e.g. repeated /clear) updates in place.
@@ -214,15 +274,14 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
214
274
  // string would risk breaking downstream JSON.parse.
215
275
  const safe = scrubRecord('session_handoffs', {
216
276
  working_on: workingOn,
217
- completed: completed.map(c => `[${c.type}] ${c.title}`).join('\n'),
277
+ completed: completed.map((c) => `[${c.type}] ${c.title}`).join('\n'),
218
278
  unfinished,
219
- key_decisions: decisions.map(d => d.title).join('\n'),
279
+ key_decisions: decisions.map((d) => d.title).join('\n'),
220
280
  match_keywords: keywords,
221
281
  });
222
- const safeKeyFiles = JSON.stringify(
223
- [...fileSet].slice(0, 20).map(f => scrubSecrets(String(f)))
224
- );
225
- db.prepare(`
282
+ const safeKeyFiles = JSON.stringify([...fileSet].slice(0, 20).map((f) => scrubSecrets(String(f))));
283
+ db.prepare(
284
+ `
226
285
  INSERT INTO session_handoffs (project, type, session_id, working_on, completed, unfinished, key_files, key_decisions, match_keywords, created_at_epoch, git_sha_at_handoff)
227
286
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
228
287
  ON CONFLICT(project, type, session_id) DO UPDATE SET
@@ -234,11 +293,16 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
234
293
  match_keywords = excluded.match_keywords,
235
294
  created_at_epoch = excluded.created_at_epoch,
236
295
  git_sha_at_handoff = excluded.git_sha_at_handoff
237
- `).run(
238
- project, type, storedSessionId,
296
+ `,
297
+ ).run(
298
+ project,
299
+ type,
300
+ storedSessionId,
239
301
  truncate(safe.working_on, 1000),
240
302
  safe.completed,
241
- safe.unfinished.length > 3000 ? Array.from(safe.unfinished).slice(0, 2999).join('') + '…' : safe.unfinished,
303
+ safe.unfinished.length > 3000
304
+ ? Array.from(safe.unfinished).slice(0, 2999).join('') + '…'
305
+ : safe.unfinished,
242
306
  safeKeyFiles,
243
307
  safe.key_decisions,
244
308
  safe.match_keywords,
@@ -283,48 +347,66 @@ export function detectContinuationIntent(db, promptText, project, currentCcSessi
283
347
  // anchor is same-session only — else a parallel same-project session at the same commit
284
348
  // would hijack (and then delete) another session's clear handoff.
285
349
  const anchor = currentCcSessionId
286
- ? db.prepare(`
350
+ ? db
351
+ .prepare(
352
+ `
287
353
  SELECT created_at_epoch, match_keywords FROM session_handoffs
288
354
  WHERE project = ? AND git_sha_at_handoff = ? AND (type = 'exit' OR session_id = ?)
289
355
  ORDER BY created_at_epoch DESC LIMIT 1
290
- `).get(project, currentSha, currentCcSessionId)
291
- : db.prepare(`
356
+ `,
357
+ )
358
+ .get(project, currentSha, currentCcSessionId)
359
+ : db
360
+ .prepare(
361
+ `
292
362
  SELECT created_at_epoch, match_keywords FROM session_handoffs
293
363
  WHERE project = ? AND git_sha_at_handoff = ?
294
364
  ORDER BY created_at_epoch DESC LIMIT 1
295
- `).get(project, currentSha);
296
- if (anchor && (Date.now() - anchor.created_at_epoch <= HANDOFF_ANCHOR_MAX_AGE)) {
365
+ `,
366
+ )
367
+ .get(project, currentSha);
368
+ if (anchor && Date.now() - anchor.created_at_epoch <= HANDOFF_ANCHOR_MAX_AGE) {
297
369
  // Unmoved HEAD is a strong resume signal, but must not hijack a NEW task typed at the
298
370
  // same commit: gate long prompts on keyword overlap (mirror Stage 0). Short prompts
299
371
  // (resume nudges) auto-continue.
300
372
  if (promptText.length < 40) return true;
301
373
  const hTokens = anchor.match_keywords ? new Set(tokenizeHandoff(anchor.match_keywords)) : null;
302
- if (!hTokens || tokenizeHandoff(promptText).some(t => hTokens.has(t))) return true;
374
+ if (!hTokens || tokenizeHandoff(promptText).some((t) => hTokens.has(t))) return true;
303
375
  // long prompt with zero keyword overlap → fall through to Stage 0/1/2
304
376
  }
305
377
  }
306
- } catch { /* git/DB failure must not break the rest of the pipeline */ }
378
+ } catch {
379
+ /* git/DB failure must not break the rest of the pipeline */
380
+ }
307
381
 
308
382
  // Stage 0: Non-expired 'clear' handoff — assume continuation unless long unrelated prompt.
309
383
  // Session scoping: with currentCcSessionId, only your OWN clear handoff qualifies.
310
384
  const clearHandoff = currentCcSessionId
311
- ? db.prepare(`
385
+ ? db
386
+ .prepare(
387
+ `
312
388
  SELECT created_at_epoch, match_keywords FROM session_handoffs
313
389
  WHERE project = ? AND type = 'clear' AND session_id = ?
314
390
  ORDER BY created_at_epoch DESC LIMIT 1
315
- `).get(project, currentCcSessionId)
316
- : db.prepare(`
391
+ `,
392
+ )
393
+ .get(project, currentCcSessionId)
394
+ : db
395
+ .prepare(
396
+ `
317
397
  SELECT created_at_epoch, match_keywords FROM session_handoffs
318
398
  WHERE project = ? AND type = 'clear'
319
399
  ORDER BY created_at_epoch DESC LIMIT 1
320
- `).get(project);
400
+ `,
401
+ )
402
+ .get(project);
321
403
 
322
- if (clearHandoff && (Date.now() - clearHandoff.created_at_epoch <= HANDOFF_EXPIRY_CLEAR)) {
404
+ if (clearHandoff && Date.now() - clearHandoff.created_at_epoch <= HANDOFF_EXPIRY_CLEAR) {
323
405
  const pTokens = tokenizeHandoff(promptText);
324
406
  const hTokens = clearHandoff.match_keywords
325
407
  ? new Set(tokenizeHandoff(clearHandoff.match_keywords))
326
408
  : null;
327
- const hasOverlap = hTokens && pTokens.some(t => hTokens.has(t));
409
+ const hasOverlap = hTokens && pTokens.some((t) => hTokens.has(t));
328
410
  if (promptText.length < 40) {
329
411
  // Short prompts: session-scoped clear = same user/context, auto-continue.
330
412
  // Unscoped (legacy / no session_id in hook input) requires an explicit
@@ -348,21 +430,29 @@ export function detectContinuationIntent(db, promptText, project, currentCcSessi
348
430
  // Session scoping: exit handoffs from OTHER sessions are still candidates (you may
349
431
  // be resuming a previous session), but clear handoffs must be same-session.
350
432
  const handoffs = currentCcSessionId
351
- ? db.prepare(`
433
+ ? db
434
+ .prepare(
435
+ `
352
436
  SELECT type, match_keywords, created_at_epoch FROM session_handoffs
353
437
  WHERE project = ?
354
438
  AND ((type = 'clear' AND session_id = ?) OR type = 'exit')
355
439
  ORDER BY created_at_epoch DESC
356
- `).all(project, currentCcSessionId)
357
- : db.prepare(`
440
+ `,
441
+ )
442
+ .all(project, currentCcSessionId)
443
+ : db
444
+ .prepare(
445
+ `
358
446
  SELECT type, match_keywords, created_at_epoch FROM session_handoffs
359
447
  WHERE project = ? ORDER BY created_at_epoch DESC
360
- `).all(project);
448
+ `,
449
+ )
450
+ .all(project);
361
451
  if (handoffs.length === 0) return false;
362
452
 
363
453
  // Filter expired handoffs
364
454
  const now = Date.now();
365
- const validHandoffs = handoffs.filter(h => {
455
+ const validHandoffs = handoffs.filter((h) => {
366
456
  const age = now - h.created_at_epoch;
367
457
  const maxAge = h.type === 'clear' ? HANDOFF_EXPIRY_CLEAR : HANDOFF_EXPIRY_EXIT;
368
458
  return age <= maxAge;
@@ -403,21 +493,31 @@ export function pickHandoffToInject(db, project, currentCcSessionId = null) {
403
493
  // Fetch recent handoffs and find the most recent non-expired one.
404
494
  // A newer but expired 'clear' handoff must not shadow a still-valid 'exit' handoff.
405
495
  const handoffs = currentCcSessionId
406
- ? db.prepare(`
496
+ ? db
497
+ .prepare(
498
+ `
407
499
  SELECT * FROM session_handoffs
408
500
  WHERE project = ?
409
501
  AND ((type = 'clear' AND session_id = ?) OR (type = 'exit' AND session_id != ?))
410
502
  ORDER BY created_at_epoch DESC LIMIT 5
411
- `).all(project, currentCcSessionId, currentCcSessionId)
412
- : db.prepare(`
503
+ `,
504
+ )
505
+ .all(project, currentCcSessionId, currentCcSessionId)
506
+ : db
507
+ .prepare(
508
+ `
413
509
  SELECT * FROM session_handoffs
414
510
  WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 5
415
- `).all(project);
416
- return handoffs.find(h => {
417
- const age = now - h.created_at_epoch;
418
- const maxAge = h.type === 'clear' ? HANDOFF_EXPIRY_CLEAR : HANDOFF_EXPIRY_EXIT;
419
- return age <= maxAge;
420
- }) || null;
511
+ `,
512
+ )
513
+ .all(project);
514
+ return (
515
+ handoffs.find((h) => {
516
+ const age = now - h.created_at_epoch;
517
+ const maxAge = h.type === 'clear' ? HANDOFF_EXPIRY_CLEAR : HANDOFF_EXPIRY_EXIT;
518
+ return age <= maxAge;
519
+ }) || null
520
+ );
421
521
  }
422
522
 
423
523
  export function renderHandoffInjection(db, project, currentCcSessionId = null) {
@@ -428,10 +528,14 @@ export function renderHandoffInjection(db, project, currentCcSessionId = null) {
428
528
 
429
529
  function renderHandoffFromRow(handoff, db, project) {
430
530
  const ageSec = Math.round((Date.now() - handoff.created_at_epoch) / 1000);
431
- const ageStr = ageSec < 60 ? `${ageSec}s` :
432
- ageSec < 3600 ? `${Math.round(ageSec / 60)}m` :
433
- ageSec < 86400 ? `${Math.round(ageSec / 3600)}h` :
434
- `${Math.round(ageSec / 86400)}d`;
531
+ const ageStr =
532
+ ageSec < 60
533
+ ? `${ageSec}s`
534
+ : ageSec < 3600
535
+ ? `${Math.round(ageSec / 60)}m`
536
+ : ageSec < 86400
537
+ ? `${Math.round(ageSec / 3600)}h`
538
+ : `${Math.round(ageSec / 86400)}d`;
435
539
 
436
540
  // Framing header: `UserPromptSubmit` hook writes this block to stdout, which
437
541
  // Claude Code surfaces alongside the real user prompt. Without an explicit
@@ -452,7 +556,13 @@ function renderHandoffFromRow(handoff, db, project) {
452
556
  lines.push('## Working On', neutralizeContextDelimiters(handoff.working_on), '');
453
557
  }
454
558
  if (handoff.completed) {
455
- lines.push('## Completed', ...neutralizeContextDelimiters(handoff.completed).split('\n').map(l => `- ${l}`), '');
559
+ lines.push(
560
+ '## Completed',
561
+ ...neutralizeContextDelimiters(handoff.completed)
562
+ .split('\n')
563
+ .map((l) => `- ${l}`),
564
+ '',
565
+ );
456
566
  }
457
567
  if (handoff.unfinished) {
458
568
  // Extract only the pending-work portion (before narrative history separator).
@@ -461,7 +571,13 @@ function renderHandoffFromRow(handoff, db, project) {
461
571
  // completeness claim the episode buffer can't support.
462
572
  const pending = extractUnfinishedSummary(handoff.unfinished);
463
573
  if (pending) {
464
- lines.push('## Recent activity', ...neutralizeContextDelimiters(pending).split('; ').map(l => `- ${l}`), '');
574
+ lines.push(
575
+ '## Recent activity',
576
+ ...neutralizeContextDelimiters(pending)
577
+ .split('; ')
578
+ .map((l) => `- ${l}`),
579
+ '',
580
+ );
465
581
  }
466
582
  }
467
583
  if (handoff.key_files) {
@@ -470,11 +586,18 @@ function renderHandoffFromRow(handoff, db, project) {
470
586
  // Defang basenames too: a filename on disk can contain a literal authority tag
471
587
  // (Linux allows almost any char but '/'), and this is the one field in this block
472
588
  // that was rendered raw while working_on/unfinished/key_decisions all neutralize.
473
- if (files.length > 0) lines.push('## Key Files', neutralizeContextDelimiters(files.map(f => basename(f)).join(', ')), '');
589
+ if (files.length > 0)
590
+ lines.push('## Key Files', neutralizeContextDelimiters(files.map((f) => basename(f)).join(', ')), '');
474
591
  } catch {}
475
592
  }
476
593
  if (handoff.key_decisions) {
477
- lines.push('## Key Decisions', ...neutralizeContextDelimiters(handoff.key_decisions).split('\n').map(l => `- ${l}`), '');
594
+ lines.push(
595
+ '## Key Decisions',
596
+ ...neutralizeContextDelimiters(handoff.key_decisions)
597
+ .split('\n')
598
+ .map((l) => `- ${l}`),
599
+ '',
600
+ );
478
601
  }
479
602
 
480
603
  lines.push('</session-handoff>');
@@ -488,21 +611,29 @@ function renderHandoffFromRow(handoff, db, project) {
488
611
  // ids align — legacy rows + tests), then fall back to the most-recent summary for the
489
612
  // project, which at resume time is the summary from the session that wrote this handoff.
490
613
  try {
491
- let summary = db.prepare(`
614
+ let summary = db
615
+ .prepare(
616
+ `
492
617
  SELECT completed, next_steps, remaining_items FROM session_summaries
493
618
  WHERE memory_session_id = ? AND project = ?
494
619
  ORDER BY created_at_epoch DESC LIMIT 1
495
- `).get(handoff.session_id, project);
620
+ `,
621
+ )
622
+ .get(handoff.session_id, project);
496
623
  if (!summary) {
497
624
  // Pick the project summary CLOSEST IN TIME to this handoff, not merely the newest:
498
625
  // a handoff and its own session's summary are written within ms of each other at
499
626
  // session end, so nearest-timestamp recovers the right session even when a different
500
627
  // session later wrote a newer summary for the same project (concurrent/interleaved use).
501
- summary = db.prepare(`
628
+ summary = db
629
+ .prepare(
630
+ `
502
631
  SELECT completed, next_steps, remaining_items FROM session_summaries
503
632
  WHERE project = ?
504
633
  ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
505
- `).get(project, handoff.created_at_epoch ?? 0);
634
+ `,
635
+ )
636
+ .get(project, handoff.created_at_epoch ?? 0);
506
637
  }
507
638
  if (summary && (summary.completed || summary.next_steps || summary.remaining_items)) {
508
639
  lines.push('');
@@ -511,7 +642,8 @@ function renderHandoffFromRow(handoff, db, project) {
511
642
  // extractStructuredSummary over the assistant transcript tail — replayed text that can
512
643
  // carry tool-XML / forged authority tags, same class as working_on above (audit MED-4).
513
644
  if (summary.completed) lines.push(neutralizeContextDelimiters(summary.completed));
514
- if (summary.remaining_items) lines.push(`Remaining: ${neutralizeContextDelimiters(summary.remaining_items)}`);
645
+ if (summary.remaining_items)
646
+ lines.push(`Remaining: ${neutralizeContextDelimiters(summary.remaining_items)}`);
515
647
  if (summary.next_steps) lines.push(`Next steps: ${neutralizeContextDelimiters(summary.next_steps)}`);
516
648
  lines.push('</session-summary>');
517
649
  }