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-context.mjs CHANGED
@@ -9,11 +9,25 @@ import { basename, join } from 'path';
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { atomicWriteFileSync } from './lib/atomic-write.mjs';
11
11
  import {
12
- estimateTokens, truncate, typeIcon, fmtTime, inferProject,
13
- debugLog, neutralizeContextDelimiters,
14
- DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause,
12
+ estimateTokens,
13
+ truncate,
14
+ typeIcon,
15
+ fmtTime,
16
+ inferProject,
17
+ debugLog,
18
+ neutralizeContextDelimiters,
19
+ DECAY_HALF_LIFE_BY_TYPE,
20
+ DEFAULT_DECAY_HALF_LIFE_MS,
21
+ notLowSignalTitleClause,
15
22
  } from './utils.mjs';
16
- import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet, isQuietHooks, KEY_CONTEXT_LIMIT } from './hook-shared.mjs';
23
+ import {
24
+ STALE_SESSION_MS,
25
+ RELATED_OBS_WINDOW_MS,
26
+ RUNTIME_DIR,
27
+ effectiveQuiet,
28
+ isQuietHooks,
29
+ KEY_CONTEXT_LIMIT,
30
+ } from './hook-shared.mjs';
17
31
  import { extractUnfinishedSummary } from './hook-handoff.mjs';
18
32
  import { recentInjectableEvents, renderInjectableEvent } from './lib/events-injection.mjs';
19
33
  import { liveObsFilterSql } from './lib/inject-search-core.mjs';
@@ -39,15 +53,21 @@ import { DAY_MS } from './lib/time-constants.mjs';
39
53
  // SessionStart. Escape pipes and collapse any CR/LF/tab to a space so one obs
40
54
  // stays one row/cell.
41
55
  function mdCell(s) {
42
- return String(s ?? '').replace(/[\r\n\t]+/g, ' ').replace(/\|/g, '\\|');
56
+ return String(s ?? '')
57
+ .replace(/[\r\n\t]+/g, ' ')
58
+ .replace(/\|/g, '\\|');
43
59
  }
44
60
 
