claude-mem-lite 3.64.0 → 3.66.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/deep-search.mjs +2 -1
- package/format-utils.mjs +2 -1
- package/haiku-client.mjs +39 -6
- package/hook-context.mjs +28 -22
- package/hook-handoff.mjs +3 -4
- package/hook-llm.mjs +7 -5
- package/hook-memory.mjs +4 -3
- package/hook-optimize.mjs +15 -16
- package/hook-precompact.mjs +15 -2
- package/hook-shared.mjs +97 -5
- package/hook.mjs +59 -20
- package/lib/citation-tracker.mjs +47 -3
- package/lib/db-backup.mjs +2 -1
- package/lib/deferred-work.mjs +1 -1
- package/lib/err-sampler.mjs +1 -1
- package/lib/hook-telemetry.mjs +1 -1
- package/lib/inject-search-core.mjs +16 -13
- package/lib/injected-ids.mjs +20 -0
- package/lib/keyctx-marker.mjs +71 -0
- package/lib/maintain-core.mjs +5 -4
- package/lib/metrics.mjs +1 -1
- package/lib/recall-core.mjs +2 -2
- package/lib/recent-core.mjs +3 -1
- package/lib/save-enrich.mjs +3 -2
- package/lib/search-core.mjs +6 -4
- package/lib/stats-core.mjs +6 -4
- package/lib/stats-quality.mjs +2 -1
- package/lib/time-constants.mjs +21 -0
- package/lib/timeline-core.mjs +7 -6
- package/mem-cli.mjs +19 -19
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -1
- package/registry-enricher.mjs +2 -2
- package/registry-recommend.mjs +3 -2
- package/scoring-sql.mjs +8 -7
- package/scripts/pre-tool-recall.js +2 -1
- package/scripts/user-prompt-search.js +2 -1
- package/search-scoring.mjs +4 -3
- package/server.mjs +10 -4
- package/source-files.mjs +6 -0
- package/tfidf.mjs +3 -3
- package/tier.mjs +4 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.66.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/deep-search.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { sanitizeFtsQuery } from './utils.mjs';
|
|
|
36
36
|
import { RRF_K } from './tfidf.mjs';
|
|
37
37
|
import { rrfAccumulate } from './lib/rrf.mjs';
|
|
38
38
|
import { llmRerankOrder, defaultRerankLLM } from './rerank.mjs';
|
|
39
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
39
40
|
|
|
40
41
|
// original + up to 3 rewrites (keyword / concept-expansion / HyDE).
|
|
41
42
|
export const MAX_VARIANTS = 4;
|
|
@@ -68,7 +69,7 @@ export const AUTO_DEEP_MIN_CORPUS = 10;
|
|
|
68
69
|
*/
|
|
69
70
|
export function hasEscalatableCorpus(db, project, min = AUTO_DEEP_MIN_CORPUS) {
|
|
70
71
|
try {
|
|
71
|
-
const where = ['
|
|
72
|
+
const where = [liveObsFilterSql('')];
|
|
72
73
|
const params = [];
|
|
73
74
|
if (project) { where.push('project = ?'); params.push(project); }
|
|
74
75
|
const row = db.prepare(`SELECT COUNT(*) AS c FROM observations WHERE ${where.join(' AND ')}`).get(...params);
|
package/format-utils.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
1
2
|
// claude-mem-lite: String formatting and display utilities
|
|
2
3
|
// Extracted from utils.mjs for focused responsibility
|
|
3
4
|
|
|
@@ -231,7 +232,7 @@ export function isoWeekKey(epochMs) {
|
|
|
231
232
|
const tmp = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
232
233
|
tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay() || 7));
|
|
233
234
|
const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
|
|
234
|
-
const weekNum = Math.ceil(((tmp - yearStart) /
|
|
235
|
+
const weekNum = Math.ceil(((tmp - yearStart) / DAY_MS + 1) / 7);
|
|
235
236
|
const isoYear = tmp.getUTCFullYear();
|
|
236
237
|
return `${isoYear}-W${String(weekNum).padStart(2, '0')}`;
|
|
237
238
|
}
|
package/haiku-client.mjs
CHANGED
|
@@ -100,6 +100,28 @@ const MODEL_MAP = {
|
|
|
100
100
|
// A call that genuinely needs sampling can pass opts.temperature to override.
|
|
101
101
|
const DEFAULT_LLM_TEMPERATURE = 0;
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Timeout budget for BACKGROUND LLM work (detached enrich/optimize/summary
|
|
105
|
+
* workers, registry indexing) — the calls with no latency budget at all.
|
|
106
|
+
*
|
|
107
|
+
* Every dispatcher below degrades to `claude -p` when the keyed provider fails,
|
|
108
|
+
* and the CLI leg pays a full Claude Code boot before inference: measured
|
|
109
|
+
* 8.1s / 9.2s / 11.7s / 13.4s on an idle machine for a 400-token JSON reply
|
|
110
|
+
* (2026-08-16), against API-leg latencies under 2s. Callers that sized their
|
|
111
|
+
* timeout for the API leg (15–20s) were therefore killing the fallback
|
|
112
|
+
* mid-flight — save-enrich's 15s budget left 1.6s of headroom over the worst
|
|
113
|
+
* sample, which is how 6/57 (10.5%) of instrumented runs landed on
|
|
114
|
+
* reason:'llm-null' and why manual saves stopped getting search_aliases.
|
|
115
|
+
*
|
|
116
|
+
* Deliberately NOT applied as a floor inside callModelCLI / callHaikuCLI /
|
|
117
|
+
* callModelCLIAsync: those are also reached from latency-bound callers — the
|
|
118
|
+
* lesson bridge's 2.5s fail-open budget on the PreToolUse hook, and deep-search
|
|
119
|
+
* rerank on the MCP request path — where failing fast beats blocking a user for
|
|
120
|
+
* 45s. The allowance is caller-side policy, not a clamp.
|
|
121
|
+
* Pinned both ways by `tests/llm-timeout-budget.test.mjs`.
|
|
122
|
+
*/
|
|
123
|
+
export const BG_LLM_TIMEOUT_MS = 45000;
|
|
124
|
+
|
|
103
125
|
/**
|
|
104
126
|
* Resolve the LLM model to use for background calls.
|
|
105
127
|
* Reads CLAUDE_MEM_MODEL env var, defaults to 'haiku'.
|
|
@@ -409,11 +431,20 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
409
431
|
function callModelCLI(prompt, model, { timeout }) {
|
|
410
432
|
const modelName = MODEL_MAP[model] ? model : 'haiku';
|
|
411
433
|
try {
|
|
412
|
-
|
|
434
|
+
// --no-session-persistence + DISABLE_CLAUDEMD_HOOKS (2026-08-16): these
|
|
435
|
+
// headless calls were paying the full interactive-session tax — 1,004
|
|
436
|
+
// transcripts piled up in ~/.claude/projects/-tmp/, and every spawn ran
|
|
437
|
+
// the claudemd plugin's whole hook fan-out (its SessionStart banner alone
|
|
438
|
+
// logged 682 rows in 3 days, drowning that project's telemetry). The
|
|
439
|
+
// persistence flag is OAuth-safe (probed); `--bare`/CLAUDE_CODE_SIMPLE are
|
|
440
|
+
// NOT (they hard-require ANTHROPIC_API_KEY — "Not logged in" on OAuth
|
|
441
|
+
// machines). The user's global CLAUDE.md injection has no OAuth-safe
|
|
442
|
+
// opt-out; accepted (haiku + prompt caching keeps it cheap).
|
|
443
|
+
const result = execFileSync(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
|
|
413
444
|
input: flattenForCLI(prompt),
|
|
414
445
|
timeout,
|
|
415
446
|
encoding: 'utf8',
|
|
416
|
-
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
|
|
447
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
|
|
417
448
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
418
449
|
cwd: '/tmp',
|
|
419
450
|
});
|
|
@@ -453,8 +484,9 @@ export function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
453
484
|
const modelName = MODEL_MAP[model] ? model : 'haiku';
|
|
454
485
|
let child;
|
|
455
486
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
487
|
+
// Same headless-tax flags as callModelCLI (rationale there).
|
|
488
|
+
child = spawn(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
|
|
489
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
|
|
458
490
|
cwd: '/tmp',
|
|
459
491
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
460
492
|
});
|
|
@@ -603,11 +635,12 @@ async function callOpenRouterAPI(prompt, tier, { timeout, maxTokens, temperature
|
|
|
603
635
|
function callHaikuCLI(prompt, { timeout }) {
|
|
604
636
|
const { cli: modelName } = resolveModel();
|
|
605
637
|
try {
|
|
606
|
-
|
|
638
|
+
// Same headless-tax flags as callModelCLI (rationale there).
|
|
639
|
+
const result = execFileSync(getClaudePath(), ['-p', '--model', modelName, '--no-session-persistence'], {
|
|
607
640
|
input: flattenForCLI(prompt),
|
|
608
641
|
timeout,
|
|
609
642
|
encoding: 'utf8',
|
|
610
|
-
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
|
|
643
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
|
|
611
644
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
612
645
|
cwd: '/tmp', // Prevent ghost sessions in user's /resume list
|
|
613
646
|
});
|
package/hook-context.mjs
CHANGED
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
debugLog, debugCatch, neutralizeContextDelimiters,
|
|
13
13
|
DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause,
|
|
14
14
|
} from './utils.mjs';
|
|
15
|
-
import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet, isQuietHooks } from './hook-shared.mjs';
|
|
15
|
+
import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet, isQuietHooks, KEY_CONTEXT_LIMIT } from './hook-shared.mjs';
|
|
16
16
|
import { extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
17
17
|
import { recentInjectableEvents, renderInjectableEvent } from './lib/events-injection.mjs';
|
|
18
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
18
19
|
|
|
20
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
19
21
|
/**
|
|
20
22
|
* Infer the project directory from environment variables or cwd.
|
|
21
23
|
* @returns {string} Absolute path to the project directory
|
|
@@ -42,7 +44,7 @@ function mdCell(s) {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export function computeAdaptiveWindows(db, project) {
|
|
45
|
-
const sevenDaysAgo = Date.now() - 7 *
|
|
47
|
+
const sevenDaysAgo = Date.now() - 7 * DAY_MS;
|
|
46
48
|
const row = db.prepare(`
|
|
47
49
|
SELECT COUNT(*) as c FROM observations
|
|
48
50
|
WHERE project = ? AND created_at_epoch > ? AND COALESCE(compressed_into, 0) = 0
|
|
@@ -51,13 +53,13 @@ export function computeAdaptiveWindows(db, project) {
|
|
|
51
53
|
|
|
52
54
|
if (velocity > 10) {
|
|
53
55
|
// High velocity: tighter windows, focus on very recent
|
|
54
|
-
return { tier1: 12 * 3600000, tier2: 3 *
|
|
56
|
+
return { tier1: 12 * 3600000, tier2: 3 * DAY_MS, tier3: 14 * DAY_MS, sessWindow: 3 * DAY_MS };
|
|
55
57
|
} else if (velocity >= 3) {
|
|
56
58
|
// Medium velocity: default windows
|
|
57
|
-
return { tier1: 24 * 3600000, tier2: 7 *
|
|
59
|
+
return { tier1: 24 * 3600000, tier2: 7 * DAY_MS, tier3: 30 * DAY_MS, sessWindow: 7 * DAY_MS };
|
|
58
60
|
} else {
|
|
59
61
|
// Low velocity: wider windows, older data still relevant
|
|
60
|
-
return { tier1: 48 * 3600000, tier2: 14 *
|
|
62
|
+
return { tier1: 48 * 3600000, tier2: 14 * DAY_MS, tier3: 60 * DAY_MS, sessWindow: 14 * DAY_MS };
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -83,12 +85,7 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
|
|
|
83
85
|
const obsPool = db.prepare(`
|
|
84
86
|
SELECT id, type, title, narrative, importance, created_at_epoch, files_modified, lesson_learned
|
|
85
87
|
FROM observations
|
|
86
|
-
WHERE project = ? AND
|
|
87
|
-
-- superseded invisibility: auto-dedup (hook.mjs) sets superseded_at but leaves
|
|
88
|
-
-- compressed_into=0, so the compressed filter alone lets the hidden near-duplicate
|
|
89
|
-
-- resurface in the most-visible surface (injected every SessionStart). Sibling
|
|
90
|
-
-- keyObs already filters this; obsPool + fallbackObs must match.
|
|
91
|
-
AND superseded_at IS NULL
|
|
88
|
+
WHERE project = ? AND ${liveObsFilterSql('')}
|
|
92
89
|
AND ${notLowSignalTitleClause('')}
|
|
93
90
|
AND (
|
|
94
91
|
(created_at_epoch > ? AND importance >= 1)
|
|
@@ -293,9 +290,15 @@ export function cleanupClaudeMdLegacyBlock() {
|
|
|
293
290
|
* @param {string|null} [currentCcSessionId=null] Claude Code session id — when provided,
|
|
294
291
|
* the "Working State (from /clear)" block is filtered to handoffs owned by this
|
|
295
292
|
* session, preventing parallel-session bleed (see docs/bug.txt).
|
|
293
|
+
* @param {object|null} [collector=null] Optional out-param: when given, its
|
|
294
|
+
* `keyContextIds` property is set to the obs ids ACTUALLY rendered into the
|
|
295
|
+
* File Lessons / Key Context sections ([] under quiet/adopted or when the
|
|
296
|
+
* sections are empty). handleUserPrompt persists this as its exclude-set
|
|
297
|
+
* (D#123: the exclude-set must mirror real injections, not the keyObs query).
|
|
296
298
|
* @returns {string} Joined markdown lines (without <claude-mem-context> wrappers)
|
|
297
299
|
*/
|
|
298
|
-
export function buildSessionContextLines(db, project, now = new Date(), currentCcSessionId = null) {
|
|
300
|
+
export function buildSessionContextLines(db, project, now = new Date(), currentCcSessionId = null, collector = null) {
|
|
301
|
+
if (collector) collector.keyContextIds = [];
|
|
299
302
|
// 1. Token-budgeted observation selection
|
|
300
303
|
const selected = selectWithTokenBudget(db, project, 2000);
|
|
301
304
|
const observations = selected.observations;
|
|
@@ -308,8 +311,7 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
308
311
|
fallbackObs = db.prepare(`
|
|
309
312
|
SELECT id, type, title, project, created_at
|
|
310
313
|
FROM observations
|
|
311
|
-
WHERE
|
|
312
|
-
AND superseded_at IS NULL
|
|
314
|
+
WHERE ${liveObsFilterSql('')}
|
|
313
315
|
AND ${notLowSignalTitleClause('')}
|
|
314
316
|
AND (
|
|
315
317
|
(created_at_epoch > ? AND importance >= 1)
|
|
@@ -335,10 +337,9 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
335
337
|
// and Key Context (informational). Pushed into summaryLines.
|
|
336
338
|
const keyObs = db.prepare(`
|
|
337
339
|
SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
|
|
338
|
-
WHERE o.project = ? AND
|
|
339
|
-
AND o.superseded_at IS NULL
|
|
340
|
+
WHERE o.project = ? AND ${liveObsFilterSql('o')}
|
|
340
341
|
AND COALESCE(o.importance, 1) >= 2
|
|
341
|
-
ORDER BY o.created_at_epoch DESC LIMIT
|
|
342
|
+
ORDER BY o.created_at_epoch DESC LIMIT ${KEY_CONTEXT_LIMIT}
|
|
342
343
|
`).all(project);
|
|
343
344
|
|
|
344
345
|
if (keyObs.length > 0) {
|
|
@@ -357,30 +358,35 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
357
358
|
const files = JSON.parse(o.files_modified);
|
|
358
359
|
const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
|
|
359
360
|
if (fname) {
|
|
360
|
-
fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
|
|
361
|
+
fileLessons.push({ id: o.id, line: `- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})` });
|
|
361
362
|
continue;
|
|
362
363
|
}
|
|
363
364
|
} catch { /* fall through to keyContext */ }
|
|
364
365
|
}
|
|
365
366
|
const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
|
|
366
|
-
keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
|
|
367
|
+
keyContext.push({ id: o.id, line: `- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}` });
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
// Phase A (QUIET_HOOKS) + Phase D (adopted sentinel): drop descriptive
|
|
370
371
|
// File Lessons / Key Context sections when the user has opted into low-noise
|
|
371
372
|
// hooks OR adopted invited-memory (MEMORY.md sentinel carries the triggers
|
|
372
373
|
// at higher system-prompt authority). The Recent table still fires so #IDs
|
|
373
|
-
// remain reachable via mem_get.
|
|
374
|
+
// remain reachable via mem_get. The collector sees only rows that survive
|
|
375
|
+
// BOTH the quiet gate and the per-section slice — rendered rows, nothing else.
|
|
374
376
|
const quiet = effectiveQuiet();
|
|
375
377
|
if (fileLessons.length > 0 && !quiet) {
|
|
378
|
+
const shown = fileLessons.slice(0, 5);
|
|
376
379
|
summaryLines.push('### File Lessons');
|
|
377
|
-
summaryLines.push(...
|
|
380
|
+
summaryLines.push(...shown.map((e) => e.line));
|
|
378
381
|
summaryLines.push('');
|
|
382
|
+
if (collector) collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
379
383
|
}
|
|
380
384
|
if (keyContext.length > 0 && !quiet) {
|
|
385
|
+
const shown = keyContext.slice(0, 5);
|
|
381
386
|
summaryLines.push('### Key Context');
|
|
382
|
-
summaryLines.push(...
|
|
387
|
+
summaryLines.push(...shown.map((e) => e.line));
|
|
383
388
|
summaryLines.push('');
|
|
389
|
+
if (collector) collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
384
390
|
}
|
|
385
391
|
} else if (!latestSummary && !effectiveQuiet()) {
|
|
386
392
|
// Fallback: no summary AND no key observations — show recent activity.
|
package/hook-handoff.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
// immutable in ESM and cannot be mocked after the fact.
|
|
14
14
|
import * as gitStateModule from './lib/git-state.mjs';
|
|
15
15
|
import * as taskReaderModule from './lib/task-reader.mjs';
|
|
16
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Build and save a handoff snapshot to session_handoffs table.
|
|
@@ -76,8 +77,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
76
77
|
if (subjectPrompts.length === 0) {
|
|
77
78
|
const fallback = db.prepare(`
|
|
78
79
|
SELECT title FROM observations
|
|
79
|
-
WHERE project = ? AND
|
|
80
|
-
AND superseded_at IS NULL
|
|
80
|
+
WHERE project = ? AND ${liveObsFilterSql('')}
|
|
81
81
|
AND COALESCE(importance, 1) >= 3
|
|
82
82
|
AND ${notLowSignalTitleClause('')}
|
|
83
83
|
ORDER BY created_at_epoch DESC LIMIT 1
|
|
@@ -185,8 +185,7 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
185
185
|
const decisions = db.prepare(`
|
|
186
186
|
SELECT title FROM observations
|
|
187
187
|
WHERE memory_session_id = ? AND COALESCE(importance, 1) >= 2
|
|
188
|
-
AND
|
|
189
|
-
AND superseded_at IS NULL ${obsWindowClause}
|
|
188
|
+
AND ${liveObsFilterSql('')} ${obsWindowClause}
|
|
190
189
|
ORDER BY created_at_epoch DESC LIMIT 10
|
|
191
190
|
`).all(sessionId, ...obsWindowParams).filter(d => d.title && !LOW_SIGNAL_TITLE.test(d.title)).slice(0, 5);
|
|
192
191
|
|
package/hook-llm.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getCurrentBranch, notLowSignalTitleClause,
|
|
11
11
|
} from './utils.mjs';
|
|
12
12
|
import { acquireLLMSlot, releaseLLMSlot } from './hook-semaphore.mjs';
|
|
13
|
+
import { BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
|
|
13
14
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
14
15
|
import { getVocabulary, computeVector, vecTextForRow } from './tfidf.mjs';
|
|
15
16
|
import { insertObservationRow, insertObservationFiles, insertObservationVector, normalizeScope } from './lib/observation-write.mjs';
|
|
@@ -23,6 +24,7 @@ import { isNoiseObservation, capNoiseImportance, isLowYieldChangeObs } from './l
|
|
|
23
24
|
import { episodeHasSignificantContent } from './hook-episode.mjs';
|
|
24
25
|
import { OBS_TYPE_SET } from './lib/obs-types.mjs';
|
|
25
26
|
|
|
27
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
26
28
|
// T9: memdir-incompatible types live in the `events` table, not `observations`.
|
|
27
29
|
// Set lookup is O(1) — authoritative source is lib/activity.mjs::EVENT_TYPES.
|
|
28
30
|
const EVENT_TYPE_SET = new Set(EVENT_TYPES);
|
|
@@ -84,7 +86,7 @@ export function recordRetryAttempt(db, recovered, bucket = dateBucketUtc()) {
|
|
|
84
86
|
* YYYY-MM-DD lexicographic order).
|
|
85
87
|
*/
|
|
86
88
|
export function readRetryStats(db, days = 30) {
|
|
87
|
-
const cutoff = new Date(Date.now() - days *
|
|
89
|
+
const cutoff = new Date(Date.now() - days * DAY_MS);
|
|
88
90
|
return db.prepare(
|
|
89
91
|
`SELECT date_bucket, attempts, recovered FROM lesson_retry_stats
|
|
90
92
|
WHERE date_bucket >= ? ORDER BY date_bucket DESC`
|
|
@@ -205,8 +207,8 @@ export function saveObservation(obs, projectOverride, sessionIdOverride, externa
|
|
|
205
207
|
// 3-day Jaccard catches near-duplicates without blocking legitimately new observations
|
|
206
208
|
const LOW_SIGNAL = LOW_SIGNAL_TITLE;
|
|
207
209
|
if (obs.title && LOW_SIGNAL.test(obs.title)) {
|
|
208
|
-
const sevenDaysAgo = now.getTime() - 7 *
|
|
209
|
-
const threeDaysAgo = now.getTime() - 3 *
|
|
210
|
+
const sevenDaysAgo = now.getTime() - 7 * DAY_MS;
|
|
211
|
+
const threeDaysAgo = now.getTime() - 3 * DAY_MS;
|
|
210
212
|
// Phase 1: exact title match within 7 days
|
|
211
213
|
const exactDup = db.prepare(`
|
|
212
214
|
SELECT 1 FROM observations
|
|
@@ -856,7 +858,7 @@ ${actionList}`;
|
|
|
856
858
|
const retrySlot = await acquireLLMSlot();
|
|
857
859
|
try {
|
|
858
860
|
const retryPrompt = buildLessonRetryPrompt(episode, parsed);
|
|
859
|
-
const retryRaw = retrySlot ? await callLLM(retryPrompt,
|
|
861
|
+
const retryRaw = retrySlot ? await callLLM(retryPrompt, BG_LLM_TIMEOUT_MS) : null;
|
|
860
862
|
if (retryRaw) {
|
|
861
863
|
const retry = parseJsonFromLLM(retryRaw);
|
|
862
864
|
const retryLesson = typeof retry?.lesson === 'string' ? retry.lesson.trim() : '';
|
|
@@ -1135,7 +1137,7 @@ ${obsList}`;
|
|
|
1135
1137
|
|
|
1136
1138
|
let raw, llmParsed;
|
|
1137
1139
|
try {
|
|
1138
|
-
raw = await callLLM(prompt,
|
|
1140
|
+
raw = await callLLM(prompt, BG_LLM_TIMEOUT_MS);
|
|
1139
1141
|
llmParsed = parseJsonFromLLM(raw);
|
|
1140
1142
|
} finally {
|
|
1141
1143
|
releaseLLMSlot();
|
package/hook-memory.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import { DB_DIR } from './schema.mjs';
|
|
|
9
9
|
import { extractIdents } from './lib/lesson-idents.mjs';
|
|
10
10
|
import { formatSubagentContext } from './lib/task-imperative.mjs';
|
|
11
11
|
|
|
12
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
12
13
|
const MAX_MEMORY_INJECTIONS = 3;
|
|
13
|
-
const MEMORY_LOOKBACK_MS = 60 *
|
|
14
|
+
const MEMORY_LOOKBACK_MS = 60 * DAY_MS; // 60 days
|
|
14
15
|
// Aligned with TYPE_QUALITY_CASE in scoring-sql.mjs (R2 rebalance).
|
|
15
16
|
// Weights calibrated to empirical avg access_count:
|
|
16
17
|
// decision 6.05, discovery 3.32, bugfix 2.24, feature 2.04, change 0.93, refactor 0.54.
|
|
@@ -98,7 +99,7 @@ function candidateCoverage(row, queryTerms) {
|
|
|
98
99
|
return hits / queryTerms.length;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
const FILE_RECALL_LOOKBACK_MS = 60 *
|
|
102
|
+
const FILE_RECALL_LOOKBACK_MS = 60 * DAY_MS; // 60 days
|
|
102
103
|
const MAX_FILE_RECALL = 2;
|
|
103
104
|
|
|
104
105
|
// P1: stale-obs verify-before-use threshold. An injected obs older than this
|
|
@@ -107,7 +108,7 @@ const MAX_FILE_RECALL = 2;
|
|
|
107
108
|
// renamed since capture. Pure-decision/architecture obs (no file_paths)
|
|
108
109
|
// don't get the hint: their drift is text-only and Claude already verifies
|
|
109
110
|
// at consumption time per the project mem-usage contract.
|
|
110
|
-
const STALE_OBS_THRESHOLD_MS = 30 *
|
|
111
|
+
const STALE_OBS_THRESHOLD_MS = 30 * DAY_MS;
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
114
|
* Format a single line for the <memory-context> block emitted by
|
package/hook-optimize.mjs
CHANGED
|
@@ -13,14 +13,16 @@ import {
|
|
|
13
13
|
computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity, clampImportance, cjkBigrams,
|
|
14
14
|
notLowSignalTitleClause, scrubSecrets,
|
|
15
15
|
} from './utils.mjs';
|
|
16
|
-
import { callModelJSON } from './haiku-client.mjs';
|
|
16
|
+
import { callModelJSON, BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
|
|
17
17
|
import { acquireLLMSlot, releaseLLMSlot } from './hook-semaphore.mjs';
|
|
18
18
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
19
19
|
import { getVocabulary, computeVector, cosineSimilarity, vecTextForRow } from './tfidf.mjs';
|
|
20
20
|
import { MERGE_JACCARD_LOW, AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
21
21
|
import { DB_DIR } from './schema.mjs';
|
|
22
22
|
import { OBS_TYPE_SET } from './lib/obs-types.mjs';
|
|
23
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
23
24
|
|
|
25
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
24
26
|
const RUNTIME_DIR = join(DB_DIR, 'runtime');
|
|
25
27
|
|
|
26
28
|
// ─── Budget ─────────────────────────────────────────────────────────────────
|
|
@@ -104,8 +106,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
104
106
|
const stmt = db.prepare(`
|
|
105
107
|
SELECT id, title, narrative, type, subtitle, concepts, facts, text, search_aliases, importance, project
|
|
106
108
|
FROM observations
|
|
107
|
-
WHERE
|
|
108
|
-
AND superseded_at IS NULL
|
|
109
|
+
WHERE ${liveObsFilterSql('')}
|
|
109
110
|
AND (search_aliases IS NULL OR search_aliases = '')
|
|
110
111
|
AND LENGTH(COALESCE(narrative, '')) > 100
|
|
111
112
|
AND ${notLowSignalTitleClause('')}
|
|
@@ -119,8 +120,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
119
120
|
const stmt = db.prepare(`
|
|
120
121
|
SELECT id, title, narrative, type, subtitle, concepts, facts, search_aliases, importance, project
|
|
121
122
|
FROM observations
|
|
122
|
-
WHERE
|
|
123
|
-
AND superseded_at IS NULL
|
|
123
|
+
WHERE ${liveObsFilterSql('')}
|
|
124
124
|
AND optimized_at IS NULL
|
|
125
125
|
AND type IN ('bugfix','refactor','feature','decision')
|
|
126
126
|
AND (lesson_learned IS NULL OR lesson_learned = '')
|
|
@@ -175,7 +175,7 @@ Narrative: ${truncate(cand.narrative || '(no narrative)', 500)}
|
|
|
175
175
|
|
|
176
176
|
JSON: {"search_aliases":["alt phrasing","synonym","spelled-out jargon","CJK term if the domain word has one"]}
|
|
177
177
|
Give 3-6 aliases: words a user might search for the SAME concept but that are NOT already in the title (synonyms, the spelled-out form of an acronym, the jargon term for a described symptom, a CJK translation of a key domain term).`;
|
|
178
|
-
const parsed = await callModelJSON(aliasPrompt, 'haiku', { timeout:
|
|
178
|
+
const parsed = await callModelJSON(aliasPrompt, 'haiku', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 300 });
|
|
179
179
|
const aliasArr = parsed && Array.isArray(parsed.search_aliases)
|
|
180
180
|
? parsed.search_aliases.filter((a) => typeof a === 'string' && a.trim().length > 0)
|
|
181
181
|
: [];
|
|
@@ -204,7 +204,7 @@ importance: 0=no value, 1=routine, 2=notable non-obvious insight, 3=critical. De
|
|
|
204
204
|
lesson_learned: State what was learned. If routine, write "none".
|
|
205
205
|
search_aliases: 2-6 alternative search terms (include CJK if applicable).`;
|
|
206
206
|
|
|
207
|
-
const parsed = await callModelJSON(prompt, 'haiku', { timeout:
|
|
207
|
+
const parsed = await callModelJSON(prompt, 'haiku', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 500 });
|
|
208
208
|
if (!parsed || !parsed.title) { skipped++; continue; }
|
|
209
209
|
|
|
210
210
|
// Auto-hide on importance:0 targets fully-degraded NARROW rows (this branch predates
|
|
@@ -289,7 +289,7 @@ search_aliases: 2-6 alternative search terms (include CJK if applicable).`;
|
|
|
289
289
|
// ─── Task 2: Normalize ─────────────────────────────────────────────────────
|
|
290
290
|
|
|
291
291
|
const NORMALIZE_GATE_FILE = join(RUNTIME_DIR, 'last-normalize.json');
|
|
292
|
-
const NORMALIZE_INTERVAL_MS = 7 *
|
|
292
|
+
const NORMALIZE_INTERVAL_MS = 7 * DAY_MS; // 7 days
|
|
293
293
|
|
|
294
294
|
// Pure gate decision (no IO) — exported for testing. Fail-OPEN on a
|
|
295
295
|
// malformed-but-valid-JSON gate: a missing/non-numeric `epoch` makes
|
|
@@ -356,7 +356,7 @@ Rules:
|
|
|
356
356
|
- Include CJK ↔ English equivalents if present
|
|
357
357
|
- Skip terms that have no synonyms in the list`;
|
|
358
358
|
|
|
359
|
-
const parsed = await callModelJSON(prompt, 'sonnet', { timeout:
|
|
359
|
+
const parsed = await callModelJSON(prompt, 'sonnet', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 1000 });
|
|
360
360
|
if (!parsed?.groups || !Array.isArray(parsed.groups)) return [];
|
|
361
361
|
return parsed.groups.filter(g => g.canonical && Array.isArray(g.aliases) && g.aliases.length > 0);
|
|
362
362
|
} catch (e) {
|
|
@@ -448,7 +448,7 @@ export async function executeNormalize(db, force = false, { project } = {}) {
|
|
|
448
448
|
|
|
449
449
|
// ─── Task 3: Cluster-merge ─────────────────────────────────────────────────
|
|
450
450
|
|
|
451
|
-
const MERGE_TIME_WINDOW_MS = 30 *
|
|
451
|
+
const MERGE_TIME_WINDOW_MS = 30 * DAY_MS;
|
|
452
452
|
// Merge-review band [MERGE_JACCARD_LOW, AUTO_MERGE_THRESHOLD): titles in this
|
|
453
453
|
// Jaccard range are LLM-reviewed for merge; at/above AUTO_MERGE_THRESHOLD they'd
|
|
454
454
|
// already auto-merge elsewhere, below MERGE_JACCARD_LOW they're too dissimilar.
|
|
@@ -459,8 +459,7 @@ export function findMergeCandidates(db, maxClusters = 5, { project } = {}) {
|
|
|
459
459
|
const stmt = db.prepare(`
|
|
460
460
|
SELECT id, title, narrative, project, type, access_count, importance, created_at_epoch, minhash_sig, lesson_learned, concepts, facts
|
|
461
461
|
FROM observations
|
|
462
|
-
WHERE
|
|
463
|
-
AND superseded_at IS NULL
|
|
462
|
+
WHERE ${liveObsFilterSql('')}
|
|
464
463
|
AND optimized_at IS NULL
|
|
465
464
|
AND title IS NOT NULL AND title != ''
|
|
466
465
|
AND created_at_epoch > ?
|
|
@@ -525,7 +524,7 @@ Return ONLY valid JSON:
|
|
|
525
524
|
- If they should NOT be merged: {"should_merge":false}
|
|
526
525
|
- If they SHOULD be merged: {"should_merge":true,"merged_title":"≤120 char comprehensive title","merged_narrative":"comprehensive ≤800 char summary preserving all key details","merged_concepts":["kw1","kw2"],"merged_facts":["specific fact 1"],"merged_lesson":"synthesized non-obvious lesson or null","importance":2}`;
|
|
527
526
|
|
|
528
|
-
const parsed = await callModelJSON(prompt, 'sonnet', { timeout:
|
|
527
|
+
const parsed = await callModelJSON(prompt, 'sonnet', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 1000 });
|
|
529
528
|
if (!parsed || !parsed.should_merge) return { merged: false };
|
|
530
529
|
|
|
531
530
|
// Keeper = highest importance, then highest access_count. Previously access_count
|
|
@@ -643,11 +642,11 @@ export async function executeClusterMerge(db, maxClusters = 5, { project } = {})
|
|
|
643
642
|
|
|
644
643
|
// ─── Task 4: Smart-compress ────────────────────────────────────────────────
|
|
645
644
|
|
|
646
|
-
const COMPRESS_TIME_SPLIT_MS = 14 *
|
|
645
|
+
const COMPRESS_TIME_SPLIT_MS = 14 * DAY_MS;
|
|
647
646
|
const COMPRESS_COSINE_THRESHOLD = 0.3;
|
|
648
647
|
|
|
649
648
|
export function findSmartCompressCandidates(db, ageDays = 30, { project } = {}) {
|
|
650
|
-
const cutoff = Date.now() - ageDays *
|
|
649
|
+
const cutoff = Date.now() - ageDays * DAY_MS;
|
|
651
650
|
const projectClause = project ? 'AND project = ?' : '';
|
|
652
651
|
const stmt = db.prepare(`
|
|
653
652
|
SELECT id, title, narrative, lesson_learned, project, type, created_at_epoch
|
|
@@ -759,7 +758,7 @@ ${obsDescriptions}
|
|
|
759
758
|
|
|
760
759
|
JSON: {"title":"descriptive summary ≤120 chars","narrative":"comprehensive summary ≤800 chars preserving key decisions and lessons","concepts":["kw1","kw2"],"facts":["all specific facts preserved"],"lesson_learned":"most important synthesized lesson or 'none'","search_aliases":["alt search 1","alt search 2"]}`;
|
|
761
760
|
|
|
762
|
-
const parsed = await callModelJSON(prompt, 'sonnet', { timeout:
|
|
761
|
+
const parsed = await callModelJSON(prompt, 'sonnet', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 1000 });
|
|
763
762
|
if (!parsed || !parsed.title) return { compressed: false };
|
|
764
763
|
|
|
765
764
|
// Scrub BEFORE truncate (see re-enrich note): boundary cut on scrubbed text.
|
package/hook-precompact.mjs
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
9
9
|
import { inferProject, debugCatch, debugLog } from './utils.mjs';
|
|
10
|
+
import { RUNTIME_DIR } from './hook-shared.mjs';
|
|
11
|
+
import { recordKeyContextInjection } from './lib/keyctx-marker.mjs';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* Build + emit the memory context block on stdout.
|
|
14
|
+
* Build + emit the memory context block on stdout. Writes the Key Context ids
|
|
15
|
+
* the re-emitted block renders (refreshing handleUserPrompt's exclude-set — see
|
|
16
|
+
* D#123 in hook.mjs) and bumps injection_count on those rows (D#124).
|
|
13
17
|
*
|
|
14
18
|
* @param {object} ctx
|
|
15
19
|
* @param {import('better-sqlite3').Database} ctx.db
|
|
@@ -19,9 +23,18 @@ import { inferProject, debugCatch, debugLog } from './utils.mjs';
|
|
|
19
23
|
*/
|
|
20
24
|
export function handlePreCompact({ db, project, sessionId }) {
|
|
21
25
|
try {
|
|
22
|
-
const
|
|
26
|
+
const collector = {};
|
|
27
|
+
const body = buildSessionContextLines(db, project, new Date(), sessionId || null, collector);
|
|
23
28
|
if (!body || String(body).trim() === '') return;
|
|
24
29
|
process.stdout.write(`<claude-mem-context>\n${body}\n</claude-mem-context>\n`);
|
|
30
|
+
// Same recorder as handleSessionStart: marker + injection_count bump (D#124).
|
|
31
|
+
// A re-render into a compacted context is a fresh injection of those rows.
|
|
32
|
+
recordKeyContextInjection(db, {
|
|
33
|
+
runtimeDir: RUNTIME_DIR,
|
|
34
|
+
project,
|
|
35
|
+
sessionId: sessionId || null,
|
|
36
|
+
ids: collector.keyContextIds || [],
|
|
37
|
+
});
|
|
25
38
|
} catch (e) {
|
|
26
39
|
debugCatch(e, 'handlePreCompact');
|
|
27
40
|
}
|