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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- 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,
|
|
13
|
-
|
|
14
|
-
|
|
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 {
|
|
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 ?? '')
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
210
|
+
`,
|
|
211
|
+
)
|
|
212
|
+
.all(project, tier1Ago, tier2Ago, tier3Ago);
|
|
189
213
|
|
|
190
|
-
const sessPool = db
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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 {
|
|
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 {
|
|
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 = () => {
|
|
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) {
|
|
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(
|
|
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() -
|
|
407
|
-
fallbackObs = db
|
|
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
|
-
|
|
476
|
+
`,
|
|
477
|
+
)
|
|
478
|
+
.all(fbOneDayAgo, fbSevenDaysAgo);
|
|
419
479
|
}
|
|
420
480
|
|
|
421
481
|
// 3. Latest session summary → base summaryLines
|
|
422
|
-
const latestSummary = db
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
528
|
+
} catch {
|
|
529
|
+
/* fall through to keyContext */
|
|
530
|
+
}
|
|
461
531
|
}
|
|
462
532
|
const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
|
|
463
|
-
keyContext.push({
|
|
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
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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) {
|
|
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) {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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)
|
|
220
|
-
|
|
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) {
|
|
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 {
|
|
280
|
+
try {
|
|
281
|
+
unlinkSync(fp);
|
|
282
|
+
} catch {}
|
|
241
283
|
}
|
|
242
284
|
} catch {
|
|
243
285
|
// Corrupt pending file — remove
|
|
244
|
-
try {
|
|
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 (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 {
|
|
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
|
}
|