45
61
  export function computeAdaptiveWindows(db, project) {
46
62
  const sevenDaysAgo = Date.now() - 7 * DAY_MS;
47
- const row = db.prepare(`
63
+ const row = db
64
+ .prepare(
65
+ `
48
66
  SELECT COUNT(*) as c FROM observations
49
67
  WHERE project = ? AND created_at_epoch > ? AND COALESCE(compressed_into, 0) = 0
50
- `).get(project, sevenDaysAgo);
68
+ `,
69
+ )
70
+ .get(project, sevenDaysAgo);
51
71
  const velocity = (row?.c || 0) / 7; // observations per day
52
72
 
53
73
  if (velocity > 10) {
@@ -173,7 +193,9 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
173
193
  // R1/R3: exclude LOW_SIGNAL degraded titles ("Modified X", "Worked on X",
174
194
  // "Reviewed N files:", raw error logs) from the Key Context table at
175
195
  // session start — they pollute the visible "Recent" table with noise.
176
- const obsPool = db.prepare(`
196
+ const obsPool = db
197
+ .prepare(
198
+ `
177
199
  SELECT id, type, title, narrative, importance, created_at_epoch, files_modified, lesson_learned
178
200
  FROM observations
179
201
  WHERE project = ? AND ${liveObsFilterSql('')}
@@ -185,15 +207,21 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
185
207
  )
186
208
  ORDER BY created_at_epoch DESC
187
209
  LIMIT ${KEYCTX_POOL_OBS}
188
- `).all(project, tier1Ago, tier2Ago, tier3Ago);
210
+ `,
211
+ )
212
+ .all(project, tier1Ago, tier2Ago, tier3Ago);
189
213
 
190
- const sessPool = db.prepare(`
214
+ const sessPool = db
215
+ .prepare(
216
+ `
191
217
  SELECT id, request, completed, next_steps, created_at_epoch
192
218
  FROM session_summaries
193
219
  WHERE project = ? AND created_at_epoch > ?
194
220
  ORDER BY created_at_epoch DESC
195
221
  LIMIT ${KEYCTX_POOL_SESS}
196
- `).all(project, now_ms - windows.sessWindow);
222
+ `,
223
+ )
224
+ .all(project, now_ms - windows.sessWindow);
197
225
 
198
226
  const selectedObs = [];
199
227
  const selectedSess = [];
@@ -201,9 +229,9 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
201
229
 
202
230
  // Score each candidate: value = recency * type_quality * importance, cost = tokens
203
231
  // Recency uses exponential half-life (consistent with server.mjs BM25 scoring)
204
- const scoredObs = obsPool.map(o => {
232
+ const scoredObs = obsPool.map((o) => {
205
233
  const halfLifeMs = DECAY_HALF_LIFE_BY_TYPE[o.type] || DEFAULT_DECAY_HALF_LIFE_MS;
206
- const recency = 1.0 + Math.exp(-0.693 * (now_ms - o.created_at_epoch) / halfLifeMs);
234
+ const recency = 1.0 + Math.exp((-0.693 * (now_ms - o.created_at_epoch)) / halfLifeMs);
207
235
  const typeQuality = TYPE_QUALITY[o.type] || TYPE_QUALITY_DEFAULT;
208
236
  const impBoost = 0.5 + 0.5 * (o.importance || 1);
209
237
  const lessonBoost = o.lesson_learned ? 1.3 : 1.0;
@@ -217,8 +245,8 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
217
245
  return { ...o, value, cost, valueDensity: cost > 0 ? value / Math.sqrt(cost) : 0 };
218
246
  });
219
247
 
220
- const scoredSess = sessPool.map(s => {
221
- const recency = 1.0 + Math.exp(-0.693 * (now_ms - s.created_at_epoch) / DEFAULT_DECAY_HALF_LIFE_MS);
248
+ const scoredSess = sessPool.map((s) => {
249
+ const recency = 1.0 + Math.exp((-0.693 * (now_ms - s.created_at_epoch)) / DEFAULT_DECAY_HALF_LIFE_MS);
222
250
  const value = recency * 1.5; // Session summaries slightly boosted
223
251
  const cost = estimateTokens((s.request || '') + (s.completed || '') + (s.next_steps || ''));
224
252
  return { ...s, value, cost, valueDensity: cost > 0 ? value / Math.sqrt(cost) : 0 };
@@ -226,8 +254,8 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
226
254
 
227
255
  // Combine and sort by value density (greedy knapsack)
228
256
  const allCandidates = [
229
- ...scoredObs.map(o => ({ ...o, _kind: 'obs' })),
230
- ...scoredSess.map(s => ({ ...s, _kind: 'sess' })),
257
+ ...scoredObs.map((o) => ({ ...o, _kind: 'obs' })),
258
+ ...scoredSess.map((s) => ({ ...s, _kind: 'sess' })),
231
259
  ].sort((a, b) => b.valueDensity - a.valueDensity);
232
260
 
233
261
  const selectedTypes = new Map(); // type → count for diversity constraint
@@ -273,9 +301,20 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
273
301
 
274
302
  totalTokens += c.cost;
275
303
  if (c._kind === 'obs') {
276
- selectedObs.push({ id: c.id, type: c.type, title: c.title, created_at: new Date(c.created_at_epoch).toISOString() });
304
+ selectedObs.push({
305
+ id: c.id,
306
+ type: c.type,
307
+ title: c.title,
308
+ created_at: new Date(c.created_at_epoch).toISOString(),
309
+ });
277
310
  } else {
278
- selectedSess.push({ id: c.id, request: c.request, completed: c.completed, next_steps: c.next_steps, created_at: new Date(c.created_at_epoch).toISOString() });
311
+ selectedSess.push({
312
+ id: c.id,
313
+ request: c.request,
314
+ completed: c.completed,
315
+ next_steps: c.next_steps,
316
+ created_at: new Date(c.created_at_epoch).toISOString(),
317
+ });
279
318
  }
280
319
  }
281
320
 
@@ -304,15 +343,23 @@ export function cleanupClaudeMdLegacyBlock() {
304
343
 
305
344
  const claudeMdPath = join(inferProjectDir(), 'CLAUDE.md');
306
345
  let content;
307
- try { content = readFileSync(claudeMdPath, 'utf8'); } catch {
346
+ try {
347
+ content = readFileSync(claudeMdPath, 'utf8');
348
+ } catch {
308
349
  // CLAUDE.md missing — still drop the marker so we don't re-stat every session
309
- try { writeFileSync(markerPath, String(Date.now())); } catch {}
350
+ try {
351
+ writeFileSync(markerPath, String(Date.now()));
352
+ } catch {}
310
353
  return;
311
354
  }
312
355
 
313
356
  // Helper: drop the marker regardless of exit path (found / not found / write failed).
314
357
  // Kept inline so the early-return sites below stay readable.
315
- const dropMarker = () => { try { writeFileSync(markerPath, String(Date.now())); } catch {} };
358
+ const dropMarker = () => {
359
+ try {
360
+ writeFileSync(markerPath, String(Date.now()));
361
+ } catch {}
362
+ };
316
363
 
317
364
  const startTag = '<claude-mem-context>';
318
365
  const endTag = '</claude-mem-context>';
@@ -350,7 +397,10 @@ export function cleanupClaudeMdLegacyBlock() {
350
397
  // Collapse any ≥3 consecutive newlines to two, then ensure exactly one trailing newline.
351
398
  const normalized = cleaned.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n');
352
399
 
353
- if (normalized === content) { dropMarker(); return; }
400
+ if (normalized === content) {
401
+ dropMarker();
402
+ return;
403
+ }
354
404
 
355
405
  // atomicWriteFileSync keeps the per-pid temp (two concurrent first-run SessionStarts in
356
406
  // one project must not rename each other's half-written temp onto the user's tracked
@@ -393,7 +443,13 @@ export function cleanupClaudeMdLegacyBlock() {
393
443
  * (D#123: the exclude-set must mirror real injections, not the keyObs query).
394
444
  * @returns {string} Joined markdown lines (without <claude-mem-context> wrappers)
395
445
  */
396
- export function buildSessionContextLines(db, project, now = new Date(), currentCcSessionId = null, collector = null) {
446
+ export function buildSessionContextLines(
447
+ db,
448
+ project,
449
+ now = new Date(),
450
+ currentCcSessionId = null,
451
+ collector = null,
452
+ ) {
397
453
  if (collector) collector.keyContextIds = [];
398
454
  // 1. Token-budgeted observation selection
399
455
  const selected = selectWithTokenBudget(db, project, 2000);
@@ -403,8 +459,10 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
403
459
  let fallbackObs = [];
404
460
  if (observations.length < 3) {
405
461
  const fbOneDayAgo = now.getTime() - STALE_SESSION_MS;
406
- const fbSevenDaysAgo = now.getTime() - FALLBACK_OBS_WINDOW_MS;
407
- fallbackObs = db.prepare(`
462
+ const fbSevenDaysAgo = now.getTime() - RELATED_OBS_WINDOW_MS;
463
+ fallbackObs = db
464
+ .prepare(
465
+ `
408
466
  SELECT id, type, title, project, created_at
409
467
  FROM observations
410
468
  WHERE ${liveObsFilterSql('')}
@@ -415,28 +473,38 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
415
473
  )
416
474
  ORDER BY created_at_epoch DESC
417
475
  LIMIT 5
418
- `).all(fbOneDayAgo, fbSevenDaysAgo);
476
+ `,
477
+ )
478
+ .all(fbOneDayAgo, fbSevenDaysAgo);
419
479
  }
420
480
 
421
481
  // 3. Latest session summary → base summaryLines
422
- const latestSummary = db.prepare(`
482
+ const latestSummary = db
483
+ .prepare(
484
+ `
423
485
  SELECT request, completed, next_steps, remaining_items, lessons, key_decisions, created_at
424
486
  FROM session_summaries
425
487
  WHERE project = ?
426
488
  ORDER BY created_at_epoch DESC
427
489
  LIMIT 1
428
- `).get(project);
490
+ `,
491
+ )
492
+ .get(project);
429
493
 
430
494
  const summaryLines = buildSummaryLines(latestSummary);
431
495
 
432
496
  // 4. Key context: top high-importance observations split into File Lessons (actionable)
433
497
  // and Key Context (informational). Pushed into summaryLines.
434
- const keyObs = db.prepare(`
498
+ const keyObs = db
499
+ .prepare(
500
+ `
435
501
  SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
436
502
  WHERE o.project = ? AND ${liveObsFilterSql('o')}
437
503
  AND COALESCE(o.importance, 1) >= 2
438
504
  ORDER BY o.created_at_epoch DESC LIMIT ${KEY_CONTEXT_LIMIT}
439
- `).all(project);
505
+ `,
506
+ )
507
+ .all(project);
440
508
 
441
509
  if (keyObs.length > 0) {
442
510
  const fileLessons = [];
@@ -457,10 +525,15 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
457
525
  fileLessons.push({ id: o.id, line: `- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})` });
458
526
  continue;
459
527
  }
460
- } catch { /* fall through to keyContext */ }
528
+ } catch {
529
+ /* fall through to keyContext */
530
+ }
461
531
  }
462
532
  const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
463
- keyContext.push({ id: o.id, line: `- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}` });
533
+ keyContext.push({
534
+ id: o.id,
535
+ line: `- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`,
536
+ });
464
537
  }
465
538
 
466
539
  // Phase A (QUIET_HOOKS) + Phase D (adopted sentinel): drop descriptive
@@ -544,18 +617,26 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
544
617
  const HANDOFF_TTL_MS = 48 * 60 * 60 * 1000;
545
618
  const handoffMinEpoch = Date.now() - HANDOFF_TTL_MS;
546
619
  const prevClearHandoff = currentCcSessionId
547
- ? db.prepare(`
620
+ ? db
621
+ .prepare(
622
+ `
548
623
  SELECT working_on, unfinished, key_files
549
624
  FROM session_handoffs
550
625
  WHERE project = ? AND type = 'clear' AND session_id = ? AND created_at_epoch > ?
551
626
  ORDER BY created_at_epoch DESC LIMIT 1
552
- `).get(project, currentCcSessionId, handoffMinEpoch)
553
- : db.prepare(`
627
+ `,
628
+ )
629
+ .get(project, currentCcSessionId, handoffMinEpoch)
630
+ : db
631
+ .prepare(
632
+ `
554
633
  SELECT working_on, unfinished, key_files
555
634
  FROM session_handoffs
556
635
  WHERE project = ? AND type = 'clear' AND created_at_epoch > ?
557
636
  ORDER BY created_at_epoch DESC LIMIT 1
558
- `).get(project, handoffMinEpoch);
637
+ `,
638
+ )
639
+ .get(project, handoffMinEpoch);
559
640
 
560
641
  const handoffLines = [];
561
642
  if (prevClearHandoff) {
@@ -570,8 +651,10 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
570
651
  if (prevClearHandoff.key_files) {
571
652
  try {
572
653
  const files = JSON.parse(prevClearHandoff.key_files);
573
- if (files.length > 0) handoffLines.push(`- Key files: ${files.map(f => basename(f)).join(', ')}`);
574
- } catch { /* malformed JSON — skip */ }
654
+ if (files.length > 0) handoffLines.push(`- Key files: ${files.map((f) => basename(f)).join(', ')}`);
655
+ } catch {
656
+ /* malformed JSON — skip */
657
+ }
575
658
  }
576
659
  handoffLines.push('');
577
660
  }
@@ -581,7 +664,9 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
581
664
  // ordinal so user can refer to "处理1" / "handle item 1" naturally; D#<id>
582
665
  // is the stable handle for tool-layer references (closes_deferred=[N]).
583
666
  // Quiet-hooks does NOT suppress: cross-session continuity is the whole point.
584
- const deferredItems = db.prepare(`
667
+ const deferredItems = db
668
+ .prepare(
669
+ `
585
670
  SELECT id, title, priority,
586
671
  ROW_NUMBER() OVER (
587
672
  ORDER BY priority DESC, created_at_epoch ASC
@@ -590,16 +675,16 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
590
675
  WHERE project = ? AND status = 'open'
591
676
  ORDER BY priority DESC, created_at_epoch ASC
592
677
  LIMIT 5
593
- `).all(project);
678
+ `,
679
+ )
680
+ .all(project);
594
681
 
595
682
  const deferredLines = [];
596
683
  if (deferredItems.length > 0) {
597
684
  deferredLines.push('### Deferred Work');
598
685
  for (const d of deferredItems) {
599
686
  const pTag = d.priority === 3 ? '🔴' : d.priority === 1 ? '⚪' : '🟡';
600
- deferredLines.push(
601
- `${d.ordinal}. ${pTag} [P${d.priority}] ${truncate(d.title, 120)} (D#${d.id})`
602
- );
687
+ deferredLines.push(`${d.ordinal}. ${pTag} [P${d.priority}] ${truncate(d.title, 120)} (D#${d.id})`);
603
688
  }
604
689
  deferredLines.push('');
605
690
  }
@@ -615,7 +700,9 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
615
700
  obsLines.push('|----|------|---|-------|');
616
701
  for (const o of obsToShow) {
617
702
  const proj = o.project && o.project !== project ? ` (${o.project})` : '';
618
- obsLines.push(`| #${o.id} | ${fmtTime(o.created_at)} | ${typeIcon(o.type)} | ${mdCell(truncate(o.title || '(untitled)', 60) + proj)} |`);
703
+ obsLines.push(
704
+ `| #${o.id} | ${fmtTime(o.created_at)} | ${typeIcon(o.type)} | ${mdCell(truncate(o.title || '(untitled)', 60) + proj)} |`,
705
+ );
619
706
  }
620
707
  }
621
708
 
@@ -623,7 +710,9 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
623
710
  // can't prematurely close the <claude-mem-context> block it's wrapped in (mdCell does
624
711
  // the same for `|`). One source of truth: both the SessionStart hook and the CLI
625
712
  // `context` command consume this return.
626
- return neutralizeContextDelimiters([...summaryLines, ...handoffLines, ...deferredLines, ...obsLines].join('\n'));
713
+ return neutralizeContextDelimiters(
714
+ [...summaryLines, ...handoffLines, ...deferredLines, ...obsLines].join('\n'),
715
+ );
627
716
  }
628
717
 
629
718
  /**
package/hook-episode.mjs CHANGED
@@ -2,7 +2,18 @@
2
2
  // Handles file-based episode storage with advisory locking and pending entry recovery
3
3
 
4
4
  import { join } from 'path';
5
- import { readFileSync, writeFileSync, unlinkSync, readdirSync, openSync, closeSync, writeSync, renameSync, statSync, constants as fsConstants } from 'fs';
5
+ import {
6
+ readFileSync,
7
+ writeFileSync,
8
+ unlinkSync,
9
+ readdirSync,
10
+ openSync,
11
+ closeSync,
12
+ writeSync,
13
+ renameSync,
14
+ statSync,
15
+ constants as fsConstants,
16
+ } from 'fs';
6
17
  import { inferProject, EDIT_TOOLS } from './utils.mjs';
7
18
  import { RUNTIME_DIR } from './hook-shared.mjs';
8
19
 
@@ -13,7 +24,9 @@ import { RUNTIME_DIR } from './hook-shared.mjs';
13
24
  export function readEpisodeRaw() {
14
25
  try {
15
26
  return JSON.parse(readFileSync(join(RUNTIME_DIR, `ep-${inferProject()}.json`), 'utf8'));
16
- } catch { return null; }
27
+ } catch {
28
+ return null;
29
+ }
17
30
  }
18
31
 
19
32
  /**
@@ -60,16 +73,28 @@ export function acquireLock(maxWaitMs = 500) {
60
73
  const age = Date.now() - (info.ts || 0);
61
74
  let stale = age > 30000; // >30s = stale
62
75
  if (!stale && info.pid) {
63
- try { process.kill(info.pid, 0); } catch (killErr) {
76
+ try {
77
+ process.kill(info.pid, 0);
78
+ } catch (killErr) {
64
79
  stale = killErr.code === 'ESRCH'; // Only stale if process truly gone
65
80
  }
66
81
  }
67
- if (stale) { try { unlinkSync(lf); } catch {} continue; }
82
+ if (stale) {
83
+ try {
84
+ unlinkSync(lf);
85
+ } catch {}
86
+ continue;
87
+ }
68
88
  } catch {
69
89
  // Can't read lock — check mtime
70
90
  try {
71
91
  const st = statSync(lf);
72
- if (Date.now() - st.mtimeMs > 30000) { try { unlinkSync(lf); } catch {} continue; }
92
+ if (Date.now() - st.mtimeMs > 30000) {
93
+ try {
94
+ unlinkSync(lf);
95
+ } catch {}
96
+ continue;
97
+ }
73
98
  } catch {}
74
99
  }
75
100
  // WARNING: Atomics.wait blocks the main thread. This is intentional and safe here
@@ -86,7 +111,9 @@ export function acquireLock(maxWaitMs = 500) {
86
111
  * Release the advisory file lock for episode buffer operations.
87
112
  */
88
113
  export function releaseLock() {
89
- try { unlinkSync(lockFile()); } catch {}
114
+ try {
115
+ unlinkSync(lockFile());
116
+ } catch {}
90
117
  }
91
118
 
92
119
  /**
@@ -116,7 +143,9 @@ export function writeEpisode(episode) {
116
143
  try {
117
144
  renameSync(tmp, target);
118
145
  } catch (err) {
119
- try { unlinkSync(tmp); } catch {}
146
+ try {
147
+ unlinkSync(tmp);
148
+ } catch {}
120
149
  throw err;
121
150
  }
122
151
  }
@@ -164,7 +193,7 @@ export function planEpisodeFlush(episode) {
164
193
  subs.push({
165
194
  ...episode,
166
195
  entries,
167
- files: [...new Set(entries.flatMap(e => e.files || []))],
196
+ files: [...new Set(entries.flatMap((e) => e.files || []))],
168
197
  filesRead: episode.filesRead,
169
198
  savedId: undefined,
170
199
  });
@@ -202,7 +231,9 @@ export function writePendingEntry(entry, sessionId, project) {
202
231
  writeFileSync(tmp, JSON.stringify({ entry, sessionId, project, ts }), { mode: 0o600 });
203
232
  renameSync(tmp, pendingFile);
204
233
  } catch {
205
- try { unlinkSync(tmp); } catch {}
234
+ try {
235
+ unlinkSync(tmp);
236
+ } catch {}
206
237
  }
207
238
  }
208
239
 
@@ -216,8 +247,12 @@ export function mergePendingEntries(episode) {
216
247
  const MAX_PENDING_MERGE = 50;
217
248
  let files;
218
249
  try {
219
- files = readdirSync(RUNTIME_DIR).filter(f => f.startsWith('pending-')).sort();
220
- } catch { return; }
250
+ files = readdirSync(RUNTIME_DIR)
251
+ .filter((f) => f.startsWith('pending-'))
252
+ .sort();
253
+ } catch {
254
+ return;
255
+ }
221
256
 
222
257
  let merged = 0;
223
258
  for (const f of files) {
@@ -226,7 +261,12 @@ export function mergePendingEntries(episode) {
226
261
  try {
227
262
  const raw = readFileSync(fp, 'utf8');
228
263
  const pending = JSON.parse(raw);
229
- if (pending.ts < oneHourAgo) { try { unlinkSync(fp); } catch {} continue; }
264
+ if (pending.ts < oneHourAgo) {
265
+ try {
266
+ unlinkSync(fp);
267
+ } catch {}
268
+ continue;
269
+ }
230
270
  // Only merge entries belonging to the same project
231
271
  if (pending.project && episode.project && pending.project !== episode.project) continue;
232
272
  if (pending.entry) {
@@ -237,11 +277,15 @@ export function mergePendingEntries(episode) {
237
277
  merged++;
238
278
  } else {
239
279
  // No entry data — clean up the file without merging
240
- try { unlinkSync(fp); } catch {}
280
+ try {
281
+ unlinkSync(fp);
282
+ } catch {}
241
283
  }
242
284
  } catch {
243
285
  // Corrupt pending file — remove
244
- try { unlinkSync(fp); } catch {}
286
+ try {
287
+ unlinkSync(fp);
288
+ } catch {}
245
289
  }
246
290
  }
247
291
  }
@@ -390,30 +434,39 @@ const RESEARCH_ENTRY_THRESHOLD = 8;
390
434
  */
391
435
  export function explainSignificance(episode) {
392
436
  const entries = episode?.entries || [];
393
- const grepCount = entries.filter(e => e.tool === 'Grep').length;
394
- const readCount = entries.filter(e => e.tool === 'Read' || e.tool === 'Grep').length;
437
+ const grepCount = entries.filter((e) => e.tool === 'Grep').length;
438
+ const readCount = entries.filter((e) => e.tool === 'Read' || e.tool === 'Grep').length;
395
439
  const base = { readCount, grepCount, grepDecisive: false };
396
440
 
397
441
  // 1. File edits → always significant (code changes matter)
398
- if (entries.some(e => EDIT_TOOLS.has(e.tool))) return { ...base, significant: true, rule: 1 };
442
+ if (entries.some((e) => EDIT_TOOLS.has(e.tool))) return { ...base, significant: true, rule: 1 };
399
443
 
400
444
  // 2. Test/build errors → significant (actionable failures)
401
445
  // Plain bash errors without edits are noise (e.g. typos, exploration errors)
402
- if (entries.some(e => e.tool === 'Bash' && e.isError && (e.bashSig?.isTest || e.bashSig?.isBuild))) {
446
+ if (entries.some((e) => e.tool === 'Bash' && e.isError && (e.bashSig?.isTest || e.bashSig?.isBuild))) {
403
447
  return { ...base, significant: true, rule: 2 };
404
448
  }
405
449
 
406
450
  // 3. Important files touched (config, schema, security, migration)
407
451
  // Checks episode.files (all touched files, including reads) — catches important-file investigation
408
452
  const allFiles = episode?.files || [];
409
- if (allFiles.some(f =>
410
- /\.(env|yml|yaml|toml|lock|sql|prisma|proto)$/.test(f) ||
411
- /(config|schema|migration|auth|security)/i.test(f)
412
- )) return { ...base, significant: true, rule: 3 };
453
+ if (
454
+ allFiles.some(
455
+ (f) =>
456
+ /\.(env|yml|yaml|toml|lock|sql|prisma|proto)$/.test(f) ||
457
+ /(config|schema|migration|auth|security)/i.test(f),
458
+ )
459
+ )
460
+ return { ...base, significant: true, rule: 3 };
413
461
 
414
462
  // 4. Research pattern: reading many files indicates investigation
415
463
  if (readCount >= RESEARCH_ENTRY_THRESHOLD) {
416
- return { ...base, significant: true, rule: 4, grepDecisive: readCount - grepCount < RESEARCH_ENTRY_THRESHOLD };
464
+ return {
465
+ ...base,
466
+ significant: true,
467
+ rule: 4,
468
+ grepDecisive: readCount - grepCount < RESEARCH_ENTRY_THRESHOLD,
469
+ };
417
470
  }
418
471
  return { ...base, significant: false, rule: null };
419
472
  }