claude-mem-lite 3.65.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/format-utils.mjs +2 -1
- package/haiku-client.mjs +39 -6
- package/hook-context.mjs +5 -4
- package/hook-llm.mjs +7 -5
- package/hook-memory.mjs +4 -3
- package/hook-optimize.mjs +11 -10
- package/hook-precompact.mjs +12 -12
- package/hook-shared.mjs +91 -5
- package/hook.mjs +28 -14
- 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/keyctx-marker.mjs +71 -0
- package/lib/maintain-core.mjs +2 -1
- package/lib/metrics.mjs +1 -1
- package/lib/save-enrich.mjs +3 -2
- package/lib/search-core.mjs +2 -1
- package/lib/stats-core.mjs +4 -3
- package/lib/stats-quality.mjs +2 -1
- package/lib/time-constants.mjs +21 -0
- package/mem-cli.mjs +6 -5
- 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 +2 -1
- package/server.mjs +10 -4
- package/source-files.mjs +6 -0
- package/tier.mjs +4 -3
package/lib/citation-tracker.mjs
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { debugCatch } from '../utils.mjs';
|
|
16
|
+
import { keyContextIdsFileName } from './injected-ids.mjs';
|
|
16
17
|
|
|
18
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
17
19
|
// `#123` / `#45678` at a word boundary — matches the CLAUDE.md cite pattern.
|
|
18
20
|
// Bounded to 1-7 digits to skip URL fragments, markdown anchors, etc.
|
|
19
21
|
const CITATION_RE = /#(\d{1,7})\b/g;
|
|
@@ -342,16 +344,55 @@ export function extractInjectedFromFyi(transcriptPath, opts = {}) {
|
|
|
342
344
|
return ids;
|
|
343
345
|
}
|
|
344
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Extract observation IDs rendered into the SessionStart / PreCompact
|
|
349
|
+
* `<claude-mem-context>` File Lessons + Key Context sections (D#124).
|
|
350
|
+
*
|
|
351
|
+
* The odd one out among the extractors: this surface leaves no hook attachment
|
|
352
|
+
* in the transcript — the block is written straight to stdout at SessionStart —
|
|
353
|
+
* so there is nothing to parse. It reads the per-session marker instead, the
|
|
354
|
+
* same file handleUserPrompt uses as its exclude-set, which by construction
|
|
355
|
+
* lists what was ACTUALLY rendered (empty on quiet/adopted projects where the
|
|
356
|
+
* sections never appear).
|
|
357
|
+
*
|
|
358
|
+
* Session-gated: a marker whose recorded session differs from the caller's is
|
|
359
|
+
* another window's render and must not enter this session's denominator. No
|
|
360
|
+
* coordinates → empty set, so transcript-only callers keep their old behaviour.
|
|
361
|
+
*
|
|
362
|
+
* @param {object} [ctx]
|
|
363
|
+
* @param {string} [ctx.runtimeDir]
|
|
364
|
+
* @param {string} [ctx.project]
|
|
365
|
+
* @param {string|null} [ctx.sessionId]
|
|
366
|
+
* @returns {Set<number>}
|
|
367
|
+
*/
|
|
368
|
+
export function extractInjectedFromKeyContext({ runtimeDir, project, sessionId = null } = {}) {
|
|
369
|
+
const ids = new Set();
|
|
370
|
+
if (!runtimeDir || !project) return ids;
|
|
371
|
+
try {
|
|
372
|
+
const raw = readFileSync(join(runtimeDir, keyContextIdsFileName(project, sessionId)), 'utf8');
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
if (sessionId && parsed?.session && parsed.session !== sessionId) return ids;
|
|
375
|
+
for (const id of Array.isArray(parsed?.ids) ? parsed.ids : []) addObsId(ids, id);
|
|
376
|
+
} catch { /* no marker / torn write → nothing was rendered, fail open */ }
|
|
377
|
+
return ids;
|
|
378
|
+
}
|
|
379
|
+
|
|
345
380
|
/**
|
|
346
381
|
* Union of every injection surface's IDs for a transcript: pre-tool-recall +
|
|
347
382
|
* UserPromptSubmit `<memory-context>` + PostToolUse error-recall + the
|
|
348
|
-
* user-prompt-search FYI block
|
|
383
|
+
* user-prompt-search FYI block + the SessionStart Key Context block. Single
|
|
384
|
+
* integration point the Stop handler calls.
|
|
349
385
|
*
|
|
350
386
|
* @param {string|null|undefined} transcriptPath
|
|
351
387
|
* @param {object} [opts]
|
|
352
388
|
* @param {boolean} [opts.mainOnly=false] Skip sidechain-injected IDs. The
|
|
353
389
|
* citation-decay caller passes true so the injected denominator matches the
|
|
354
390
|
* mainOnly cited numerator; the P4 access-bump caller omits it (broader).
|
|
391
|
+
* @param {string} [opts.runtimeDir] With `project`, enables the Key Context
|
|
392
|
+
* face. Omitted by callers that only have a transcript path (computeCiteRecall
|
|
393
|
+
* over an arbitrary file), which then see the four transcript-derived faces.
|
|
394
|
+
* @param {string} [opts.project]
|
|
395
|
+
* @param {string|null} [opts.sessionId]
|
|
355
396
|
* @returns {Set<number>}
|
|
356
397
|
*/
|
|
357
398
|
export function extractAllInjected(transcriptPath, opts = {}) {
|
|
@@ -360,6 +401,9 @@ export function extractAllInjected(transcriptPath, opts = {}) {
|
|
|
360
401
|
...extractInjectedFromUserPromptSubmit(transcriptPath, opts),
|
|
361
402
|
...extractInjectedFromErrorRecall(transcriptPath, opts),
|
|
362
403
|
...extractInjectedFromFyi(transcriptPath, opts),
|
|
404
|
+
// Marker-derived, not transcript-derived: no-op unless the caller passes
|
|
405
|
+
// runtimeDir + project (see extractInjectedFromKeyContext).
|
|
406
|
+
...extractInjectedFromKeyContext(opts),
|
|
363
407
|
]);
|
|
364
408
|
}
|
|
365
409
|
|
|
@@ -808,8 +852,8 @@ export function computeCitationFunnelTrend(db, { days = 7, limit = 10, project =
|
|
|
808
852
|
if (!db) return empty;
|
|
809
853
|
try {
|
|
810
854
|
const now = Date.now();
|
|
811
|
-
const windowStart = now - days *
|
|
812
|
-
const priorStart = now - 2 * days *
|
|
855
|
+
const windowStart = now - days * DAY_MS;
|
|
856
|
+
const priorStart = now - 2 * days * DAY_MS;
|
|
813
857
|
const projClause = project ? 'AND project = ?' : '';
|
|
814
858
|
|
|
815
859
|
const sessions = db.prepare(`
|
package/lib/db-backup.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { readdirSync, unlinkSync, statSync } from 'fs';
|
|
|
11
11
|
import { dirname, basename, join } from 'path';
|
|
12
12
|
import { debugLog } from '../utils.mjs';
|
|
13
13
|
|
|
14
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
14
15
|
// M-9 (audit 2026-08-14): per-tag retention alone let one-shot tags live forever —
|
|
15
16
|
// each tag kept its newest 3, but a tag written once (pre-backfill-v3390 and four
|
|
16
17
|
// siblings) never got a second snapshot to age it out, so 9 orphaned .bak files held
|
|
@@ -35,7 +36,7 @@ export function backupBudgetBytes() {
|
|
|
35
36
|
// a per-tag exemption would make every one-shot tag's only snapshot immortal —
|
|
36
37
|
// exactly the 360MB orphan shape M-9 exists to evict. Mirrors the purge grace
|
|
37
38
|
// convention (7d) used by stale-observation retention.
|
|
38
|
-
export const BACKUP_EVICTION_GRACE_MS = 7 *
|
|
39
|
+
export const BACKUP_EVICTION_GRACE_MS = 7 * DAY_MS;
|
|
39
40
|
|
|
40
41
|
// Canonical snapshot shape `<base>.<tag>-<ISO stamp>-<pid>-<seq>.bak` (what
|
|
41
42
|
// snapshotDb writes). Budget eviction deletes ONLY names matching this — a
|
package/lib/deferred-work.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
1
2
|
// claude-mem-lite — deferred_work data layer
|
|
2
3
|
// Pure-data CRUD + ordinal resolver + transactional closure helper.
|
|
3
4
|
// Decoupled from observations table: different lifecycle, different scoring.
|
|
@@ -60,7 +61,6 @@ export function listOpenWithOrdinal(db, project, limit = 10) {
|
|
|
60
61
|
|
|
61
62
|
// ─── G11: list age + stale refresh hint (roadmap 2026-07-18) ─────────────────
|
|
62
63
|
|
|
63
|
-
const DAY_MS = 86_400_000;
|
|
64
64
|
// Internal const (was exported at v3.51.0 birth with zero external importers —
|
|
65
65
|
// knip-baseline discipline: un-export rather than grow the unused-exports list).
|
|
66
66
|
const DEFER_STALE_DAYS = 30;
|
package/lib/err-sampler.mjs
CHANGED
|
@@ -19,8 +19,8 @@ import { appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSyn
|
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { scrubSecrets } from '../secret-scrub.mjs';
|
|
21
21
|
|
|
22
|
-
const DAY_MS = 86400000;
|
|
23
22
|
|
|
23
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
24
24
|
function today() {
|
|
25
25
|
// UTC date string; sharding daily keeps files bounded even at high sample rates.
|
|
26
26
|
return new Date(Date.now()).toISOString().slice(0, 10);
|
package/lib/hook-telemetry.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { join } from 'path';
|
|
|
26
26
|
import { isNativeBindingError } from './binding-probe.mjs';
|
|
27
27
|
import { recordNativeBindingBreakage } from './native-binding-hint.mjs';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
30
30
|
const RETENTION_MS = 14 * DAY_MS;
|
|
31
31
|
const HOOK_ERRORS_SUBDIR = 'hook-errors';
|
|
32
32
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// lib/keyctx-marker.mjs — the one place the SessionStart Key Context render is
|
|
2
|
+
// recorded. Two things happen together because they must describe the SAME set:
|
|
3
|
+
//
|
|
4
|
+
// ① the per-session marker file (D#123 review C-1): the obs ids ACTUALLY
|
|
5
|
+
// rendered into <claude-mem-context>, which handleUserPrompt reads as its
|
|
6
|
+
// exclude-set. Written even when empty, so a resumed session can never act
|
|
7
|
+
// on a previous session's stale semantics.
|
|
8
|
+
// ② injection_count / last_injected_at on those same rows (D#124): before
|
|
9
|
+
// this, Key Context was a shown-but-uncounted surface — up to 10 rows per
|
|
10
|
+
// session that could accrue no denominator and therefore never promote or
|
|
11
|
+
// demote through applyCitationDecay.
|
|
12
|
+
//
|
|
13
|
+
// handleSessionStart (hook.mjs) and handlePreCompact (hook-precompact.mjs) both
|
|
14
|
+
// render the block, so both call this. Keeping the pair in one function is the
|
|
15
|
+
// point: the CLI/MCP and SessionStart/PreCompact twin pairs in this repo have
|
|
16
|
+
// drifted often enough that "two callers, one body" is the standing rule.
|
|
17
|
+
//
|
|
18
|
+
// Never throws: a marker-write failure must not break context delivery, and it
|
|
19
|
+
// must not cost the metering either — the bump runs first.
|
|
20
|
+
|
|
21
|
+
import { writeFileSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { debugCatch } from '../utils.mjs';
|
|
24
|
+
import { keyContextIdsFileName } from './injected-ids.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Record one Key Context render: bump the rendered rows, then persist the id
|
|
28
|
+
* list for the prompt-time exclude-set and the citation extractor.
|
|
29
|
+
*
|
|
30
|
+
* @param {import('better-sqlite3').Database} db
|
|
31
|
+
* @param {object} ctx
|
|
32
|
+
* @param {string} ctx.runtimeDir RUNTIME_DIR of the caller
|
|
33
|
+
* @param {string} ctx.project inferProject() value (filename-safe)
|
|
34
|
+
* @param {string|null} [ctx.sessionId] CC session id
|
|
35
|
+
* @param {number[]} [ctx.ids] obs ids ACTUALLY rendered (empty on quiet projects)
|
|
36
|
+
* @returns {{bumped: number, written: boolean}}
|
|
37
|
+
*/
|
|
38
|
+
export function recordKeyContextInjection(db, { runtimeDir, project, sessionId = null, ids = [] } = {}) {
|
|
39
|
+
const clean = [];
|
|
40
|
+
for (const raw of Array.isArray(ids) ? ids : []) {
|
|
41
|
+
const id = Number(raw);
|
|
42
|
+
if (Number.isInteger(id) && id > 0 && id < 1e7) clean.push(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let bumped = 0;
|
|
46
|
+
if (db && clean.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
// Mirrors hook-memory.mjs's UPS bump verbatim so the two surfaces feed the
|
|
50
|
+
// same counter with the same semantics. Per-row try/catch for FTS trigger
|
|
51
|
+
// safety (project_non_obvious.md).
|
|
52
|
+
const stmt = db.prepare(
|
|
53
|
+
'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
|
|
54
|
+
);
|
|
55
|
+
for (const id of clean) {
|
|
56
|
+
try { stmt.run(now, id); bumped++; } catch { /* single-row failure must not drop the rest */ }
|
|
57
|
+
}
|
|
58
|
+
} catch (e) { debugCatch(e, 'keyctx-bump'); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let written = false;
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(runtimeDir, keyContextIdsFileName(project, sessionId)),
|
|
65
|
+
JSON.stringify({ ids: clean, ts: Date.now(), session: sessionId || null }),
|
|
66
|
+
);
|
|
67
|
+
written = true;
|
|
68
|
+
} catch (e) { debugCatch(e, 'keyctx-marker-write'); }
|
|
69
|
+
|
|
70
|
+
return { bumped, written };
|
|
71
|
+
}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -17,7 +17,8 @@ import { rebuildVocabulary, computeVector, _resetVocabCache, vectorsEnabled, vec
|
|
|
17
17
|
import { DEDUP_JACCARD_THRESHOLD, MINHASH_PRE_THRESHOLD as MINHASH_PRE_THRESHOLD_SRC, FUZZY_DEDUP_THRESHOLD, FUZZY_BODY_THRESHOLD, MINHASH_PREFILTER } from './dedup-constants.mjs';
|
|
18
18
|
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
21
|
+
export const STALE_AGE_MS = 30 * DAY_MS;
|
|
21
22
|
export const OP_CAP = 1000;
|
|
22
23
|
export const SCAN_LIMIT = 500;
|
|
23
24
|
export const DUPLICATE_LIMIT = 50;
|
package/lib/metrics.mjs
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from 'fs';
|
|
20
20
|
import { join } from 'path';
|
|
21
21
|
|
|
22
|
-
const DAY_MS = 86400000;
|
|
23
22
|
|
|
23
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
24
24
|
function today() { return new Date(Date.now()).toISOString().slice(0, 10); }
|
|
25
25
|
|
|
26
26
|
function metricsEnabled() {
|
package/lib/save-enrich.mjs
CHANGED
|
@@ -84,7 +84,8 @@ export async function executeSaveEnrich(db, id, { callJson } = {}) {
|
|
|
84
84
|
const wantAliases = !(row.search_aliases && row.search_aliases.trim());
|
|
85
85
|
if (!wantLesson && !wantAliases) return { enriched: false, reason: 'complete' };
|
|
86
86
|
|
|
87
|
-
const
|
|
87
|
+
const { callModelJSON, BG_LLM_TIMEOUT_MS } = await import('../haiku-client.mjs');
|
|
88
|
+
const call = callJson || callModelJSON;
|
|
88
89
|
const prompt = `A coding memory was just saved. Enrich its retrievability. Return ONLY valid JSON, no markdown fences.
|
|
89
90
|
|
|
90
91
|
Title: ${truncate(row.title || '(untitled)', 200)}
|
|
@@ -96,7 +97,7 @@ search_aliases: 3-6 terms a user might search for the SAME memory that are NOT a
|
|
|
96
97
|
|
|
97
98
|
let parsed = null;
|
|
98
99
|
try {
|
|
99
|
-
parsed = await call(prompt, 'haiku', { timeout:
|
|
100
|
+
parsed = await call(prompt, 'haiku', { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 400 });
|
|
100
101
|
} catch (e) { debugCatch(e, 'save-enrich-llm'); }
|
|
101
102
|
if (!parsed || typeof parsed !== 'object') return { enriched: false, reason: 'llm-null' };
|
|
102
103
|
|
package/lib/search-core.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { countSearchTotal, attachBodyTokens } from '../search-engine.mjs';
|
|
|
31
31
|
import { notLowSignalTitleClause } from '../scoring-sql.mjs';
|
|
32
32
|
import { liveObsFilterSql, recencyDecaySql } from './inject-search-core.mjs';
|
|
33
33
|
|
|
34
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
34
35
|
/** Sanitize a user query to FTS5 syntax; optionally force OR semantics. */
|
|
35
36
|
export function buildSearchFtsQuery(query, { or = false } = {}) {
|
|
36
37
|
let ftsQuery = sanitizeFtsQuery(query);
|
|
@@ -40,7 +41,7 @@ export function buildSearchFtsQuery(query, { or = false } = {}) {
|
|
|
40
41
|
|
|
41
42
|
// Relative-duration units for `--since`. Months/years intentionally omitted —
|
|
42
43
|
// they're calendar-ambiguous; absolute --from/--to cover longer windows.
|
|
43
|
-
const DURATION_UNIT_MS = { s: 1000, m: 60000, h: 3600000, d:
|
|
44
|
+
const DURATION_UNIT_MS = { s: 1000, m: 60000, h: 3600000, d: DAY_MS, w: 604800000 };
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Parse a relative duration like "7d", "24h", "90m", "2w", "30s" into ms.
|
package/lib/stats-core.mjs
CHANGED
|
@@ -12,13 +12,14 @@ import { TIER_CASE_SQL, tierSqlParams } from '../tier.mjs';
|
|
|
12
12
|
import { computeNoiseGauge } from './stats-quality.mjs';
|
|
13
13
|
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
14
14
|
|
|
15
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
15
16
|
/**
|
|
16
17
|
* Compute the primary stats feed. Row shapes are returned exactly as the twin
|
|
17
18
|
* blocks produced them ({c}/{t}/{v} single-row objects, arrays for distributions)
|
|
18
19
|
* so both call sites render with minimal diff.
|
|
19
20
|
*/
|
|
20
21
|
export function computeStatsFeed(db, { project = null, days = 30, now = Date.now() } = {}) {
|
|
21
|
-
const cutoff = now - days *
|
|
22
|
+
const cutoff = now - days * DAY_MS;
|
|
22
23
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
23
24
|
const baseParams = project ? [project] : [];
|
|
24
25
|
|
|
@@ -51,7 +52,7 @@ export function computeStatsFeed(db, { project = null, days = 30, now = Date.now
|
|
|
51
52
|
SELECT date(created_at) as day, COUNT(*) as c FROM observations
|
|
52
53
|
WHERE created_at_epoch >= ? ${projectFilter}
|
|
53
54
|
GROUP BY day ORDER BY day DESC LIMIT 7
|
|
54
|
-
`).all(now - 7 *
|
|
55
|
+
`).all(now - 7 * DAY_MS, ...baseParams);
|
|
55
56
|
|
|
56
57
|
// Data health
|
|
57
58
|
const tokenEst = db.prepare(`
|
|
@@ -60,7 +61,7 @@ export function computeStatsFeed(db, { project = null, days = 30, now = Date.now
|
|
|
60
61
|
`).get(...baseParams);
|
|
61
62
|
const avgImp = db.prepare(`SELECT AVG(COALESCE(importance,1)) as v FROM observations WHERE 1=1 ${projectFilter}`).get(...baseParams);
|
|
62
63
|
|
|
63
|
-
const thirtyDaysAgo = now - 30 *
|
|
64
|
+
const thirtyDaysAgo = now - 30 * DAY_MS;
|
|
64
65
|
// v3.23 noise-gauge de-blinding: `<= 1` makes the imp=0 dormant population visible
|
|
65
66
|
// (decay floor + LLM low-signal filter push ~half the live corpus to 0);
|
|
66
67
|
// injection_count=0 mirrors decay's NEVER-INJECTED guard so injected-but-decayed
|
package/lib/stats-quality.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
|
7
7
|
import { truncate } from '../format-utils.mjs';
|
|
8
8
|
import { COMPRESSED_PENDING_PURGE } from '../utils.mjs';
|
|
9
9
|
|
|
10
|
+
import { DAY_MS } from './time-constants.mjs';
|
|
10
11
|
// v3.42 F7: noise/low-signal ratios must divide by the LIVE (non-compressed) observation
|
|
11
12
|
// count, NOT the all-rows total. Both numerators (lowVal, lowSignalTitle) already exclude
|
|
12
13
|
// compressed rows (`compressed_into = 0`); dividing by an all-inclusive denominator
|
|
@@ -25,7 +26,7 @@ export function computeNoiseGauge({ liveTotal, lowValCount, lowSignalCount }) {
|
|
|
25
26
|
export function computeQualityStats(db, { project, days }) {
|
|
26
27
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
27
28
|
const baseParams = project ? [project] : [];
|
|
28
|
-
const cutoff = Date.now() - days *
|
|
29
|
+
const cutoff = Date.now() - days * DAY_MS;
|
|
29
30
|
|
|
30
31
|
// LOW_SIGNAL match = NOT notLowSignal. Pure title-only builder: this is a
|
|
31
32
|
// METRIC counting pattern-titled rows ("Low-signal titles" in stats output),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// lib/time-constants.mjs — millisecond time units, defined once.
|
|
2
|
+
//
|
|
3
|
+
// Four modules each carried their own `const DAY_MS` (deferred-work, metrics,
|
|
4
|
+
// err-sampler, hook-telemetry) with ~60 more bare 86400000 literals scattered
|
|
5
|
+
// across the runtime. The value cannot drift — that is not the problem. The
|
|
6
|
+
// problem is that a reader auditing a window ("is this 30 days or 30 hours?")
|
|
7
|
+
// had to re-derive the magic number at every site, and a grep for the policy
|
|
8
|
+
// returned a number rather than a name.
|
|
9
|
+
//
|
|
10
|
+
// Zero imports on purpose: lib/metrics.mjs and lib/err-sampler.mjs are sinks
|
|
11
|
+
// that must never crash or slow their callers, so anything they import has to
|
|
12
|
+
// stay a leaf.
|
|
13
|
+
|
|
14
|
+
// Kept internal rather than exported: only DAY_MS has runtime consumers today,
|
|
15
|
+
// and an export born with zero importers is how the knip baseline drifted
|
|
16
|
+
// 46→47 during v3.50–3.53 (DEFER_STALE_DAYS). Promote one when a caller needs it.
|
|
17
|
+
const SECOND_MS = 1000;
|
|
18
|
+
const MINUTE_MS = 60 * SECOND_MS;
|
|
19
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
20
|
+
|
|
21
|
+
export const DAY_MS = 24 * HOUR_MS;
|
package/mem-cli.mjs
CHANGED
|
@@ -1197,7 +1197,7 @@ async function cmdStats(db, args) {
|
|
|
1197
1197
|
// recorded in the last 24h. Surfaces silent breakage (DB corruption,
|
|
1198
1198
|
// CC upstream field rename) that would otherwise stay invisible — the
|
|
1199
1199
|
// failure mode that left code-graph's matcher bug undetected for 10 sessions.
|
|
1200
|
-
const hookErrors24h = countRecentHookErrors(join(DB_DIR, 'runtime'), now -
|
|
1200
|
+
const hookErrors24h = countRecentHookErrors(join(DB_DIR, 'runtime'), now - DAY_MS);
|
|
1201
1201
|
|
|
1202
1202
|
// M-9 (audit 2026-08-14): disk footprint — a "lite" store had accumulated 360MB of
|
|
1203
1203
|
// pre-maintain snapshots against a 59MB DB with nothing reporting it. Cheap probes
|
|
@@ -1636,7 +1636,7 @@ function cmdExport(db, args) {
|
|
|
1636
1636
|
if (flags.to) {
|
|
1637
1637
|
exportToEpoch = new Date(flags.to).getTime();
|
|
1638
1638
|
if (isNaN(exportToEpoch)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
|
|
1639
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) exportToEpoch +=
|
|
1639
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(flags.to)) exportToEpoch += DAY_MS - 1;
|
|
1640
1640
|
wheres.push('created_at_epoch <= ?'); params.push(exportToEpoch);
|
|
1641
1641
|
}
|
|
1642
1642
|
if (exportFromEpoch !== null && exportToEpoch !== null && exportFromEpoch > exportToEpoch) {
|
|
@@ -1883,7 +1883,7 @@ function cmdCompress(db, args) {
|
|
|
1883
1883
|
}
|
|
1884
1884
|
ageDays = parsed;
|
|
1885
1885
|
}
|
|
1886
|
-
const cutoff = Date.now() - ageDays *
|
|
1886
|
+
const cutoff = Date.now() - ageDays * DAY_MS;
|
|
1887
1887
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1888
1888
|
|
|
1889
1889
|
const candidates = selectCompressionCandidates(db, { cutoff, project });
|
|
@@ -2021,7 +2021,7 @@ function cmdMaintain(db, args) {
|
|
|
2021
2021
|
}
|
|
2022
2022
|
retainDays = parsed;
|
|
2023
2023
|
}
|
|
2024
|
-
const retainCutoff = Date.now() - retainDays *
|
|
2024
|
+
const retainCutoff = Date.now() - retainDays * DAY_MS;
|
|
2025
2025
|
// purge_stale is the only DELETE here — require --confirm so a mis-typed run can't wipe rows.
|
|
2026
2026
|
const confirmed = flags.confirm === true || flags.confirm === 'true';
|
|
2027
2027
|
|
|
@@ -2592,7 +2592,7 @@ function cmdCitationStats(db, args) {
|
|
|
2592
2592
|
out(`Recently demoted (last ${days}d, importance ↓):`);
|
|
2593
2593
|
if (demoted.length === 0) out(' (none)');
|
|
2594
2594
|
for (const r of demoted) {
|
|
2595
|
-
const ago = Math.round((Date.now() - r.demoted_at) /
|
|
2595
|
+
const ago = Math.round((Date.now() - r.demoted_at) / DAY_MS);
|
|
2596
2596
|
out(` #${r.id} [${r.type}] ${(r.title || '').slice(0, 60)} imp=${r.importance} ${ago}d ago`);
|
|
2597
2597
|
}
|
|
2598
2598
|
}
|
|
@@ -3140,6 +3140,7 @@ import { cmdDoctor } from './cli/doctor.mjs';
|
|
|
3140
3140
|
// cmdActivity (T7 v2.31) extracted to cli/activity.mjs (v2.41 split).
|
|
3141
3141
|
import { cmdActivity } from './cli/activity.mjs';
|
|
3142
3142
|
|
|
3143
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
3143
3144
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
3144
3145
|
|
|
3145
3146
|
export async function run(argv) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.66.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -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
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
"lib/release-digest.mjs",
|
|
84
84
|
"lib/mem-override.mjs",
|
|
85
85
|
"lib/injected-ids.mjs",
|
|
86
|
+
"lib/time-constants.mjs",
|
|
87
|
+
"lib/keyctx-marker.mjs",
|
|
86
88
|
"lib/inject-search-core.mjs",
|
|
87
89
|
"lib/get-core.mjs",
|
|
88
90
|
"lib/browse-core.mjs",
|
package/registry-enricher.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Sends resource content to Haiku for semantic metadata generation
|
|
3
3
|
// Graceful degradation: failure preserves existing data
|
|
4
4
|
|
|
5
|
-
import { callHaikuJSON } from './haiku-client.mjs';
|
|
5
|
+
import { callHaikuJSON, BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
|
|
6
6
|
import { truncate, debugCatch } from './utils.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -84,7 +84,7 @@ export async function enrichResource(db, name, type, content) {
|
|
|
84
84
|
|
|
85
85
|
try {
|
|
86
86
|
const prompt = buildEnrichPrompt(name, content, existing);
|
|
87
|
-
const result = await callHaikuJSON(prompt, { timeout:
|
|
87
|
+
const result = await callHaikuJSON(prompt, { timeout: BG_LLM_TIMEOUT_MS, maxTokens: 500 });
|
|
88
88
|
|
|
89
89
|
if (!result || !result.capability_summary) {
|
|
90
90
|
db.prepare("UPDATE resources SET enrichment_status = 'failed' WHERE name = ? AND type = ?").run(name, type);
|
package/registry-recommend.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { join } from 'path';
|
|
|
8
8
|
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
9
9
|
import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
|
|
10
10
|
|
|
11
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
11
12
|
const VALID_MODES = new Set(['shadow', 'live', 'off']);
|
|
12
13
|
|
|
13
14
|
/** Recommendation mode from env; default 'shadow', unknown → 'shadow'. */
|
|
@@ -137,7 +138,7 @@ export function gcOldShadowShards(retainDays = 90) {
|
|
|
137
138
|
try {
|
|
138
139
|
const dir = shadowDir();
|
|
139
140
|
if (!existsSync(dir)) return 0;
|
|
140
|
-
const cutoff = new Date(Date.now() - retainDays *
|
|
141
|
+
const cutoff = new Date(Date.now() - retainDays * DAY_MS).toISOString().slice(0, 10);
|
|
141
142
|
let removed = 0;
|
|
142
143
|
for (const name of readdirSync(dir)) {
|
|
143
144
|
const m = /^(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
|
|
@@ -155,7 +156,7 @@ export function logShadowAdoption(project, rec) { appendShadow({ ts: new Date().
|
|
|
155
156
|
/** Yield parsed shadow rows from the last `days` daily shards. */
|
|
156
157
|
export function* readShadowLog(days = 7) {
|
|
157
158
|
for (let i = 0; i < days; i++) {
|
|
158
|
-
const d = new Date(Date.now() - i *
|
|
159
|
+
const d = new Date(Date.now() - i * DAY_MS).toISOString().slice(0, 10);
|
|
159
160
|
let raw;
|
|
160
161
|
try { raw = readFileSync(join(shadowDir(), `${d}.jsonl`), 'utf8'); } catch { continue; }
|
|
161
162
|
for (const line of raw.split('\n')) { if (!line) continue; try { yield JSON.parse(line); } catch { /* skip malformed */ } }
|
package/scoring-sql.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
5
5
|
|
|
6
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
6
7
|
// ─── Why these multipliers exist (read before "simplifying" them) ────────────
|
|
7
8
|
//
|
|
8
9
|
// The recency-decay, type-quality, project-boost, importance, cite, and noise
|
|
@@ -33,14 +34,14 @@ import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
|
33
34
|
|
|
34
35
|
/** Recency half-life per observation type (in milliseconds) */
|
|
35
36
|
export const DECAY_HALF_LIFE_BY_TYPE = {
|
|
36
|
-
decision: 90 *
|
|
37
|
-
discovery: 60 *
|
|
38
|
-
feature: 30 *
|
|
39
|
-
bugfix: 14 *
|
|
40
|
-
refactor: 14 *
|
|
41
|
-
change: 7 *
|
|
37
|
+
decision: 90 * DAY_MS, // 90 days — architectural decisions persist
|
|
38
|
+
discovery: 60 * DAY_MS, // 60 days — learned patterns last
|
|
39
|
+
feature: 30 * DAY_MS, // 30 days — feature work is mid-range
|
|
40
|
+
bugfix: 14 * DAY_MS, // 14 days — bugs are usually one-off
|
|
41
|
+
refactor: 14 * DAY_MS, // 14 days — code cleanup
|
|
42
|
+
change: 7 * DAY_MS, // 7 days — routine changes decay fast
|
|
42
43
|
};
|
|
43
|
-
export const DEFAULT_DECAY_HALF_LIFE_MS = 14 *
|
|
44
|
+
export const DEFAULT_DECAY_HALF_LIFE_MS = 14 * DAY_MS;
|
|
44
45
|
|
|
45
46
|
// ─── BM25 Weight Constants ──────────────────────────────────────────────────
|
|
46
47
|
// Single source of truth for FTS5 BM25 weight expressions.
|
|
@@ -20,6 +20,7 @@ import { recordMetric } from '../lib/metrics.mjs';
|
|
|
20
20
|
import { presentIdents } from '../lib/lesson-idents.mjs';
|
|
21
21
|
import { neutralizeContextDelimiters } from '../format-utils.mjs';
|
|
22
22
|
|
|
23
|
+
import { DAY_MS } from '../lib/time-constants.mjs';
|
|
23
24
|
// CLAUDE_MEM_DIR matches schema.mjs / main CLI — one env var sandboxes the
|
|
24
25
|
// whole system. CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR remain as
|
|
25
26
|
// per-component overrides for tests that mix isolated + real paths.
|
|
@@ -372,7 +373,7 @@ try {
|
|
|
372
373
|
const fileMatch = fileMatchClause('of2');
|
|
373
374
|
const fileParams = fileMatchParams(filePath);
|
|
374
375
|
// 60-day lookback to avoid surfacing ancient observations
|
|
375
|
-
const cutoff = Date.now() - 60 *
|
|
376
|
+
const cutoff = Date.now() - 60 * DAY_MS;
|
|
376
377
|
|
|
377
378
|
// Surface actionable lessons first, then high-importance bugfix/decision observations.
|
|
378
379
|
// Priority: 1) observations with lesson_learned (most actionable for preventing repeat bugs)
|
|
@@ -18,6 +18,7 @@ import { recommendSkill } from '../registry-recommend.mjs';
|
|
|
18
18
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
19
19
|
import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
20
20
|
|
|
21
|
+
import { DAY_MS } from '../lib/time-constants.mjs';
|
|
21
22
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
23
24
|
// Telemetry sink (lib/hook-telemetry.mjs contract): env override for tests, else
|
|
@@ -37,7 +38,7 @@ const injectedIdsFileFor = (sessionId) =>
|
|
|
37
38
|
// gated by explicit "before/previously/记得" prompts where breadth is the
|
|
38
39
|
// point). Env override for projects that want broader recall or to A/B.
|
|
39
40
|
const MAX_RESULTS = Number(process.env.CLAUDE_MEM_UPS_MAX_RESULTS || 3);
|
|
40
|
-
const LOOKBACK_MS = 60 *
|
|
41
|
+
const LOOKBACK_MS = 60 * DAY_MS; // 60 days
|
|
41
42
|
|
|
42
43
|
// v2.56.x: Past-similar-questions fallback row cap. Cut from 3 → 1 after
|
|
43
44
|
// 30d transcript scan (#8062 follow-up, 2026-05-09) showed UPS prompt-fallback
|
package/search-scoring.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { porterStem } from './tfidf.mjs';
|
|
|
11
11
|
import { CLI_INVOKE } from './cli-path.mjs';
|
|
12
12
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
13
13
|
|
|
14
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
14
15
|
// ─── MCP Server Instructions Builder ───────────────────────────────────────
|
|
15
16
|
// Phase A (v2.31.3+): when quiet=true, drops WHEN-TO-USE proactive-trigger and
|
|
16
17
|
// Decision-rules sections; keeps the irreducible CLI/MCP tool list. Intended
|
|
@@ -302,7 +303,7 @@ export function runIdleCleanup(db) {
|
|
|
302
303
|
|
|
303
304
|
db.transaction(() => {
|
|
304
305
|
for (const { types, days } of staleThresholds) {
|
|
305
|
-
const cutoff = Date.now() - days *
|
|
306
|
+
const cutoff = Date.now() - days * DAY_MS;
|
|
306
307
|
|
|
307
308
|
const marked = db.prepare(`
|
|
308
309
|
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
package/server.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredToken
|
|
|
52
52
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
53
53
|
import { applyObsUpdate } from './lib/observation-write.mjs';
|
|
54
54
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
55
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
55
56
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
56
57
|
import { fetchRecent } from './lib/recent-core.mjs';
|
|
57
58
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
@@ -1032,7 +1033,7 @@ server.registerTool(
|
|
|
1032
1033
|
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
1033
1034
|
const preview = args.preview !== false;
|
|
1034
1035
|
const ageDays = args.age_days ?? 30;
|
|
1035
|
-
const cutoff = Date.now() - ageDays *
|
|
1036
|
+
const cutoff = Date.now() - ageDays * DAY_MS;
|
|
1036
1037
|
const candidates = selectCompressionCandidates(db, { cutoff, project: args.project || null });
|
|
1037
1038
|
|
|
1038
1039
|
if (candidates.length === 0) {
|
|
@@ -1181,7 +1182,7 @@ server.registerTool(
|
|
|
1181
1182
|
// only rows a PRIOR run marked (backed up); rows decay marks below wait one cycle.
|
|
1182
1183
|
if (ops.includes('purge_stale')) {
|
|
1183
1184
|
const retainDays = args.retain_days ?? 30;
|
|
1184
|
-
const retainCutoff = Date.now() - retainDays *
|
|
1185
|
+
const retainCutoff = Date.now() - retainDays * DAY_MS;
|
|
1185
1186
|
if (!purgeConfirmed) {
|
|
1186
1187
|
// Dry-run preview (parity with CLI `maintain` without --confirm): the other
|
|
1187
1188
|
// requested non-destructive ops still run below.
|
|
@@ -1687,8 +1688,12 @@ export async function handleExportForTest(db, args) {
|
|
|
1687
1688
|
async function runExport(db, args) {
|
|
1688
1689
|
const wheres = [];
|
|
1689
1690
|
const params = [];
|
|
1690
|
-
|
|
1691
|
-
|
|
1691
|
+
// Composed, not hand-written: the CLI twin (mem-cli.mjs cmdExport) already
|
|
1692
|
+
// routes through liveObsFilterSql, and a hand-rolled copy here is how the
|
|
1693
|
+
// live-row predicate drifted apart on other surfaces (P2-11/D#123). With
|
|
1694
|
+
// include_compressed the compressed half is dropped but retractions still
|
|
1695
|
+
// are not — tombstone export is opt-in, supersession is never exported.
|
|
1696
|
+
wheres.push(args.include_compressed ? 'superseded_at IS NULL' : liveObsFilterSql(''));
|
|
1692
1697
|
if (args.project) { wheres.push('project = ?'); params.push(_resolveProjectShared(db, args.project)); }
|
|
1693
1698
|
if (args.type) { wheres.push('type = ?'); params.push(args.type); }
|
|
1694
1699
|
// T3-P1-A: surface invalid dates instead of silently dropping the filter — mirrors
|
|
@@ -1801,6 +1806,7 @@ server.registerTool(
|
|
|
1801
1806
|
// Handler extracted to server/fts-check.mjs (v2.41 split).
|
|
1802
1807
|
import { handleMemFtsCheck } from './server/fts-check.mjs';
|
|
1803
1808
|
|
|
1809
|
+
import { DAY_MS } from './lib/time-constants.mjs';
|
|
1804
1810
|
server.registerTool(
|
|
1805
1811
|
'mem_fts_check',
|
|
1806
1812
|
{
|
package/source-files.mjs
CHANGED
|
@@ -123,6 +123,12 @@ export const SOURCE_FILES = [
|
|
|
123
123
|
// scripts/user-prompt-search.js + scripts/pre-tool-recall.js. Under lib/ for
|
|
124
124
|
// the same scripts-dir-rename reason as mem-override.mjs above.
|
|
125
125
|
'lib/injected-ids.mjs',
|
|
126
|
+
// P2-13 (narrowed): millisecond time units, single-sourced from the four
|
|
127
|
+
// modules that each declared their own DAY_MS. Leaf module, zero imports.
|
|
128
|
+
'lib/time-constants.mjs',
|
|
129
|
+
// D#124: Key Context marker write + injection_count bump, shared by
|
|
130
|
+
// handleSessionStart and handlePreCompact. Statically imported by hook.mjs.
|
|
131
|
+
'lib/keyctx-marker.mjs',
|
|
126
132
|
// P2-11: injection-side shared SQL core (live-row filter / clamped decay /
|
|
127
133
|
// injection relevance chain). Statically imported by hook.mjs, hook-memory.mjs,
|
|
128
134
|
// search-engine.mjs AND the standalone hook scripts — missing it from the
|