claude-mem-lite 3.63.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-memory.mjs +6 -9
- package/hook-optimize.mjs +4 -6
- package/hook-precompact.mjs +15 -2
- package/hook-shared.mjs +6 -0
- package/hook.mjs +59 -31
- package/lib/browse-core.mjs +67 -0
- package/lib/delete-core.mjs +18 -1
- package/lib/get-core.mjs +33 -0
- package/lib/inject-search-core.mjs +87 -0
- package/lib/injected-ids.mjs +49 -0
- package/lib/maintain-core.mjs +3 -3
- package/lib/observation-write.mjs +38 -2
- package/lib/recall-core.mjs +2 -2
- package/lib/recent-core.mjs +3 -1
- package/lib/search-core.mjs +14 -4
- package/lib/stats-core.mjs +2 -1
- package/lib/timeline-core.mjs +7 -6
- package/mem-cli.mjs +75 -141
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -1
- package/registry.mjs +53 -1
- package/scoring-sql.mjs +5 -4
- package/scripts/pre-skill-bridge.js +3 -1
- package/scripts/pre-tool-recall.js +13 -9
- package/scripts/prompt-search-utils.mjs +3 -0
- package/scripts/user-prompt-search.js +26 -25
- package/search-engine.mjs +21 -15
- package/search-scoring.mjs +2 -2
- package/server.mjs +43 -112
- package/source-files.mjs +14 -0
- package/tfidf.mjs +3 -3
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
// Lightweight: only imports schema.mjs and utils.mjs, no MCP SDK
|
|
5
5
|
|
|
6
6
|
import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
|
|
7
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25,
|
|
8
|
-
import {
|
|
7
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, notLowSignalTitleClause, stripPrivate, neutralizeContextDelimiters, MAX_UPS_PROMPT_BYTES } from '../utils.mjs';
|
|
8
|
+
import { liveObsFilterSql, injectionRelevanceSql } from '../lib/inject-search-core.mjs';
|
|
9
9
|
import { cjkPrecisionOk } from '../nlp.mjs';
|
|
10
10
|
import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
|
|
11
11
|
import { join, sep } from 'path';
|
|
12
12
|
import { pathToFileURL } from 'url';
|
|
13
13
|
import Database from 'better-sqlite3';
|
|
14
14
|
import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, extractDeferredRefs, DEDUP_STALE_MS, matchRegistrySkillName, detectMemOverride } from './prompt-search-utils.mjs';
|
|
15
|
+
import { injectedIdsFileName } from '../lib/injected-ids.mjs';
|
|
15
16
|
import { getDeferredByIds } from '../lib/deferred-work.mjs';
|
|
16
17
|
import { recommendSkill } from '../registry-recommend.mjs';
|
|
17
18
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
@@ -22,7 +23,12 @@ import { atomicWriteFileSync } from '../lib/atomic-write.mjs';
|
|
|
22
23
|
// Telemetry sink (lib/hook-telemetry.mjs contract): env override for tests, else
|
|
23
24
|
// <data-dir>/runtime — the same dir the sibling hook scripts + `stats` read.
|
|
24
25
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DB_DIR, 'runtime');
|
|
25
|
-
|
|
26
|
+
// D#120: one marker file per CC session — payload-only session keying (M-6) let
|
|
27
|
+
// two concurrent windows full-replace each other's marker, killing dedup between
|
|
28
|
+
// them and resetting `count` on every alternation. Derived per invocation once
|
|
29
|
+
// the session id is parsed from stdin; no session id → legacy project-keyed file.
|
|
30
|
+
const injectedIdsFileFor = (sessionId) =>
|
|
31
|
+
join(DB_DIR, 'runtime', injectedIdsFileName(inferProject(), sessionId));
|
|
26
32
|
// Per-prompt UPS cap. Cut from 5 → 3 after the 2026-05-09 per-hook recall
|
|
27
33
|
// scan (#8255): UPS contributed 74% of silent injected IDs (131/177) at 26%
|
|
28
34
|
// recall, vs PreToolUse:Read at 94% recall on a tighter file-keyed set.
|
|
@@ -66,7 +72,7 @@ const PROMPT_MIN_LENGTH = 15;
|
|
|
66
72
|
// v2.33.1: follow-up prompts ("前面那个", "继续 X", "再看看 Y") are short by
|
|
67
73
|
// nature but semantically depend on prior turns. Once a session has injected
|
|
68
74
|
// memory at least once, relax gates so short follow-ups still get recall.
|
|
69
|
-
// Detection:
|
|
75
|
+
// Detection: injected-ids marker count > 0 within DEDUP_STALE_MS window.
|
|
70
76
|
const FOLLOWUP_PROMPT_MIN_LENGTH = 8;
|
|
71
77
|
const FOLLOWUP_BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN_FOLLOWUP || 5e-6);
|
|
72
78
|
|
|
@@ -173,9 +179,9 @@ export function corpusFloorScale(db) {
|
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
|
|
176
|
-
function isFollowUpSession() {
|
|
182
|
+
function isFollowUpSession(injectedIdsFile) {
|
|
177
183
|
try {
|
|
178
|
-
const raw = readFileSync(
|
|
184
|
+
const raw = readFileSync(injectedIdsFile, 'utf8');
|
|
179
185
|
const { ts, count = 0 } = JSON.parse(raw);
|
|
180
186
|
if (!ts || Date.now() - ts > DEDUP_STALE_MS) return false;
|
|
181
187
|
return count > 0;
|
|
@@ -348,12 +354,7 @@ export function searchByFts(db, queryText, project, limit, typeFilter,
|
|
|
348
354
|
const sql = `
|
|
349
355
|
SELECT o.id, o.type, o.title, o.lesson_learned,
|
|
350
356
|
${OBS_BM25} as bm25_raw,
|
|
351
|
-
${
|
|
352
|
-
* (1.0 + EXP(-0.693 * MAX(0, ? - o.created_at_epoch) / ${TYPE_DECAY_CASE}))
|
|
353
|
-
* ${TYPE_QUALITY_CASE}
|
|
354
|
-
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
355
|
-
* ${noisePenaltyClause('o')}
|
|
356
|
-
* ${citeFactorClause('o')} as relevance
|
|
357
|
+
${injectionRelevanceSql('o')} as relevance
|
|
357
358
|
FROM observations_fts
|
|
358
359
|
JOIN observations o ON o.id = observations_fts.rowid
|
|
359
360
|
WHERE observations_fts MATCH ?
|
|
@@ -361,8 +362,7 @@ export function searchByFts(db, queryText, project, limit, typeFilter,
|
|
|
361
362
|
AND o.importance >= 1
|
|
362
363
|
AND o.created_at_epoch > ?
|
|
363
364
|
AND (? IS NULL OR o.created_at_epoch <= ?)
|
|
364
|
-
AND
|
|
365
|
-
AND o.superseded_at IS NULL
|
|
365
|
+
AND ${liveObsFilterSql('o')}
|
|
366
366
|
AND ${notLowSignalTitleClause('o')}
|
|
367
367
|
${typeClause}
|
|
368
368
|
ORDER BY relevance
|
|
@@ -408,8 +408,7 @@ function searchByFile(db, files, project, limit) {
|
|
|
408
408
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
409
409
|
WHERE o.project = ?
|
|
410
410
|
AND o.importance >= 1
|
|
411
|
-
AND
|
|
412
|
-
AND o.superseded_at IS NULL
|
|
411
|
+
AND ${liveObsFilterSql('o')}
|
|
413
412
|
AND o.created_at_epoch > ?
|
|
414
413
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
415
414
|
AND ${notLowSignalTitleClause('o')}
|
|
@@ -484,8 +483,7 @@ function searchRecent(db, project, limit) {
|
|
|
484
483
|
FROM observations
|
|
485
484
|
WHERE project = ?
|
|
486
485
|
AND importance >= 1
|
|
487
|
-
AND
|
|
488
|
-
AND superseded_at IS NULL
|
|
486
|
+
AND ${liveObsFilterSql('')}
|
|
489
487
|
AND created_at_epoch > ?
|
|
490
488
|
AND ${notLowSignalTitleClause('')}
|
|
491
489
|
ORDER BY created_at_epoch DESC
|
|
@@ -636,6 +634,9 @@ async function main() {
|
|
|
636
634
|
// would never legitimately be wrapped in <private>).
|
|
637
635
|
if (rawPrompt.startsWith('<task-notification>')) return;
|
|
638
636
|
|
|
637
|
+
// D#120: session-keyed dedup marker — every read/write below uses this path.
|
|
638
|
+
const injectedIdsFile = injectedIdsFileFor(hookData.session_id);
|
|
639
|
+
|
|
639
640
|
// Strip <private>...</private> blocks before length gates and FTS query
|
|
640
641
|
// construction — private content must not pad effective length nor leak
|
|
641
642
|
// into the FTS MATCH query terms. Mirrors hook.mjs handleUserPrompt.
|
|
@@ -665,7 +666,7 @@ async function main() {
|
|
|
665
666
|
// Namespace dedup ids as "D<id>" (parity with the "P<id>" prompt-corpus
|
|
666
667
|
// convention) so obs ids can't collide in the shared injected-ids file.
|
|
667
668
|
const dedupIds = openRows.map(r => `D${r.id}`);
|
|
668
|
-
if (openRows.length > 0 && !shouldSkipByDedup(dedupIds,
|
|
669
|
+
if (openRows.length > 0 && !shouldSkipByDedup(dedupIds, injectedIdsFile, hookData.session_id)) {
|
|
669
670
|
const lines = ['[mem] Deferred work referenced in prompt (open items, full detail):'];
|
|
670
671
|
for (const r of openRows) {
|
|
671
672
|
const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
|
|
@@ -684,7 +685,7 @@ async function main() {
|
|
|
684
685
|
let prevIds = [];
|
|
685
686
|
let prevCount = 0;
|
|
686
687
|
try {
|
|
687
|
-
const prev = JSON.parse(readFileSync(
|
|
688
|
+
const prev = JSON.parse(readFileSync(injectedIdsFile, 'utf8'));
|
|
688
689
|
// M-6: inherit only same-session (or legacy) state — another session's
|
|
689
690
|
// ids/count must not carry over. Atomic write below: a torn concurrent
|
|
690
691
|
// write left the shared marker as invalid JSON (dedup silently off).
|
|
@@ -694,7 +695,7 @@ async function main() {
|
|
|
694
695
|
prevCount = prev.count || 0;
|
|
695
696
|
}
|
|
696
697
|
} catch {}
|
|
697
|
-
atomicWriteFileSync(
|
|
698
|
+
atomicWriteFileSync(injectedIdsFile, JSON.stringify({
|
|
698
699
|
ids: [...new Set([...prevIds.map(String), ...dedupIds])],
|
|
699
700
|
ts: Date.now(),
|
|
700
701
|
count: prevCount + 1,
|
|
@@ -713,7 +714,7 @@ async function main() {
|
|
|
713
714
|
// "fix bug now") that carry too few content tokens for a meaningful FTS lookup.
|
|
714
715
|
// v2.33.1: follow-up prompts in an already-active session get a lower gate —
|
|
715
716
|
// short continuations ("前面那个?", "does it work?") depend on prior context.
|
|
716
|
-
const followUp = isFollowUpSession();
|
|
717
|
+
const followUp = isFollowUpSession(injectedIdsFile);
|
|
717
718
|
const promptMinLen = followUp ? FOLLOWUP_PROMPT_MIN_LENGTH : PROMPT_MIN_LENGTH;
|
|
718
719
|
if (computeEffectiveLen(promptText.trim()) < promptMinLen) { try { db?.close(); } catch {} return; }
|
|
719
720
|
const bm25Floor = followUp ? FOLLOWUP_BM25_MIN_SCORE : BM25_MIN_SCORE;
|
|
@@ -870,7 +871,7 @@ async function main() {
|
|
|
870
871
|
const candidateIds = rows.length > 0
|
|
871
872
|
? rows.map(r => r.id)
|
|
872
873
|
: promptRows.map(r => `P${r.id}`);
|
|
873
|
-
const dedupSkip = shouldSkipByDedup(candidateIds,
|
|
874
|
+
const dedupSkip = shouldSkipByDedup(candidateIds, injectedIdsFile, hookData.session_id);
|
|
874
875
|
|
|
875
876
|
const output = !dedupSkip
|
|
876
877
|
? (rows.length > 0 ? formatResults(rows) : formatPromptResults(promptRows))
|
|
@@ -881,14 +882,14 @@ async function main() {
|
|
|
881
882
|
try {
|
|
882
883
|
let prevCount = 0;
|
|
883
884
|
try {
|
|
884
|
-
const prev = JSON.parse(readFileSync(
|
|
885
|
+
const prev = JSON.parse(readFileSync(injectedIdsFile, 'utf8'));
|
|
885
886
|
// M-6: same-session (or legacy) count only; atomic write (torn-write guard).
|
|
886
887
|
if (prev.ts && Date.now() - prev.ts < DEDUP_STALE_MS
|
|
887
888
|
&& !(prev.session && hookData.session_id && prev.session !== hookData.session_id)) {
|
|
888
889
|
prevCount = prev.count || 0;
|
|
889
890
|
}
|
|
890
891
|
} catch {}
|
|
891
|
-
atomicWriteFileSync(
|
|
892
|
+
atomicWriteFileSync(injectedIdsFile, JSON.stringify({
|
|
892
893
|
ids: candidateIds,
|
|
893
894
|
ts: Date.now(),
|
|
894
895
|
count: prevCount + 1,
|
package/search-engine.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// module exists to eliminate.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
OBS_BM25,
|
|
9
|
+
OBS_BM25, TYPE_QUALITY_CASE,
|
|
10
10
|
DEFAULT_DECAY_HALF_LIFE_MS,
|
|
11
11
|
notLowSignalTitleClause, LOW_SIGNAL_TITLE,
|
|
12
12
|
relaxFtsQueryToOr, debugLog, debugCatch, estimateTokens,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { citeFactorClause } from './scoring-sql.mjs';
|
|
16
16
|
import { getVocabulary, computeVector, vectorSearch, rrfMerge, vectorsEnabled } from './tfidf.mjs';
|
|
17
17
|
import { extractPRFTerms, expandQueryByConcepts } from './search-scoring.mjs';
|
|
18
|
+
import { liveObsFilterSql, recencyDecaySql } from './lib/inject-search-core.mjs';
|
|
18
19
|
|
|
19
20
|
// Scoring expressions — full adds project boost + access bonus; simple is for
|
|
20
21
|
// expansion paths where boost would over-amplify already-loose matches.
|
|
@@ -35,7 +36,7 @@ import { extractPRFTerms, expandQueryByConcepts } from './search-scoring.mjs';
|
|
|
35
36
|
// carry zero cite/noise state); the behavioral pin lives in
|
|
36
37
|
// tests/audit-fixes-20260816.test.mjs (M-3).
|
|
37
38
|
const FULL_SCORE = `${OBS_BM25}
|
|
38
|
-
* (
|
|
39
|
+
* ${recencyDecaySql({ tsExpr: 'MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))' })}
|
|
39
40
|
* ${TYPE_QUALITY_CASE}
|
|
40
41
|
* (CASE WHEN ? IS NOT NULL AND o.project = ? THEN 2.0 ELSE 1.0 END)
|
|
41
42
|
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
@@ -44,11 +45,16 @@ const FULL_SCORE = `${OBS_BM25}
|
|
|
44
45
|
* ${noisePenaltyClause('o')}
|
|
45
46
|
* ${citeFactorClause('o')}`;
|
|
46
47
|
|
|
48
|
+
// D#121: noisePenalty joins SIMPLE (an entrenched-noise row demoted 0.2× on every
|
|
49
|
+
// direct surface re-entered concept/PRF expansion at full magnitude); citeFactor
|
|
50
|
+
// stays OUT deliberately — it can amplify 3×, and SIMPLE exists precisely to avoid
|
|
51
|
+
// amplifying already-loose expansion matches. Noise only shrinks: safe direction.
|
|
47
52
|
const SIMPLE_SCORE = `${OBS_BM25}
|
|
48
|
-
* (
|
|
53
|
+
* ${recencyDecaySql({ tsExpr: 'MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))' })}
|
|
49
54
|
* ${TYPE_QUALITY_CASE}
|
|
50
55
|
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
51
|
-
* (1.0 + 0.3 * (o.lesson_learned IS NOT NULL AND o.lesson_learned NOT IN ('', 'none')))
|
|
56
|
+
* (1.0 + 0.3 * (o.lesson_learned IS NOT NULL AND o.lesson_learned NOT IN ('', 'none')))
|
|
57
|
+
* ${noisePenaltyClause('o')}`;
|
|
52
58
|
|
|
53
59
|
// Shared column set for fetching an observation surfaced by the vector arm — used by BOTH
|
|
54
60
|
// the RRF-merge branch (FTS also had results) and the FTS-empty fallback branch. Single
|
|
@@ -71,8 +77,7 @@ export function buildObsFtsQuery(scoring, { multiplier, withSnippet, withOffset,
|
|
|
71
77
|
FROM observations_fts
|
|
72
78
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
73
79
|
WHERE observations_fts MATCH ?
|
|
74
|
-
AND
|
|
75
|
-
AND o.superseded_at IS NULL
|
|
80
|
+
AND ${liveObsFilterSql('o')}
|
|
76
81
|
AND (? IS NULL OR o.project = ?)
|
|
77
82
|
AND (? IS NULL OR o.type = ?)
|
|
78
83
|
AND (? IS NULL OR o.created_at_epoch >= ?)
|
|
@@ -123,8 +128,7 @@ export function countObsFtsMatches(db, { ftsQuery, args = {}, epochFrom = null,
|
|
|
123
128
|
FROM observations_fts
|
|
124
129
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
125
130
|
WHERE observations_fts MATCH ?
|
|
126
|
-
AND
|
|
127
|
-
AND o.superseded_at IS NULL
|
|
131
|
+
AND ${liveObsFilterSql('o')}
|
|
128
132
|
AND (? IS NULL OR o.project = ?)
|
|
129
133
|
AND (? IS NULL OR o.type = ?)
|
|
130
134
|
AND (? IS NULL OR o.created_at_epoch >= ?)
|
|
@@ -316,8 +320,7 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
|
|
|
316
320
|
const topResults = db.prepare(`
|
|
317
321
|
SELECT o.title, o.narrative FROM observations_fts
|
|
318
322
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
319
|
-
WHERE observations_fts MATCH ? AND
|
|
320
|
-
AND o.superseded_at IS NULL
|
|
323
|
+
WHERE observations_fts MATCH ? AND ${liveObsFilterSql('o')}
|
|
321
324
|
AND (? IS NULL OR o.project = ?)
|
|
322
325
|
ORDER BY ${OBS_BM25}
|
|
323
326
|
LIMIT 8
|
|
@@ -384,10 +387,9 @@ export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfL
|
|
|
384
387
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
385
388
|
WHERE observations_fts MATCH ?
|
|
386
389
|
AND (? IS NULL OR o.project = ?)
|
|
387
|
-
AND
|
|
388
|
-
AND o.superseded_at IS NULL
|
|
390
|
+
AND ${liveObsFilterSql('o')}
|
|
389
391
|
ORDER BY ${OBS_BM25}
|
|
390
|
-
* (
|
|
392
|
+
* ${recencyDecaySql({ tsExpr: 'o.created_at_epoch', halfLifeSql: `${halfLifeMs}.0` })}
|
|
391
393
|
LIMIT 1
|
|
392
394
|
`;
|
|
393
395
|
const stmt = db.prepare(sql);
|
|
@@ -412,7 +414,7 @@ export function searchObservationsHybrid(db, ctx) {
|
|
|
412
414
|
|
|
413
415
|
if (!ftsQuery) {
|
|
414
416
|
const params = [];
|
|
415
|
-
const wheres = [
|
|
417
|
+
const wheres = [liveObsFilterSql('')];
|
|
416
418
|
if (args.project) { wheres.push('project = ?'); params.push(args.project); }
|
|
417
419
|
if (args.obs_type) { wheres.push('type = ?'); params.push(args.obs_type); }
|
|
418
420
|
if (epochFrom !== null) { wheres.push('created_at_epoch >= ?'); params.push(epochFrom); }
|
|
@@ -467,8 +469,12 @@ export function searchObservationsHybrid(db, ctx) {
|
|
|
467
469
|
// rescued rows now (they are the only relevance evidence available).
|
|
468
470
|
if (results.length > 0 && results.length < Math.ceil(limit / 2)) {
|
|
469
471
|
const existingIds = new Set(results.map(r => r.id));
|
|
472
|
+
// D#122 ②: capture the PRIMARY count before concept expansion mutates
|
|
473
|
+
// `results` — PRF's >=3 gate is meant to read direct-match evidence, not
|
|
474
|
+
// rows the concept pass just added.
|
|
475
|
+
const primaryCount = results.length;
|
|
470
476
|
expandObsByConceptCo(db, ctx, now, existingIds, results, includeNoise);
|
|
471
|
-
expandObsByPRF(db, ctx, now,
|
|
477
|
+
expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includeNoise);
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
// Vector search + RRF hybrid merge
|
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/server.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import { truncate, typeIcon, inferProject,
|
|
8
|
+
import { truncate, typeIcon, inferProject, fmtDate, debugLog, debugCatch, isPathConfined } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
10
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
11
|
-
import { reRankWithContext,
|
|
11
|
+
import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
14
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
@@ -22,9 +22,10 @@ import {
|
|
|
22
22
|
OP_CAP, STALE_AGE_MS,
|
|
23
23
|
} from './lib/maintain-core.mjs';
|
|
24
24
|
import { snapshotDb } from './lib/db-backup.mjs';
|
|
25
|
-
import { deleteObservations } from './lib/delete-core.mjs';
|
|
25
|
+
import { deleteObservations, previewDeleteRows } from './lib/delete-core.mjs';
|
|
26
|
+
import { fetchObsDetail, OBS_FIELDS, SESSION_DETAIL_FIELDS } from './lib/get-core.mjs';
|
|
27
|
+
import { collectBrowseTiers, getActiveMemorySessionId, BROWSE_TIERS, BROWSE_TIER_LABELS } from './lib/browse-core.mjs';
|
|
26
28
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
27
|
-
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
28
29
|
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
29
30
|
import { buildLessonNudge } from './lib/save-nudge.mjs';
|
|
30
31
|
import { formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cli/common.mjs';
|
|
@@ -45,11 +46,11 @@ function descriptionOf(name) {
|
|
|
45
46
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
46
47
|
import { join, sep } from 'path';
|
|
47
48
|
import { homedir } from 'os';
|
|
48
|
-
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
49
|
+
import { ensureRegistryDb, upsertResource, collectRegistryStats, listResourcesRanked, formatRegistryListLine } from './registry.mjs';
|
|
49
50
|
import { searchResources } from './registry-retriever.mjs';
|
|
50
51
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
51
52
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
52
|
-
import {
|
|
53
|
+
import { applyObsUpdate } from './lib/observation-write.mjs';
|
|
53
54
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
54
55
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
55
56
|
import { fetchRecent } from './lib/recent-core.mjs';
|
|
@@ -599,8 +600,6 @@ server.registerTool(
|
|
|
599
600
|
return { content: [{ type: 'text', text: 'No valid IDs provided.' }] };
|
|
600
601
|
}
|
|
601
602
|
|
|
602
|
-
const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
|
|
603
|
-
|
|
604
603
|
// `fields` filter only makes sense for obs rows; session/prompt ignore it.
|
|
605
604
|
// Validate when obs is queried — throw on all-invalid, note on partial-invalid.
|
|
606
605
|
let fieldsNote = '';
|
|
@@ -622,12 +621,8 @@ server.registerTool(
|
|
|
622
621
|
const foundBySource = { obs: new Set(), session: new Set(), prompt: new Set(), event: new Set() };
|
|
623
622
|
|
|
624
623
|
if (bySrc.obs.length > 0) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${ph})`).run(Date.now(), ...bySrc.obs);
|
|
628
|
-
autoBoostIfNeeded(db, bySrc.obs);
|
|
629
|
-
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
630
|
-
const rows = db.prepare(`SELECT * FROM observations WHERE id IN (${ph}) ORDER BY created_at_epoch ASC`).all(...bySrc.obs);
|
|
624
|
+
// Access-bump + fetch via the shared get-core (P2-12) — single source with CLI get.
|
|
625
|
+
const rows = fetchObsDetail(db, bySrc.obs);
|
|
631
626
|
const renderFields = obsFieldFilter || OBS_FIELDS;
|
|
632
627
|
for (const row of rows) {
|
|
633
628
|
foundBySource.obs.add(row.id);
|
|
@@ -650,7 +645,9 @@ server.registerTool(
|
|
|
650
645
|
if (bySrc.session.length > 0) {
|
|
651
646
|
const ph = bySrc.session.map(() => '?').join(',');
|
|
652
647
|
const rows = db.prepare(`SELECT * FROM session_summaries WHERE id IN (${ph}) ORDER BY created_at_epoch ASC`).all(...bySrc.session);
|
|
653
|
-
|
|
648
|
+
// SESSION_DETAIL_FIELDS (get-core, P2-12): shared full set — adds remaining_items
|
|
649
|
+
// (searchable via FTS but previously unrendered on BOTH detail faces).
|
|
650
|
+
const sessFields = SESSION_DETAIL_FIELDS;
|
|
654
651
|
for (const row of rows) {
|
|
655
652
|
foundBySource.session.add(row.id);
|
|
656
653
|
const lines = [`── S#${row.id} ──`];
|
|
@@ -765,21 +762,15 @@ server.registerTool(
|
|
|
765
762
|
inputSchema: memDeleteSchema,
|
|
766
763
|
},
|
|
767
764
|
safeHandler(async (args) => {
|
|
768
|
-
|
|
769
|
-
const rows = db.
|
|
770
|
-
SELECT id, type, title, project FROM observations WHERE id IN (${placeholders})
|
|
771
|
-
`).all(...args.ids);
|
|
765
|
+
// Shared preview body (lib/delete-core, P2-12) — single source with CLI delete.
|
|
766
|
+
const { rows, lines: previewLines } = previewDeleteRows(db, args.ids);
|
|
772
767
|
|
|
773
768
|
if (rows.length === 0) {
|
|
774
769
|
return { content: [{ type: 'text', text: 'No observations found for given IDs.' }] };
|
|
775
770
|
}
|
|
776
771
|
|
|
777
772
|
if (!args.confirm) {
|
|
778
|
-
|
|
779
|
-
const lines = [`Preview: ${rows.length} observation(s) will be deleted:\n`];
|
|
780
|
-
for (const r of rows) {
|
|
781
|
-
lines.push(` #${r.id} [${r.type}] ${truncate(r.title || '(untitled)', 80)} | ${r.project}`);
|
|
782
|
-
}
|
|
773
|
+
const lines = [`Preview: ${rows.length} observation(s) will be deleted:\n`, ...previewLines];
|
|
783
774
|
lines.push(`\nCall mem_delete(ids=[...], confirm=true) to execute.`);
|
|
784
775
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
785
776
|
}
|
|
@@ -1425,51 +1416,27 @@ server.registerTool(
|
|
|
1425
1416
|
}
|
|
1426
1417
|
|
|
1427
1418
|
if (action === 'list') {
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
const params = typeFilter ? [typeFilter, 'active'] : ['active'];
|
|
1431
|
-
// T3-P2-A: order by adoption then recommendation (CLI parity), and coalesce NULL counts
|
|
1432
|
-
// so the output shows "adopt:0" rather than the jarring "adopt:null".
|
|
1433
|
-
const resources = rdb.prepare(`
|
|
1434
|
-
SELECT name, type, invocation_name, recommend_count, adopt_count, capability_summary
|
|
1435
|
-
FROM resources ${where}
|
|
1436
|
-
ORDER BY COALESCE(adopt_count, 0) DESC, COALESCE(recommend_count, 0) DESC, type, name
|
|
1437
|
-
`).all(...params);
|
|
1438
|
-
|
|
1419
|
+
// Shared ranked query + row line (registry.mjs, P2-12) — single source with CLI list.
|
|
1420
|
+
const resources = listResourcesRanked(rdb, { type: args.type });
|
|
1439
1421
|
if (resources.length === 0) return { content: [{ type: 'text', text: 'No resources found.' }] };
|
|
1440
|
-
|
|
1441
|
-
const lines = resources.map(r =>
|
|
1442
|
-
`${r.type === 'skill' ? 'S' : 'A'} ${r.name}${r.invocation_name ? ` (${r.invocation_name})` : ''} — rec:${r.recommend_count ?? 0} adopt:${r.adopt_count ?? 0} — ${truncate(r.capability_summary || '', 80)}`
|
|
1443
|
-
);
|
|
1422
|
+
const lines = resources.map(formatRegistryListLine);
|
|
1444
1423
|
return { content: [{ type: 'text', text: `Resources (${resources.length}):\n${lines.join('\n')}` }] };
|
|
1445
1424
|
}
|
|
1446
1425
|
|
|
1447
1426
|
if (action === 'stats') {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const
|
|
1451
|
-
SELECT name, type, adopt_count, recommend_count
|
|
1452
|
-
FROM resources WHERE status = ? AND adopt_count > 0
|
|
1453
|
-
ORDER BY adopt_count DESC LIMIT 10
|
|
1454
|
-
`).all('active');
|
|
1455
|
-
const zeroAdopt = rdb.prepare(`
|
|
1456
|
-
SELECT COUNT(*) as c FROM resources
|
|
1457
|
-
WHERE status = ? AND recommend_count > 0 AND adopt_count = 0
|
|
1458
|
-
`).get('active');
|
|
1459
|
-
const userAdded = rdb.prepare(`
|
|
1460
|
-
SELECT COUNT(*) as c FROM resources WHERE status = ? AND source = 'user'
|
|
1461
|
-
`).get('active');
|
|
1462
|
-
|
|
1427
|
+
// Shared collection (registry.mjs collectRegistryStats, P2-12) — single
|
|
1428
|
+
// source with the CLI registry stats action.
|
|
1429
|
+
const s = collectRegistryStats(rdb);
|
|
1463
1430
|
const lines = [
|
|
1464
1431
|
`Registry Stats:`,
|
|
1465
|
-
` Total active: ${total
|
|
1466
|
-
...byType.map(t => ` ${t.type}: ${t.c}`),
|
|
1467
|
-
` User-added: ${userAdded
|
|
1468
|
-
` Zero adoption (recommended but never adopted): ${zeroAdopt
|
|
1432
|
+
` Total active: ${s.total}`,
|
|
1433
|
+
...s.byType.map(t => ` ${t.type}: ${t.c}`),
|
|
1434
|
+
` User-added: ${s.userAdded}`,
|
|
1435
|
+
` Zero adoption (recommended but never adopted): ${s.zeroAdopt}`,
|
|
1469
1436
|
];
|
|
1470
|
-
if (topAdopted.length > 0) {
|
|
1437
|
+
if (s.topAdopted.length > 0) {
|
|
1471
1438
|
lines.push('', 'Top adopted:');
|
|
1472
|
-
for (const r of topAdopted) {
|
|
1439
|
+
for (const r of s.topAdopted) {
|
|
1473
1440
|
lines.push(` ${r.name} (${r.type}): ${r.adopt_count}/${r.recommend_count}`);
|
|
1474
1441
|
}
|
|
1475
1442
|
}
|
|
@@ -1696,26 +1663,15 @@ server.registerTool(
|
|
|
1696
1663
|
const obs = db.prepare('SELECT id, title FROM observations WHERE id = ?').get(args.id);
|
|
1697
1664
|
if (!obs) return { content: [{ type: 'text', text: `Observation #${args.id} not found` }], isError: true };
|
|
1698
1665
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
}
|
|
1707
|
-
if (updates.length === 0) return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
|
|
1708
|
-
|
|
1709
|
-
params.push(args.id);
|
|
1710
|
-
|
|
1711
|
-
// Atomic: update fields + rebuild derived columns (FTS text + vector) via the
|
|
1712
|
-
// shared core — single source with CLI cmdUpdate (lib/observation-write.mjs).
|
|
1713
|
-
db.transaction(() => {
|
|
1714
|
-
db.prepare(`UPDATE observations SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
1715
|
-
rebuildObservationDerived(db, args.id);
|
|
1716
|
-
})();
|
|
1666
|
+
// Shared mutation (lib/observation-write applyObsUpdate, P2-12): scrub + UPDATE +
|
|
1667
|
+
// derived-column rebuild in one transaction — single source with CLI cmdUpdate.
|
|
1668
|
+
const updatedCols = applyObsUpdate(db, args.id, {
|
|
1669
|
+
title: args.title, narrative: args.narrative, type: args.type,
|
|
1670
|
+
importance: args.importance, lesson_learned: args.lesson_learned, concepts: args.concepts,
|
|
1671
|
+
});
|
|
1672
|
+
if (updatedCols.length === 0) return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
|
|
1717
1673
|
|
|
1718
|
-
return { content: [{ type: 'text', text: `Updated observation #${args.id}: ${
|
|
1674
|
+
return { content: [{ type: 'text', text: `Updated observation #${args.id}: ${updatedCols.join(', ')}` }] };
|
|
1719
1675
|
})
|
|
1720
1676
|
);
|
|
1721
1677
|
|
|
@@ -1869,33 +1825,18 @@ server.registerTool(
|
|
|
1869
1825
|
const limit = args.limit ?? (tierFilter ? 20 : 5);
|
|
1870
1826
|
const now = Date.now();
|
|
1871
1827
|
|
|
1872
|
-
//
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const
|
|
1878
|
-
const
|
|
1879
|
-
|
|
1880
|
-
const tiers = ['working', 'active', 'archive'];
|
|
1881
|
-
const tierLabels = { working: '🔴 Working Memory', active: '🟡 Active Memory', archive: '🔵 Archive' };
|
|
1882
|
-
const showTiers = tierFilter ? [tierFilter] : tiers;
|
|
1828
|
+
// Shared collection (lib/browse-core, P2-12) — single source with CLI browse.
|
|
1829
|
+
const { showTiers, tierData, tierCounts, grandTotal } = collectBrowseTiers(db, {
|
|
1830
|
+
project, tierFilter, limit, now,
|
|
1831
|
+
currentSessionId: getActiveMemorySessionId(db, project),
|
|
1832
|
+
});
|
|
1833
|
+
const tiers = BROWSE_TIERS;
|
|
1834
|
+
const tierLabels = BROWSE_TIER_LABELS;
|
|
1883
1835
|
|
|
1884
1836
|
const lines = [`Memory Dashboard (${project})\n`];
|
|
1885
|
-
let grandTotal = 0;
|
|
1886
|
-
const tierCounts = {};
|
|
1887
1837
|
|
|
1888
1838
|
for (const tier of showTiers) {
|
|
1889
|
-
const
|
|
1890
|
-
SELECT COUNT(*) as c FROM (
|
|
1891
|
-
SELECT ${TIER_CASE_SQL} as tier FROM observations
|
|
1892
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
|
|
1893
|
-
) WHERE tier = ?
|
|
1894
|
-
`).get(...params, project, tier);
|
|
1895
|
-
const count = countRow?.c ?? 0;
|
|
1896
|
-
tierCounts[tier] = count;
|
|
1897
|
-
grandTotal += count;
|
|
1898
|
-
|
|
1839
|
+
const { count, rows } = tierData[tier];
|
|
1899
1840
|
lines.push(`${tierLabels[tier]} (${count})`);
|
|
1900
1841
|
|
|
1901
1842
|
if (tier === 'archive' && !tierFilter) {
|
|
@@ -1905,16 +1846,6 @@ server.registerTool(
|
|
|
1905
1846
|
|
|
1906
1847
|
if (count === 0) { lines.push(''); continue; }
|
|
1907
1848
|
|
|
1908
|
-
const rows = db.prepare(`
|
|
1909
|
-
SELECT * FROM (
|
|
1910
|
-
SELECT id, type, title, created_at, created_at_epoch, ${TIER_CASE_SQL} as tier
|
|
1911
|
-
FROM observations
|
|
1912
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
|
|
1913
|
-
) WHERE tier = ?
|
|
1914
|
-
ORDER BY created_at_epoch DESC
|
|
1915
|
-
LIMIT ?
|
|
1916
|
-
`).all(...params, project, tier, limit);
|
|
1917
|
-
|
|
1918
1849
|
for (const r of rows) {
|
|
1919
1850
|
lines.push(` #${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || '(untitled)', 80)} | ${fmtDate(r.created_at)}`);
|
|
1920
1851
|
}
|
package/source-files.mjs
CHANGED
|
@@ -119,6 +119,20 @@ export const SOURCE_FILES = [
|
|
|
119
119
|
// colliding with the scripts/ directory rename in installExtractedRelease
|
|
120
120
|
// — see the SWITCHABLE_PATHS loop in hook-update.mjs.
|
|
121
121
|
'lib/mem-override.mjs',
|
|
122
|
+
// D#120: injected-ids marker file-name derivation, shared by hook.mjs +
|
|
123
|
+
// scripts/user-prompt-search.js + scripts/pre-tool-recall.js. Under lib/ for
|
|
124
|
+
// the same scripts-dir-rename reason as mem-override.mjs above.
|
|
125
|
+
'lib/injected-ids.mjs',
|
|
126
|
+
// P2-11: injection-side shared SQL core (live-row filter / clamped decay /
|
|
127
|
+
// injection relevance chain). Statically imported by hook.mjs, hook-memory.mjs,
|
|
128
|
+
// search-engine.mjs AND the standalone hook scripts — missing it from the
|
|
129
|
+
// manifest kills every retrieval surface on auto-update.
|
|
130
|
+
'lib/inject-search-core.mjs',
|
|
131
|
+
// P2-12 twin cores: get/browse shared data collection for the CLI/MCP pairs
|
|
132
|
+
// (update lives in observation-write, delete-preview in delete-core, registry
|
|
133
|
+
// stats/list in registry.mjs — all already listed).
|
|
134
|
+
'lib/get-core.mjs',
|
|
135
|
+
'lib/browse-core.mjs',
|
|
122
136
|
// v2.61 dedup refactor: shared "save one observation" pipeline used by both
|
|
123
137
|
// mem-cli.mjs::cmdSave and server.mjs::mem_save. Statically imported from both
|
|
124
138
|
// entry points; missing it from the manifest broke MCP saves on auto-update.
|
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];
|