claude-mem-lite 3.64.0 → 3.65.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/hook-context.mjs +23 -18
- package/hook-handoff.mjs +3 -4
- package/hook-optimize.mjs +4 -6
- package/hook-precompact.mjs +15 -2
- package/hook-shared.mjs +6 -0
- package/hook.mjs +37 -12
- package/lib/inject-search-core.mjs +16 -13
- package/lib/injected-ids.mjs +20 -0
- package/lib/maintain-core.mjs +3 -3
- package/lib/recall-core.mjs +2 -2
- package/lib/recent-core.mjs +3 -1
- package/lib/search-core.mjs +4 -3
- package/lib/stats-core.mjs +2 -1
- package/lib/timeline-core.mjs +7 -6
- package/mem-cli.mjs +13 -14
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/search-scoring.mjs +2 -2
- package/tfidf.mjs +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.65.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.65.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/hook-context.mjs
CHANGED
|
@@ -12,9 +12,10 @@ 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
|
|
|
19
20
|
/**
|
|
20
21
|
* Infer the project directory from environment variables or cwd.
|
|
@@ -83,12 +84,7 @@ export function selectWithTokenBudget(db, project, budget = 2000) {
|
|
|
83
84
|
const obsPool = db.prepare(`
|
|
84
85
|
SELECT id, type, title, narrative, importance, created_at_epoch, files_modified, lesson_learned
|
|
85
86
|
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
|
|
87
|
+
WHERE project = ? AND ${liveObsFilterSql('')}
|
|
92
88
|
AND ${notLowSignalTitleClause('')}
|
|
93
89
|
AND (
|
|
94
90
|
(created_at_epoch > ? AND importance >= 1)
|
|
@@ -293,9 +289,15 @@ export function cleanupClaudeMdLegacyBlock() {
|
|
|
293
289
|
* @param {string|null} [currentCcSessionId=null] Claude Code session id — when provided,
|
|
294
290
|
* the "Working State (from /clear)" block is filtered to handoffs owned by this
|
|
295
291
|
* session, preventing parallel-session bleed (see docs/bug.txt).
|
|
292
|
+
* @param {object|null} [collector=null] Optional out-param: when given, its
|
|
293
|
+
* `keyContextIds` property is set to the obs ids ACTUALLY rendered into the
|
|
294
|
+
* File Lessons / Key Context sections ([] under quiet/adopted or when the
|
|
295
|
+
* sections are empty). handleUserPrompt persists this as its exclude-set
|
|
296
|
+
* (D#123: the exclude-set must mirror real injections, not the keyObs query).
|
|
296
297
|
* @returns {string} Joined markdown lines (without <claude-mem-context> wrappers)
|
|
297
298
|
*/
|
|
298
|
-
export function buildSessionContextLines(db, project, now = new Date(), currentCcSessionId = null) {
|
|
299
|
+
export function buildSessionContextLines(db, project, now = new Date(), currentCcSessionId = null, collector = null) {
|
|
300
|
+
if (collector) collector.keyContextIds = [];
|
|
299
301
|
// 1. Token-budgeted observation selection
|
|
300
302
|
const selected = selectWithTokenBudget(db, project, 2000);
|
|
301
303
|
const observations = selected.observations;
|
|
@@ -308,8 +310,7 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
308
310
|
fallbackObs = db.prepare(`
|
|
309
311
|
SELECT id, type, title, project, created_at
|
|
310
312
|
FROM observations
|
|
311
|
-
WHERE
|
|
312
|
-
AND superseded_at IS NULL
|
|
313
|
+
WHERE ${liveObsFilterSql('')}
|
|
313
314
|
AND ${notLowSignalTitleClause('')}
|
|
314
315
|
AND (
|
|
315
316
|
(created_at_epoch > ? AND importance >= 1)
|
|
@@ -335,10 +336,9 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
335
336
|
// and Key Context (informational). Pushed into summaryLines.
|
|
336
337
|
const keyObs = db.prepare(`
|
|
337
338
|
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
|
|
339
|
+
WHERE o.project = ? AND ${liveObsFilterSql('o')}
|
|
340
340
|
AND COALESCE(o.importance, 1) >= 2
|
|
341
|
-
ORDER BY o.created_at_epoch DESC LIMIT
|
|
341
|
+
ORDER BY o.created_at_epoch DESC LIMIT ${KEY_CONTEXT_LIMIT}
|
|
342
342
|
`).all(project);
|
|
343
343
|
|
|
344
344
|
if (keyObs.length > 0) {
|
|
@@ -357,30 +357,35 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
357
357
|
const files = JSON.parse(o.files_modified);
|
|
358
358
|
const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
|
|
359
359
|
if (fname) {
|
|
360
|
-
fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
|
|
360
|
+
fileLessons.push({ id: o.id, line: `- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})` });
|
|
361
361
|
continue;
|
|
362
362
|
}
|
|
363
363
|
} catch { /* fall through to keyContext */ }
|
|
364
364
|
}
|
|
365
365
|
const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
|
|
366
|
-
keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
|
|
366
|
+
keyContext.push({ id: o.id, line: `- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}` });
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
// Phase A (QUIET_HOOKS) + Phase D (adopted sentinel): drop descriptive
|
|
370
370
|
// File Lessons / Key Context sections when the user has opted into low-noise
|
|
371
371
|
// hooks OR adopted invited-memory (MEMORY.md sentinel carries the triggers
|
|
372
372
|
// at higher system-prompt authority). The Recent table still fires so #IDs
|
|
373
|
-
// remain reachable via mem_get.
|
|
373
|
+
// remain reachable via mem_get. The collector sees only rows that survive
|
|
374
|
+
// BOTH the quiet gate and the per-section slice — rendered rows, nothing else.
|
|
374
375
|
const quiet = effectiveQuiet();
|
|
375
376
|
if (fileLessons.length > 0 && !quiet) {
|
|
377
|
+
const shown = fileLessons.slice(0, 5);
|
|
376
378
|
summaryLines.push('### File Lessons');
|
|
377
|
-
summaryLines.push(...
|
|
379
|
+
summaryLines.push(...shown.map((e) => e.line));
|
|
378
380
|
summaryLines.push('');
|
|
381
|
+
if (collector) collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
379
382
|
}
|
|
380
383
|
if (keyContext.length > 0 && !quiet) {
|
|
384
|
+
const shown = keyContext.slice(0, 5);
|
|
381
385
|
summaryLines.push('### Key Context');
|
|
382
|
-
summaryLines.push(...
|
|
386
|
+
summaryLines.push(...shown.map((e) => e.line));
|
|
383
387
|
summaryLines.push('');
|
|
388
|
+
if (collector) collector.keyContextIds.push(...shown.map((e) => e.id));
|
|
384
389
|
}
|
|
385
390
|
} else if (!latestSummary && !effectiveQuiet()) {
|
|
386
391
|
// 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-optimize.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { getVocabulary, computeVector, cosineSimilarity, vecTextForRow } from '.
|
|
|
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
|
|
|
24
25
|
const RUNTIME_DIR = join(DB_DIR, 'runtime');
|
|
25
26
|
|
|
@@ -104,8 +105,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
104
105
|
const stmt = db.prepare(`
|
|
105
106
|
SELECT id, title, narrative, type, subtitle, concepts, facts, text, search_aliases, importance, project
|
|
106
107
|
FROM observations
|
|
107
|
-
WHERE
|
|
108
|
-
AND superseded_at IS NULL
|
|
108
|
+
WHERE ${liveObsFilterSql('')}
|
|
109
109
|
AND (search_aliases IS NULL OR search_aliases = '')
|
|
110
110
|
AND LENGTH(COALESCE(narrative, '')) > 100
|
|
111
111
|
AND ${notLowSignalTitleClause('')}
|
|
@@ -119,8 +119,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
119
119
|
const stmt = db.prepare(`
|
|
120
120
|
SELECT id, title, narrative, type, subtitle, concepts, facts, search_aliases, importance, project
|
|
121
121
|
FROM observations
|
|
122
|
-
WHERE
|
|
123
|
-
AND superseded_at IS NULL
|
|
122
|
+
WHERE ${liveObsFilterSql('')}
|
|
124
123
|
AND optimized_at IS NULL
|
|
125
124
|
AND type IN ('bugfix','refactor','feature','decision')
|
|
126
125
|
AND (lesson_learned IS NULL OR lesson_learned = '')
|
|
@@ -459,8 +458,7 @@ export function findMergeCandidates(db, maxClusters = 5, { project } = {}) {
|
|
|
459
458
|
const stmt = db.prepare(`
|
|
460
459
|
SELECT id, title, narrative, project, type, access_count, importance, created_at_epoch, minhash_sig, lesson_learned, concepts, facts
|
|
461
460
|
FROM observations
|
|
462
|
-
WHERE
|
|
463
|
-
AND superseded_at IS NULL
|
|
461
|
+
WHERE ${liveObsFilterSql('')}
|
|
464
462
|
AND optimized_at IS NULL
|
|
465
463
|
AND title IS NOT NULL AND title != ''
|
|
466
464
|
AND created_at_epoch > ?
|
package/hook-precompact.mjs
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
// Differs from SessionStart-on-compact (which fires AFTER compaction):
|
|
6
6
|
// PreCompact ensures memory survives the compaction step itself.
|
|
7
7
|
|
|
8
|
+
import { writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
8
10
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
9
11
|
import { inferProject, debugCatch, debugLog } from './utils.mjs';
|
|
12
|
+
import { RUNTIME_DIR } from './hook-shared.mjs';
|
|
13
|
+
import { keyContextIdsFileName } from './lib/injected-ids.mjs';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
* Build + emit the memory context block on stdout. Pure read; no DB writes
|
|
16
|
+
* Build + emit the memory context block on stdout. Pure read; no DB writes
|
|
17
|
+
* (one runtime marker file: the Key Context ids the re-emitted block renders,
|
|
18
|
+
* refreshing handleUserPrompt's exclude-set — see D#123 in hook.mjs).
|
|
13
19
|
*
|
|
14
20
|
* @param {object} ctx
|
|
15
21
|
* @param {import('better-sqlite3').Database} ctx.db
|
|
@@ -19,9 +25,16 @@ import { inferProject, debugCatch, debugLog } from './utils.mjs';
|
|
|
19
25
|
*/
|
|
20
26
|
export function handlePreCompact({ db, project, sessionId }) {
|
|
21
27
|
try {
|
|
22
|
-
const
|
|
28
|
+
const collector = {};
|
|
29
|
+
const body = buildSessionContextLines(db, project, new Date(), sessionId || null, collector);
|
|
23
30
|
if (!body || String(body).trim() === '') return;
|
|
24
31
|
process.stdout.write(`<claude-mem-context>\n${body}\n</claude-mem-context>\n`);
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(RUNTIME_DIR, keyContextIdsFileName(project, sessionId || null)),
|
|
35
|
+
JSON.stringify({ ids: collector.keyContextIds || [], ts: Date.now(), session: sessionId || null }),
|
|
36
|
+
);
|
|
37
|
+
} catch (e) { debugCatch(e, 'pre-compact-keyctx-marker'); }
|
|
25
38
|
} catch (e) {
|
|
26
39
|
debugCatch(e, 'handlePreCompact');
|
|
27
40
|
}
|
package/hook-shared.mjs
CHANGED
|
@@ -32,6 +32,12 @@ export const STALE_LOCK_MS = 30000; // 30s
|
|
|
32
32
|
export const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 min (title dedup)
|
|
33
33
|
export const RELATED_OBS_WINDOW_MS = 7 * 86400000; // 7 days
|
|
34
34
|
export const FALLBACK_OBS_WINDOW_MS = RELATED_OBS_WINDOW_MS; // same window
|
|
35
|
+
// Candidate rows the SessionStart Key Context surface considers (hook-context.mjs
|
|
36
|
+
// keyObs; each of the two sections then renders at most 5). The user-prompt
|
|
37
|
+
// exclude-set does NOT mirror this query — it reads the ids actually rendered
|
|
38
|
+
// from the keyctx marker (D#123 review C-1: query-mirroring suppressed
|
|
39
|
+
// <memory-context> injection on quiet/adopted projects where nothing renders).
|
|
40
|
+
export const KEY_CONTEXT_LIMIT = 10;
|
|
35
41
|
|
|
36
42
|
// Phase A (v2.31.3+): MEM_QUIET_HOOKS=1 drops descriptive hook/MCP-instruction
|
|
37
43
|
// bodies (File Lessons / Key Context headers, MCP WHEN-TO-USE & decision rules,
|
package/hook.mjs
CHANGED
|
@@ -68,7 +68,7 @@ import { formatTaskImperative } from './lib/task-imperative.mjs';
|
|
|
68
68
|
import { recordSkillAdoption, gcOldShadowShards } from './registry-recommend.mjs';
|
|
69
69
|
import { gcOldMetricShards, recordMetric } from './lib/metrics.mjs';
|
|
70
70
|
import { detectMemOverride } from './lib/mem-override.mjs';
|
|
71
|
-
import { injectedIdsFileName } from './lib/injected-ids.mjs';
|
|
71
|
+
import { injectedIdsFileName, keyContextIdsFileName } from './lib/injected-ids.mjs';
|
|
72
72
|
import { liveObsFilterSql, recencyDecaySql } from './lib/inject-search-core.mjs';
|
|
73
73
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
74
74
|
import { checkForUpdate, getCachedUpdateBanner, isUpdateCheckDue } from './hook-update.mjs';
|
|
@@ -869,7 +869,8 @@ function gcStalePreRecallCooldowns() {
|
|
|
869
869
|
// D#120: the injected-ids marker is also per-session now — same growth
|
|
870
870
|
// shape as the cooldown files, same 24h GC (dedup window is 5 min).
|
|
871
871
|
const isCooldown = name.startsWith('pre-recall-cooldown-') && name.endsWith('.json');
|
|
872
|
-
const isInjectedMarker = name.startsWith('.claude-mem-injected-')
|
|
872
|
+
const isInjectedMarker = name.startsWith('.claude-mem-injected-')
|
|
873
|
+
|| name.startsWith('.claude-mem-keyctx-'); // D#123 Key Context marker — same per-session growth, same 24h GC
|
|
873
874
|
if (!isCooldown && !isInjectedMarker) continue;
|
|
874
875
|
try {
|
|
875
876
|
const p = join(RUNTIME_DIR, name);
|
|
@@ -1471,13 +1472,28 @@ async function handleSessionStart() {
|
|
|
1471
1472
|
// token-budgeted observation pool directly from the DB.
|
|
1472
1473
|
// Pass CC session id so the Working State block is scoped to this session,
|
|
1473
1474
|
// preventing parallel sessions from seeing each other's /clear handoff.
|
|
1474
|
-
const
|
|
1475
|
+
const contextCollector = {};
|
|
1476
|
+
const fullContext = buildSessionContextLines(db, project, now, ccSessionId, contextCollector);
|
|
1475
1477
|
|
|
1476
1478
|
// Stdout is the sole context-delivery channel. The SessionStart hook output
|
|
1477
1479
|
// is injected as a <system-reminder> at session start, giving Claude the
|
|
1478
1480
|
// full summary + handoff state + observations table fresh from the DB.
|
|
1479
1481
|
process.stdout.write(`<claude-mem-context>\n${fullContext}\n</claude-mem-context>\n`);
|
|
1480
1482
|
|
|
1483
|
+
// D#123 (review C-1): persist the Key Context ids ACTUALLY rendered above so
|
|
1484
|
+
// handleUserPrompt can exclude exactly those from <memory-context> — and
|
|
1485
|
+
// nothing else. Under quiet/adopted the sections don't render, the id list is
|
|
1486
|
+
// empty, and prompt-time injection is NOT suppressed (the old query-mirroring
|
|
1487
|
+
// exclude-set blanked the same-project leg on adopted projects). Written
|
|
1488
|
+
// unconditionally (even when empty) so a resumed session can't act on a
|
|
1489
|
+
// previous session's stale marker semantics; 24h GC below.
|
|
1490
|
+
try {
|
|
1491
|
+
writeFileSync(
|
|
1492
|
+
join(RUNTIME_DIR, keyContextIdsFileName(project, ccSessionId)),
|
|
1493
|
+
JSON.stringify({ ids: contextCollector.keyContextIds || [], ts: Date.now(), session: ccSessionId || null }),
|
|
1494
|
+
);
|
|
1495
|
+
} catch (e) { debugCatch(e, 'session-start-keyctx-marker'); }
|
|
1496
|
+
|
|
1481
1497
|
// One-time migration: remove any stale <claude-mem-context> block left in
|
|
1482
1498
|
// CLAUDE.md by pre-v2.30 installs. Idempotent no-op afterwards.
|
|
1483
1499
|
cleanupClaudeMdLegacyBlock();
|
|
@@ -1651,13 +1667,21 @@ async function handleUserPrompt() {
|
|
|
1651
1667
|
// (mirrors CC built-in memoryTypes.ts:215). Skip both Key Context lookup
|
|
1652
1668
|
// and the <memory-context> emission for this turn.
|
|
1653
1669
|
if (!detectMemOverride(promptText)) try {
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1670
|
+
// D#123 (review C-1): the exclude-set is the Key Context ids ACTUALLY
|
|
1671
|
+
// rendered at SessionStart — read from the marker handleSessionStart wrote,
|
|
1672
|
+
// not re-derived from a query. The old query-mirroring set excluded rows
|
|
1673
|
+
// that were never shown (quiet/adopted projects render no Key Context at
|
|
1674
|
+
// all), blanking the same-project <memory-context> leg outright. Missing
|
|
1675
|
+
// or other-session marker → empty set: unknown injections must fail open
|
|
1676
|
+
// (inject, maybe duplicate) rather than fail closed (suppress).
|
|
1677
|
+
const keyContextIds = [];
|
|
1678
|
+
try {
|
|
1679
|
+
const raw = readFileSync(join(RUNTIME_DIR, keyContextIdsFileName(project, ccSessionId)), 'utf8');
|
|
1680
|
+
const { ids, session } = JSON.parse(raw);
|
|
1681
|
+
if (Array.isArray(ids) && !(session && ccSessionId && session !== ccSessionId)) {
|
|
1682
|
+
keyContextIds.push(...ids);
|
|
1683
|
+
}
|
|
1684
|
+
} catch { /* no marker — nothing was injected, exclude nothing */ }
|
|
1661
1685
|
const pathAInjectedIds = [];
|
|
1662
1686
|
|
|
1663
1687
|
// Read IDs already injected by user-prompt-search.js to avoid duplicate injection
|
|
@@ -1686,8 +1710,9 @@ async function handleUserPrompt() {
|
|
|
1686
1710
|
const taskImperativeOn = process.env.CLAUDE_MEM_TASK_IMPERATIVE === 'on'
|
|
1687
1711
|
|| process.env.CLAUDE_MEM_TASK_IMPERATIVE === '1';
|
|
1688
1712
|
// Exclude only ids path-A (user-prompt-search.js) already injected — NOT the
|
|
1689
|
-
//
|
|
1690
|
-
// the pick. The chosen id is excluded from the <memory-context>
|
|
1713
|
+
// SessionStart Key Context set, which overlaps the high-value lesson pool and
|
|
1714
|
+
// would suppress the pick. The chosen id is excluded from the <memory-context>
|
|
1715
|
+
// block below instead.
|
|
1691
1716
|
const imperativePick = taskImperativeOn
|
|
1692
1717
|
? selectImperativeLesson(db, promptText, project, pathAInjectedIds)
|
|
1693
1718
|
: null;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
// lib/inject-search-core.mjs — the
|
|
2
|
-
// 2026-08-14). Shared home for the three SQL
|
|
3
|
-
// hand-copied twins
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// deep-search /
|
|
7
|
-
//
|
|
1
|
+
// lib/inject-search-core.mjs — the retrieval-side shared core (P2-11, audit
|
|
2
|
+
// 2026-08-14; second cut D#123, 2026-08-16). Shared home for the three SQL
|
|
3
|
+
// atoms that kept drifting across hand-copied twins — first on the five
|
|
4
|
+
// injection-side surfaces, then extended across the remaining read surfaces
|
|
5
|
+
// (hook-context / hook-handoff / hook-optimize / mem-cli / search-scoring /
|
|
6
|
+
// tfidf / deep-search / recall-core / recent-core / timeline-core / stats-core /
|
|
7
|
+
// search-core incl. its sessions+events decay arms / maintain-core; the ledger
|
|
8
|
+
// test enforces the full list). Deliberate compressed-only singles (maintain
|
|
9
|
+
// UPDATE guards, stats noise-gauge counts, export tombstone toggles, session-own
|
|
10
|
+
// history) stay inline — see the ledger test's non-member notes:
|
|
8
11
|
//
|
|
9
12
|
// * live-row filter — the compressed+superseded pair whose omission was
|
|
10
13
|
// the superseded-invariant's recurring reopening
|
|
@@ -17,12 +20,12 @@
|
|
|
17
20
|
// behavior factors (audit M-3: wired on every auto
|
|
18
21
|
// surface, missing from the explicit-surface score)
|
|
19
22
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// Each surface keeps its own deliberate pipeline composition (BM25-sort + JS
|
|
24
|
+
// scoring vs SQL full chain vs file-keyed sort — see #8786: per-surface
|
|
25
|
+
// asymmetries stay explicit); only the ATOMS are shared.
|
|
26
|
+
// tests/inject-search-core.test.mjs holds the ledger: consumer files must
|
|
27
|
+
// compose these builders, never re-inline copies (and the decay shape may not
|
|
28
|
+
// be hand-rolled anywhere, benchmark included).
|
|
26
29
|
//
|
|
27
30
|
// Lives under lib/ (not scripts/) so hook.mjs can statically import it without
|
|
28
31
|
// colliding with the installExtractedRelease scripts-dir rename (same constraint
|
package/lib/injected-ids.mjs
CHANGED
|
@@ -27,3 +27,23 @@ export function injectedIdsFileName(project, sessionId) {
|
|
|
27
27
|
const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
|
|
28
28
|
return `${base}-${safe}`;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Runtime-dir FILE NAME for the SessionStart Key Context marker: the obs ids
|
|
33
|
+
* ACTUALLY rendered into the <claude-mem-context> File Lessons / Key Context
|
|
34
|
+
* sections (empty under quiet/adopted). handleUserPrompt reads it as its
|
|
35
|
+
* exclude-set — D#123 review C-1: excluding the injector's QUERY result instead
|
|
36
|
+
* of what was really shown suppressed <memory-context> injection outright on
|
|
37
|
+
* quiet/adopted projects, where Key Context never renders at all.
|
|
38
|
+
* Session-lifetime validity (no time window): the SessionStart block stays in
|
|
39
|
+
* context for the whole session. Swept with the same 24h GC as the marker above.
|
|
40
|
+
* @param {string} project - inferProject() value (already filename-safe)
|
|
41
|
+
* @param {string} [sessionId] - CC session id
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function keyContextIdsFileName(project, sessionId) {
|
|
45
|
+
const base = `.claude-mem-keyctx-${project}`;
|
|
46
|
+
if (!sessionId) return base;
|
|
47
|
+
const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
|
|
48
|
+
return `${base}-${safe}`;
|
|
49
|
+
}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { COMPRESSED_PENDING_PURGE, computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from '../utils.mjs';
|
|
16
16
|
import { rebuildVocabulary, computeVector, _resetVocabCache, vectorsEnabled, vecTextForRow } from '../tfidf.mjs';
|
|
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
|
+
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
18
19
|
|
|
19
20
|
export const STALE_AGE_MS = 30 * 86400000;
|
|
20
21
|
export const OP_CAP = 1000;
|
|
@@ -132,9 +133,8 @@ export function recoverOrphanedChildren(db, { projectFilter = '', baseParams = [
|
|
|
132
133
|
export function recoverBuriedLessons(db, { projectFilter = '', baseParams = [] } = {}) {
|
|
133
134
|
return db.prepare(`
|
|
134
135
|
UPDATE observations SET importance = 1
|
|
135
|
-
WHERE
|
|
136
|
+
WHERE ${liveObsFilterSql('')}
|
|
136
137
|
AND COALESCE(importance, 1) = 0
|
|
137
|
-
AND superseded_at IS NULL
|
|
138
138
|
AND lesson_learned IS NOT NULL AND lesson_learned <> '' AND lower(lesson_learned) <> 'none'
|
|
139
139
|
${projectFilter}
|
|
140
140
|
`).run(...baseParams).changes;
|
|
@@ -453,7 +453,7 @@ export function rebuildVectors(db) {
|
|
|
453
453
|
if (!vocab) return { ok: false, reason: 'no observations to build vocabulary from' };
|
|
454
454
|
const allObs = db.prepare(`
|
|
455
455
|
SELECT id, title, narrative, concepts, lesson_learned, search_aliases FROM observations
|
|
456
|
-
WHERE
|
|
456
|
+
WHERE ${liveObsFilterSql('')}
|
|
457
457
|
`).all();
|
|
458
458
|
let updated = 0;
|
|
459
459
|
const insertStmt = db.prepare('INSERT OR REPLACE INTO observation_vectors (observation_id, vector, vocab_version, created_at_epoch) VALUES (?, ?, ?, ?)');
|
package/lib/recall-core.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { basename } from 'path';
|
|
9
9
|
import { notLowSignalTitleClause } from '../utils.mjs';
|
|
10
|
+
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Recall observations linked to a file (basename or full path). Returns
|
|
@@ -31,8 +32,7 @@ export function recallByFile(db, file, { limit = 10, includeNoise = false } = {}
|
|
|
31
32
|
o.created_at, o.created_at_epoch, o.project
|
|
32
33
|
FROM observations o
|
|
33
34
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
34
|
-
WHERE
|
|
35
|
-
AND o.superseded_at IS NULL
|
|
35
|
+
WHERE ${liveObsFilterSql('o')}
|
|
36
36
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
37
37
|
${noiseClause}
|
|
38
38
|
ORDER BY o.created_at_epoch DESC
|
package/lib/recent-core.mjs
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// MCP wants `project`) — same convention as recall-core, so neither surface needs
|
|
15
15
|
// its own SELECT list.
|
|
16
16
|
|
|
17
|
+
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
18
|
+
|
|
17
19
|
const RECENT_COLS = 'id, type, title, subtitle, importance, project, created_at, created_at_epoch';
|
|
18
20
|
|
|
19
21
|
// Upper bound on rows a single `recent` call may pull, shared so the cap can't
|
|
@@ -38,7 +40,7 @@ export const RECENT_MAX = 1000;
|
|
|
38
40
|
*/
|
|
39
41
|
export function fetchRecent(db, { project = null, type = null, since = null, limit = 10 } = {}) {
|
|
40
42
|
const params = [];
|
|
41
|
-
const wheres = [
|
|
43
|
+
const wheres = [liveObsFilterSql('')];
|
|
42
44
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
43
45
|
if (type) { wheres.push('type = ?'); params.push(type); }
|
|
44
46
|
if (Number.isFinite(since)) { wheres.push('created_at_epoch >= ?'); params.push(since); }
|
package/lib/search-core.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { cjkPrecisionOk, extractCjkLikePatterns } from '../nlp.mjs';
|
|
|
29
29
|
import { computeTier } from '../tier.mjs';
|
|
30
30
|
import { countSearchTotal, attachBodyTokens } from '../search-engine.mjs';
|
|
31
31
|
import { notLowSignalTitleClause } from '../scoring-sql.mjs';
|
|
32
|
+
import { liveObsFilterSql, recencyDecaySql } from './inject-search-core.mjs';
|
|
32
33
|
|
|
33
34
|
/** Sanitize a user query to FTS5 syntax; optionally force OR semantics. */
|
|
34
35
|
export function buildSearchFtsQuery(query, { or = false } = {}) {
|
|
@@ -146,7 +147,7 @@ export function searchSessionsFts(db, { ftsQuery, project = null, projectBoost =
|
|
|
146
147
|
return db.prepare(`
|
|
147
148
|
SELECT s.id, s.request, s.completed, s.project, s.created_at, s.created_at_epoch,
|
|
148
149
|
${SESS_BM25}
|
|
149
|
-
* (
|
|
150
|
+
* ${recencyDecaySql({ tsExpr: 's.created_at_epoch', halfLifeSql: `${DEFAULT_DECAY_HALF_LIFE_MS}.0` })}
|
|
150
151
|
* (CASE WHEN ? IS NOT NULL AND s.project = ? THEN 2.0 ELSE 1.0 END) as score
|
|
151
152
|
FROM session_summaries_fts
|
|
152
153
|
JOIN session_summaries s ON session_summaries_fts.rowid = s.id
|
|
@@ -235,7 +236,7 @@ export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = n
|
|
|
235
236
|
return db.prepare(`
|
|
236
237
|
SELECT e.id, e.event_type, e.title, e.body, e.project, e.importance, e.file_paths, e.created_at_epoch,
|
|
237
238
|
${EVT_BM25}
|
|
238
|
-
* (
|
|
239
|
+
* ${recencyDecaySql({ tsExpr: 'e.created_at_epoch', halfLifeSql: `${DEFAULT_DECAY_HALF_LIFE_MS}.0` })}
|
|
239
240
|
* (CASE WHEN ? IS NOT NULL AND e.project = ? THEN 2.0 ELSE 1.0 END) as score
|
|
240
241
|
FROM events_fts
|
|
241
242
|
JOIN events e ON events_fts.rowid = e.id
|
|
@@ -607,7 +608,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
607
608
|
|
|
608
609
|
// ── Type-list fallback (MCP): obs_type set + 0 matches → list recent of that type ──
|
|
609
610
|
if (obsTypeFallback && results.length === 0 && obsType) {
|
|
610
|
-
const typeWheres = [
|
|
611
|
+
const typeWheres = [liveObsFilterSql(''), 'type = ?'];
|
|
611
612
|
// Mirror the FTS path's low-signal filter (buildObsFtsQuery): this fallback is still a
|
|
612
613
|
// SEARCH surface, so degraded titles ("Modified X", "Error: …") must not lead it —
|
|
613
614
|
// acute for obs_type='change', the noise band. (The no-query recent-listing in
|
package/lib/stats-core.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { inferProject } from '../utils.mjs';
|
|
|
10
10
|
import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
11
11
|
import { TIER_CASE_SQL, tierSqlParams } from '../tier.mjs';
|
|
12
12
|
import { computeNoiseGauge } from './stats-quality.mjs';
|
|
13
|
+
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Compute the primary stats feed. Row shapes are returned exactly as the twin
|
|
@@ -98,7 +99,7 @@ export function computeStatsFeed(db, { project = null, days = 30, now = Date.now
|
|
|
98
99
|
const tierDist = db.prepare(`
|
|
99
100
|
SELECT tier, COUNT(*) as c FROM (
|
|
100
101
|
SELECT ${TIER_CASE_SQL} as tier FROM observations
|
|
101
|
-
WHERE
|
|
102
|
+
WHERE ${liveObsFilterSql('')} ${projectFilter}
|
|
102
103
|
) GROUP BY tier ORDER BY tier
|
|
103
104
|
`).all(...tdParams, ...baseParams);
|
|
104
105
|
const tierMap = Object.fromEntries(tierDist.map((r) => [r.tier, r.c]));
|
package/lib/timeline-core.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { parseIdToken } from './id-routing.mjs';
|
|
15
15
|
import { findFtsAnchor } from '../search-engine.mjs';
|
|
16
16
|
import { sanitizeFtsQuery } from '../utils.mjs';
|
|
17
|
+
import { liveObsFilterSql } from './inject-search-core.mjs';
|
|
17
18
|
|
|
18
19
|
const TIMELINE_COLS = 'id, type, title, subtitle, project, created_at, created_at_epoch';
|
|
19
20
|
|
|
@@ -21,7 +22,7 @@ const TIMELINE_COLS = 'id, type, title, subtitle, project, created_at, created_a
|
|
|
21
22
|
function nearestObservation(db, epoch, project) {
|
|
22
23
|
return db.prepare(`
|
|
23
24
|
SELECT id FROM observations
|
|
24
|
-
WHERE
|
|
25
|
+
WHERE ${liveObsFilterSql('')} ${project ? 'AND project = ?' : ''}
|
|
25
26
|
ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
|
|
26
27
|
`).get(...(project ? [project, epoch] : [epoch]));
|
|
27
28
|
}
|
|
@@ -162,9 +163,9 @@ export function resolveQueryAnchor(db, queryStr, { project = null } = {}) {
|
|
|
162
163
|
|
|
163
164
|
/** No-anchor fallback: most recent live (non-compressed, non-superseded) obs, newest first. */
|
|
164
165
|
export function fetchRecentTimeline(db, { project = null, limit }) {
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const liveFilter = '
|
|
166
|
+
// A superseded row must not lead the "most recent" timeline — same live-row
|
|
167
|
+
// invariant as the before/after window legs (fetchTimelineWindow).
|
|
168
|
+
const liveFilter = liveObsFilterSql('');
|
|
168
169
|
const where = project ? `WHERE ${liveFilter} AND project = ?` : `WHERE ${liveFilter}`;
|
|
169
170
|
const params = project ? [project, limit] : [limit];
|
|
170
171
|
return db.prepare(`
|
|
@@ -200,7 +201,7 @@ export function fetchTimelineWindow(db, anchorId, { before, after, project = nul
|
|
|
200
201
|
const beforeRows = db.prepare(`
|
|
201
202
|
SELECT ${TIMELINE_COLS}
|
|
202
203
|
FROM observations
|
|
203
|
-
WHERE created_at_epoch < ? AND
|
|
204
|
+
WHERE created_at_epoch < ? AND ${liveObsFilterSql('')} ${projectFilter}
|
|
204
205
|
ORDER BY created_at_epoch DESC
|
|
205
206
|
LIMIT ?
|
|
206
207
|
`).all(anchorRow.created_at_epoch, ...baseParams, before).reverse();
|
|
@@ -208,7 +209,7 @@ export function fetchTimelineWindow(db, anchorId, { before, after, project = nul
|
|
|
208
209
|
const afterRows = db.prepare(`
|
|
209
210
|
SELECT ${TIMELINE_COLS}
|
|
210
211
|
FROM observations
|
|
211
|
-
WHERE created_at_epoch > ? AND
|
|
212
|
+
WHERE created_at_epoch > ? AND ${liveObsFilterSql('')} ${projectFilter}
|
|
212
213
|
ORDER BY created_at_epoch ASC
|
|
213
214
|
LIMIT ?
|
|
214
215
|
`).all(anchorRow.created_at_epoch, ...baseParams, after);
|
package/mem-cli.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
|
32
32
|
import { buildSessionContextLines } from './hook-context.mjs';
|
|
33
33
|
import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
|
|
34
34
|
import { parseIntFlag, isNumericToken } from './lib/cli-flags.mjs';
|
|
35
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
35
36
|
import { auditMemdir, memdirPath } from './memdir.mjs';
|
|
36
37
|
import { aggregateProjectCiteRecall } from './lib/citation-tracker.mjs';
|
|
37
38
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
@@ -1604,11 +1605,14 @@ function cmdExport(db, args) {
|
|
|
1604
1605
|
if (rejectBareStringFlags(flags, ['project', 'type', 'from', 'to'])) return;
|
|
1605
1606
|
const wheres = [];
|
|
1606
1607
|
const params = [];
|
|
1607
|
-
// --include-compressed: include compressed observations (aligned with MCP mem_export)
|
|
1608
|
-
|
|
1609
|
-
|
|
1608
|
+
// --include-compressed: include compressed observations (aligned with MCP mem_export).
|
|
1609
|
+
// Superseded rows are excluded either way; the flag only toggles the compressed half
|
|
1610
|
+
// of the live-row pair (backup/export of tombstones is opt-in, retractions are not).
|
|
1611
|
+
if (flags['include-compressed'] === true || flags['include-compressed'] === 'true') {
|
|
1612
|
+
wheres.push('superseded_at IS NULL');
|
|
1613
|
+
} else {
|
|
1614
|
+
wheres.push(liveObsFilterSql(''));
|
|
1610
1615
|
}
|
|
1611
|
-
wheres.push('superseded_at IS NULL');
|
|
1612
1616
|
|
|
1613
1617
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1614
1618
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
@@ -2468,8 +2472,7 @@ function cmdCitationStats(db, args) {
|
|
|
2468
2472
|
SUM(CASE WHEN uncited_streak >= 2 THEN 1 ELSE 0 END) AS at_risk
|
|
2469
2473
|
FROM observations
|
|
2470
2474
|
WHERE created_at_epoch >= ?
|
|
2471
|
-
AND
|
|
2472
|
-
AND superseded_at IS NULL
|
|
2475
|
+
AND ${liveObsFilterSql('')}
|
|
2473
2476
|
GROUP BY project
|
|
2474
2477
|
ORDER BY resolved DESC
|
|
2475
2478
|
`).all(cutoff);
|
|
@@ -2478,8 +2481,7 @@ function cmdCitationStats(db, args) {
|
|
|
2478
2481
|
SELECT id, project, type, title, importance, uncited_streak, cited_count
|
|
2479
2482
|
FROM observations
|
|
2480
2483
|
WHERE uncited_streak >= 2
|
|
2481
|
-
AND
|
|
2482
|
-
AND superseded_at IS NULL
|
|
2484
|
+
AND ${liveObsFilterSql('')}
|
|
2483
2485
|
ORDER BY uncited_streak DESC, importance ASC
|
|
2484
2486
|
LIMIT 20
|
|
2485
2487
|
`).all();
|
|
@@ -2488,8 +2490,7 @@ function cmdCitationStats(db, args) {
|
|
|
2488
2490
|
SELECT id, project, type, title, importance, cited_count
|
|
2489
2491
|
FROM observations
|
|
2490
2492
|
WHERE importance >= 3 AND cited_count >= 1
|
|
2491
|
-
AND
|
|
2492
|
-
AND superseded_at IS NULL
|
|
2493
|
+
AND ${liveObsFilterSql('')}
|
|
2493
2494
|
ORDER BY cited_count DESC
|
|
2494
2495
|
LIMIT 10
|
|
2495
2496
|
`).all();
|
|
@@ -2499,8 +2500,7 @@ function cmdCitationStats(db, args) {
|
|
|
2499
2500
|
FROM observations
|
|
2500
2501
|
WHERE demoted_at IS NOT NULL
|
|
2501
2502
|
AND demoted_at >= ?
|
|
2502
|
-
AND
|
|
2503
|
-
AND superseded_at IS NULL
|
|
2503
|
+
AND ${liveObsFilterSql('')}
|
|
2504
2504
|
ORDER BY demoted_at DESC
|
|
2505
2505
|
LIMIT 10
|
|
2506
2506
|
`).all(cutoff);
|
|
@@ -2515,8 +2515,7 @@ function cmdCitationStats(db, args) {
|
|
|
2515
2515
|
const pollutedRows = db.prepare(`
|
|
2516
2516
|
SELECT COUNT(*) AS n FROM observations
|
|
2517
2517
|
WHERE cited_count > decay_seen_count
|
|
2518
|
-
AND
|
|
2519
|
-
AND superseded_at IS NULL
|
|
2518
|
+
AND ${liveObsFilterSql('')}
|
|
2520
2519
|
`).get();
|
|
2521
2520
|
const dataPollutionNote = pollutedRows.n > 0
|
|
2522
2521
|
? `${pollutedRows.n} obs have cited_count > decay_seen_count (pre-v34 backfill — invariant holds for new data).`
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.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.65.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.65.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",
|
package/search-scoring.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { debugCatch, COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, OBS_BM25 } from
|
|
|
9
9
|
import { BASE_STOP_WORDS } from './stop-words.mjs';
|
|
10
10
|
import { porterStem } from './tfidf.mjs';
|
|
11
11
|
import { CLI_INVOKE } from './cli-path.mjs';
|
|
12
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
12
13
|
|
|
13
14
|
// ─── MCP Server Instructions Builder ───────────────────────────────────────
|
|
14
15
|
// Phase A (v2.31.3+): when quiet=true, drops WHEN-TO-USE proactive-trigger and
|
|
@@ -225,8 +226,7 @@ export function expandQueryByConcepts(db, ftsQuery, project) {
|
|
|
225
226
|
rows = db.prepare(`
|
|
226
227
|
SELECT o.concepts FROM observations_fts
|
|
227
228
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
228
|
-
WHERE observations_fts MATCH ? AND
|
|
229
|
-
AND o.superseded_at IS NULL
|
|
229
|
+
WHERE observations_fts MATCH ? AND ${liveObsFilterSql('o')}
|
|
230
230
|
AND (? IS NULL OR o.project = ?)
|
|
231
231
|
ORDER BY ${OBS_BM25}
|
|
232
232
|
LIMIT 20
|
package/tfidf.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { cjkBigrams } from './utils.mjs';
|
|
|
7
7
|
import { BASE_STOP_WORDS } from './stop-words.mjs';
|
|
8
8
|
import { createHash } from 'crypto';
|
|
9
9
|
import { rrfAccumulate } from './lib/rrf.mjs';
|
|
10
|
+
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
10
11
|
|
|
11
12
|
export const VOCAB_DIM = 512;
|
|
12
13
|
// Fraction of the vocab dimension reserved for the highest-IDF (rarest) terms when the
|
|
@@ -240,7 +241,7 @@ export function vecTextForRow(row) {
|
|
|
240
241
|
export function buildVocabulary(db, { dim = VOCAB_DIM } = {}) {
|
|
241
242
|
const rows = db.prepare(`
|
|
242
243
|
SELECT title, narrative, concepts, lesson_learned, search_aliases FROM observations
|
|
243
|
-
WHERE
|
|
244
|
+
WHERE ${liveObsFilterSql('')}
|
|
244
245
|
`).all();
|
|
245
246
|
|
|
246
247
|
const N = rows.length;
|
|
@@ -444,8 +445,7 @@ export function vectorSearch(db, queryVec, { project, type, vocabVersion, limit
|
|
|
444
445
|
const now = Date.now();
|
|
445
446
|
|
|
446
447
|
const wheres = [
|
|
447
|
-
'
|
|
448
|
-
'o.superseded_at IS NULL',
|
|
448
|
+
liveObsFilterSql('o'),
|
|
449
449
|
'ov.vocab_version = ?',
|
|
450
450
|
];
|
|
451
451
|
const params = [vocabVersion];
|