claude-mem-lite 2.61.0 → 2.62.1
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/hook-shared.mjs +32 -1
- package/hook.mjs +12 -1
- package/install.mjs +33 -13
- package/mem-cli.mjs +9 -18
- package/package.json +1 -1
- package/search-engine.mjs +49 -0
- package/server.mjs +8 -19
package/hook-shared.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
8
8
|
import { inferProject, debugCatch } from './utils.mjs';
|
|
9
9
|
import { ensureDb, DB_DIR } from './schema.mjs';
|
|
10
10
|
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared, flattenForCLI as _flattenForCLI } from './haiku-client.mjs';
|
|
@@ -62,6 +62,37 @@ export const HANDOFF_ANCHOR_MAX_AGE = 72 * 3600000; // 72h cap on gi
|
|
|
62
62
|
export const HANDOFF_MATCH_THRESHOLD = 3; // min weighted score
|
|
63
63
|
export const CONTINUE_KEYWORDS = /继续|接着|上次|之前的|前面的|刚才|\bcontinue\b|\bresume\b|\bwhere[\s-]+we[\s-]+left\b|\bpick[\s-]+up\b|\bcarry[\s-]+on\b/i;
|
|
64
64
|
|
|
65
|
+
// Orphan-sweep threshold for `ep-flush-*` / `pending-*` runtime artifacts.
|
|
66
|
+
// handleLLMEpisode's worst-case round-trip is ~60s (delay + LLM call + DB
|
|
67
|
+
// write); 1h leaves a wide safety margin against deleting an in-flight file.
|
|
68
|
+
// Older orphans are crashed workers or pre-shutdown buffers that no live
|
|
69
|
+
// caller will ever pick up, so sweeping them on SessionStart is safe.
|
|
70
|
+
export const ORPHAN_EPISODE_AGE_MS = 60 * 60 * 1000;
|
|
71
|
+
|
|
72
|
+
// Sweep stale `ep-flush-*` and `pending-*` files in `runtimeDir` whose mtime
|
|
73
|
+
// is older than `ageMs` (default 1h). Returns the number of files removed.
|
|
74
|
+
// fs-only — no DB / no network. Used by handleSessionStart auto-maintain to
|
|
75
|
+
// prevent the doctor "Stale temp files" warning from accumulating across
|
|
76
|
+
// crashes; equivalent to the manual path in `node install.mjs cleanup` but
|
|
77
|
+
// age-gated so concurrent in-flight workers are never raced.
|
|
78
|
+
export function sweepOrphanEpisodeFiles(runtimeDir, { ageMs = ORPHAN_EPISODE_AGE_MS, now = Date.now() } = {}) {
|
|
79
|
+
let entries;
|
|
80
|
+
try { entries = readdirSync(runtimeDir); } catch { return 0; }
|
|
81
|
+
const cutoff = now - ageMs;
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const f of entries) {
|
|
84
|
+
if (!(f.startsWith('ep-flush-') || f.startsWith('pending-'))) continue;
|
|
85
|
+
const full = join(runtimeDir, f);
|
|
86
|
+
try {
|
|
87
|
+
if (statSync(full).mtimeMs < cutoff) {
|
|
88
|
+
unlinkSync(full);
|
|
89
|
+
count++;
|
|
90
|
+
}
|
|
91
|
+
} catch { /* concurrent unlink / permission — ignore */ }
|
|
92
|
+
}
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
// Ensure runtime directory exists
|
|
66
97
|
try { if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true }); } catch {}
|
|
67
98
|
|
package/hook.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
RUNTIME_DIR, EPISODE_BUFFER_SIZE, EPISODE_TIME_GAP_MS,
|
|
41
41
|
SESSION_EXPIRY_MS, STALE_SESSION_MS, STALE_LOCK_MS,
|
|
42
42
|
sessionFile, getSessionId, createSessionId, openDb,
|
|
43
|
-
spawnBackground,
|
|
43
|
+
spawnBackground, sweepOrphanEpisodeFiles,
|
|
44
44
|
} from './hook-shared.mjs';
|
|
45
45
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
46
46
|
import { extractCitationsFromTranscript, bumpCitationAccess, computeCiteRecall } from './lib/citation-tracker.mjs';
|
|
@@ -885,6 +885,17 @@ async function handleSessionStart() {
|
|
|
885
885
|
}
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
// Orphan sweep: remove `ep-flush-*` / `pending-*` runtime files older
|
|
889
|
+
// than 1h. handleLLMEpisode normally unlinks its own tmpFile on every
|
|
890
|
+
// exit path, but a crashed worker (OOM, host reboot, kill -9) leaves
|
|
891
|
+
// the file behind, and the doctor "Stale temp files" warning then
|
|
892
|
+
// accumulates indefinitely. fs-only; runs inside the 24h gate so it
|
|
893
|
+
// shares cadence with the rest of auto-maintain.
|
|
894
|
+
try {
|
|
895
|
+
const swept = sweepOrphanEpisodeFiles(RUNTIME_DIR);
|
|
896
|
+
if (swept > 0) debugLog('DEBUG', 'auto-maintain', `swept ${swept} orphan ep-flush/pending file(s)`);
|
|
897
|
+
} catch (e) { debugCatch(e, 'auto-maintain-orphan-sweep'); }
|
|
898
|
+
|
|
888
899
|
// Mark maintenance as done (24h gate) — even though compression runs in background
|
|
889
900
|
writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
|
|
890
901
|
// Weekly summary grouping runs in background to avoid blocking SessionStart
|
package/install.mjs
CHANGED
|
@@ -280,6 +280,19 @@ function ok(msg) { console.log(` ✓ ${msg}`); }
|
|
|
280
280
|
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
|
281
281
|
function fail(msg) { console.log(` ✗ ${msg}`); }
|
|
282
282
|
|
|
283
|
+
// Doctor's final summary line. Pure function so the 4-way contract
|
|
284
|
+
// (clean / warnings-only / issues / mixed) is unit-testable without spinning
|
|
285
|
+
// up the full doctor pipeline. `issues` are ✗-level (action required);
|
|
286
|
+
// `warnings` are ⚠-level (informational, "All checks passed!" must NOT lie
|
|
287
|
+
// about them).
|
|
288
|
+
export function buildDoctorSummary(issues, warnings) {
|
|
289
|
+
const wPlural = warnings === 1 ? '' : 's';
|
|
290
|
+
if (issues === 0 && warnings === 0) return 'All checks passed!';
|
|
291
|
+
if (issues === 0) return `All critical checks passed (${warnings} warning${wPlural}).`;
|
|
292
|
+
const warnSuffix = warnings > 0 ? ` (+${warnings} warning${wPlural})` : '';
|
|
293
|
+
return `${issues} issue(s) found.${warnSuffix}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
// Dev installs symlink server.mjs → the project's source file. Used to suppress
|
|
284
297
|
// misleading "first run" messages since hook-update.mjs skips state-writes in
|
|
285
298
|
// this mode (see hook-update.mjs isDevMode).
|
|
@@ -1152,6 +1165,13 @@ async function status() {
|
|
|
1152
1165
|
async function doctor() {
|
|
1153
1166
|
console.log('\nclaude-mem-lite doctor\n');
|
|
1154
1167
|
let issues = 0;
|
|
1168
|
+
let warnings = 0;
|
|
1169
|
+
// Doctor-local ⚠ helper: visually identical to the file-level `warn`, but
|
|
1170
|
+
// bumps `warnings` so the summary line can distinguish "fully green" from
|
|
1171
|
+
// "warnings present". Used for informational ⚠ checks; the two ⚠ paths
|
|
1172
|
+
// that ALSO bump `issues` (stale procs, dev drift) keep using the file-level
|
|
1173
|
+
// `warn` directly to avoid double-counting.
|
|
1174
|
+
const dwarn = (msg) => { warnings++; console.log(` ⚠ ${msg}`); };
|
|
1155
1175
|
|
|
1156
1176
|
// Node version
|
|
1157
1177
|
const nodeVer = process.version;
|
|
@@ -1209,7 +1229,7 @@ async function doctor() {
|
|
|
1209
1229
|
} else if (hasHooks) {
|
|
1210
1230
|
ok('Plugin lifecycle: hooks active');
|
|
1211
1231
|
} else {
|
|
1212
|
-
|
|
1232
|
+
dwarn('Plugin lifecycle: hooks not configured');
|
|
1213
1233
|
}
|
|
1214
1234
|
|
|
1215
1235
|
// Database
|
|
@@ -1232,7 +1252,7 @@ async function doctor() {
|
|
|
1232
1252
|
if (healthy) {
|
|
1233
1253
|
ok('FTS5 integrity: all indexes healthy');
|
|
1234
1254
|
} else {
|
|
1235
|
-
|
|
1255
|
+
dwarn('FTS5 integrity issues detected:');
|
|
1236
1256
|
for (const d of details) log(` ${d}`);
|
|
1237
1257
|
log(' Attempting FTS5 rebuild...');
|
|
1238
1258
|
const { rebuilt, errors } = rebuildFTS(rwDb);
|
|
@@ -1243,17 +1263,17 @@ async function doctor() {
|
|
|
1243
1263
|
rwDb.close();
|
|
1244
1264
|
}
|
|
1245
1265
|
} catch (e) {
|
|
1246
|
-
|
|
1266
|
+
dwarn('FTS5 integrity check failed: ' + e.message);
|
|
1247
1267
|
}
|
|
1248
1268
|
} else {
|
|
1249
|
-
|
|
1269
|
+
dwarn('FTS5 index: missing (will be created on server start)');
|
|
1250
1270
|
}
|
|
1251
1271
|
} catch (e) {
|
|
1252
1272
|
fail('Database: ' + e.message);
|
|
1253
1273
|
issues++;
|
|
1254
1274
|
}
|
|
1255
1275
|
} else {
|
|
1256
|
-
|
|
1276
|
+
dwarn('Database: not found (will be created)');
|
|
1257
1277
|
}
|
|
1258
1278
|
|
|
1259
1279
|
// Check for stale processes
|
|
@@ -1287,10 +1307,10 @@ async function doctor() {
|
|
|
1287
1307
|
// short-circuits before writing state (see hook-update.mjs isDevMode).
|
|
1288
1308
|
ok('Update state: skipped (dev mode — symlinked install)');
|
|
1289
1309
|
} else {
|
|
1290
|
-
|
|
1310
|
+
dwarn('Update state: no state file (first run?)');
|
|
1291
1311
|
}
|
|
1292
1312
|
} catch {
|
|
1293
|
-
|
|
1313
|
+
dwarn('Update state: failed to read');
|
|
1294
1314
|
}
|
|
1295
1315
|
|
|
1296
1316
|
// Dev drift: in dev-mode installs, all SOURCE_FILES entries should be
|
|
@@ -1310,7 +1330,7 @@ async function doctor() {
|
|
|
1310
1330
|
}
|
|
1311
1331
|
// Prod (all plain) install: no message — dev-drift is a dev-only concern.
|
|
1312
1332
|
} catch (e) {
|
|
1313
|
-
|
|
1333
|
+
dwarn('Dev drift: check failed — ' + e.message);
|
|
1314
1334
|
}
|
|
1315
1335
|
|
|
1316
1336
|
// Stale temp files
|
|
@@ -1329,12 +1349,12 @@ async function doctor() {
|
|
|
1329
1349
|
}
|
|
1330
1350
|
}
|
|
1331
1351
|
if (staleCount > 0) {
|
|
1332
|
-
|
|
1352
|
+
dwarn(`Stale temp files: ${staleCount} found (run: node install.mjs cleanup)`);
|
|
1333
1353
|
} else {
|
|
1334
1354
|
ok('Stale temp files: none');
|
|
1335
1355
|
}
|
|
1336
1356
|
} catch {
|
|
1337
|
-
|
|
1357
|
+
dwarn('Stale temp files: check failed');
|
|
1338
1358
|
}
|
|
1339
1359
|
|
|
1340
1360
|
// DB stats
|
|
@@ -1350,7 +1370,7 @@ async function doctor() {
|
|
|
1350
1370
|
db.close();
|
|
1351
1371
|
ok(`DB stats: ${sizeMB}MB, ${obsCount} observations, ${sessCount} sessions`);
|
|
1352
1372
|
} catch (e) {
|
|
1353
|
-
|
|
1373
|
+
dwarn('DB stats: ' + e.message);
|
|
1354
1374
|
}
|
|
1355
1375
|
}
|
|
1356
1376
|
|
|
@@ -1364,14 +1384,14 @@ async function doctor() {
|
|
|
1364
1384
|
sizeStr = execFileSync('du', ['-sh', pluginCacheBase], { encoding: 'utf8', timeout: 5000 }).trim().split('\t')[0];
|
|
1365
1385
|
} catch { sizeStr = '?'; }
|
|
1366
1386
|
if (versions.length > 3) {
|
|
1367
|
-
|
|
1387
|
+
dwarn(`Plugin cache: ${versions.length} versions (${sizeStr}) — run setup.sh or update to auto-prune to 3`);
|
|
1368
1388
|
} else {
|
|
1369
1389
|
ok(`Plugin cache: ${versions.length} version(s) (${sizeStr})`);
|
|
1370
1390
|
}
|
|
1371
1391
|
} catch {}
|
|
1372
1392
|
}
|
|
1373
1393
|
|
|
1374
|
-
console.log(`\n ${issues
|
|
1394
|
+
console.log(`\n ${buildDoctorSummary(issues, warnings)}\n`);
|
|
1375
1395
|
}
|
|
1376
1396
|
|
|
1377
1397
|
// ─── Settings helpers ───────────────────────────────────────────────────────
|
package/mem-cli.mjs
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, jaccardSimilarity, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, isoWeekKey, COMPRESSED_PENDING_PURGE,
|
|
7
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, jaccardSimilarity, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, isoWeekKey, COMPRESSED_PENDING_PURGE, SESS_BM25, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause } from './utils.mjs';
|
|
8
8
|
import { cjkPrecisionOk } from './nlp.mjs';
|
|
9
9
|
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
10
10
|
import { resolveProject } from './project-utils.mjs';
|
|
11
11
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
12
12
|
import { getVocabulary, computeVector, rebuildVocabulary, _resetVocabCache } from './tfidf.mjs';
|
|
13
13
|
import { autoBoostIfNeeded, reRankWithContext, markSuperseded } from './server-internals.mjs';
|
|
14
|
-
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
14
|
+
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
15
15
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
16
16
|
import { searchResources } from './registry-retriever.mjs';
|
|
17
17
|
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
@@ -663,25 +663,16 @@ function cmdTimeline(db, args) {
|
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
-
// Support query-based anchor: `timeline --query "search terms"` or positional
|
|
667
|
-
//
|
|
666
|
+
// Support query-based anchor: `timeline --query "search terms"` or positional.
|
|
667
|
+
// Routes through shared findFtsAnchor (paired-path with MCP mem_timeline)
|
|
668
|
+
// so AND→OR fallback semantics match `search` — without this, queries like
|
|
669
|
+
// "ep-flush leak" miss rows whose title is "ep-flush ... leaked" that
|
|
670
|
+
// search would otherwise find via OR relaxation.
|
|
668
671
|
const queryStr = flags.query || positional.join(' ');
|
|
669
672
|
if ((!anchorId || isNaN(anchorId)) && queryStr) {
|
|
670
673
|
const ftsQuery = sanitizeFtsQuery(queryStr);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const match = db.prepare(`
|
|
674
|
-
SELECT o.id FROM observations_fts
|
|
675
|
-
JOIN observations o ON observations_fts.rowid = o.id
|
|
676
|
-
WHERE observations_fts MATCH ?
|
|
677
|
-
AND (? IS NULL OR o.project = ?)
|
|
678
|
-
AND COALESCE(o.compressed_into, 0) = 0
|
|
679
|
-
ORDER BY ${OBS_BM25}
|
|
680
|
-
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
|
|
681
|
-
LIMIT 1
|
|
682
|
-
`).get(ftsQuery, project ?? null, project ?? null, nowT);
|
|
683
|
-
if (match) anchorId = match.id;
|
|
684
|
-
}
|
|
674
|
+
const found = findFtsAnchor(db, { ftsQuery, project: project ?? null });
|
|
675
|
+
if (found) anchorId = found;
|
|
685
676
|
}
|
|
686
677
|
|
|
687
678
|
// No anchor: show most recent observations (aligned with MCP mem_timeline fallback)
|
package/package.json
CHANGED
package/search-engine.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE,
|
|
10
|
+
DEFAULT_DECAY_HALF_LIFE_MS,
|
|
10
11
|
notLowSignalTitleClause, LOW_SIGNAL_TITLE,
|
|
11
12
|
relaxFtsQueryToOr, debugLog, debugCatch,
|
|
12
13
|
} from './utils.mjs';
|
|
@@ -141,6 +142,54 @@ function expandObsByPRF(db, ctx, now, primaryCount, existingIds, results, includ
|
|
|
141
142
|
* perSourceOffset, currentProject, limit, orFallbackFired }
|
|
142
143
|
* @returns {Array} list of result objects (mutated ctx may set orFallbackFired)
|
|
143
144
|
*/
|
|
145
|
+
/**
|
|
146
|
+
* Resolve `timeline --query "..."` / mem_timeline auto-anchor to a single
|
|
147
|
+
* observation id. Shared between mem-cli.mjs cmdTimeline and server.mjs
|
|
148
|
+
* mem_timeline so both surfaces use identical AND→OR fallback semantics
|
|
149
|
+
* (paired-path discipline per #8217).
|
|
150
|
+
*
|
|
151
|
+
* Pipeline:
|
|
152
|
+
* 1. FTS5 MATCH with the sanitized query (AND-by-default), recency-weighted
|
|
153
|
+
* 2. If AND returns 0 → relaxFtsQueryToOr fallback (mirrors searchObservationsHybrid)
|
|
154
|
+
*
|
|
155
|
+
* Returns the matched observation id, or null. Always skips compressed rows.
|
|
156
|
+
*
|
|
157
|
+
* @param {Database} db
|
|
158
|
+
* @param {object} opts
|
|
159
|
+
* @param {string|null} opts.ftsQuery pre-sanitized FTS5 query
|
|
160
|
+
* @param {string|null} [opts.project] restrict to this project (boost-by-membership; null = no filter)
|
|
161
|
+
* @param {number} [opts.nowT] Date.now() override (for deterministic tests)
|
|
162
|
+
* @param {number} [opts.halfLifeMs] recency half-life (default DEFAULT_DECAY_HALF_LIFE_MS)
|
|
163
|
+
* @returns {number|null}
|
|
164
|
+
*/
|
|
165
|
+
export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfLifeMs = DEFAULT_DECAY_HALF_LIFE_MS } = {}) {
|
|
166
|
+
if (!ftsQuery) return null;
|
|
167
|
+
const now = nowT ?? Date.now();
|
|
168
|
+
const sql = `
|
|
169
|
+
SELECT o.id FROM observations_fts
|
|
170
|
+
JOIN observations o ON observations_fts.rowid = o.id
|
|
171
|
+
WHERE observations_fts MATCH ?
|
|
172
|
+
AND (? IS NULL OR o.project = ?)
|
|
173
|
+
AND COALESCE(o.compressed_into, 0) = 0
|
|
174
|
+
ORDER BY ${OBS_BM25}
|
|
175
|
+
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${halfLifeMs}.0))
|
|
176
|
+
LIMIT 1
|
|
177
|
+
`;
|
|
178
|
+
const stmt = db.prepare(sql);
|
|
179
|
+
try {
|
|
180
|
+
const m = stmt.get(ftsQuery, project, project, now);
|
|
181
|
+
if (m) return m.id;
|
|
182
|
+
} catch (e) { debugCatch(e, 'findFtsAnchor-and'); }
|
|
183
|
+
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
184
|
+
if (orQuery && orQuery !== ftsQuery) {
|
|
185
|
+
try {
|
|
186
|
+
const m = stmt.get(orQuery, project, project, now);
|
|
187
|
+
if (m) return m.id;
|
|
188
|
+
} catch (e) { debugCatch(e, 'findFtsAnchor-or'); }
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
144
193
|
export function searchObservationsHybrid(db, ctx) {
|
|
145
194
|
const { ftsQuery, args, epochFrom, epochTo, perSourceLimit, perSourceOffset, currentProject, limit } = ctx;
|
|
146
195
|
const results = [];
|
package/server.mjs
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
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 { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, fmtDate, isoWeekKey, debugLog, debugCatch, COMPRESSED_PENDING_PURGE,
|
|
8
|
+
import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, fmtDate, isoWeekKey, debugLog, debugCatch, COMPRESSED_PENDING_PURGE, SESS_BM25, DEFAULT_DECAY_HALF_LIFE_MS, isPathConfined, notLowSignalTitleClause } from './utils.mjs';
|
|
9
9
|
import { extractCjkLikePatterns, cjkPrecisionOk } from './nlp.mjs';
|
|
10
10
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
11
11
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
|
|
12
12
|
import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './server-internals.mjs';
|
|
13
|
-
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
|
+
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
14
14
|
import { effectiveQuiet } from './hook-shared.mjs';
|
|
15
15
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
16
16
|
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
@@ -103,7 +103,7 @@ function resolveProject(name) { return _resolveProjectShared(db, name); }
|
|
|
103
103
|
// Importance: 0.5 + 0.5 × importance (range 0.5–2.0)
|
|
104
104
|
// Access bonus: 1 + 0.1 × ln(1 + access_count)
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// SESS_BM25, TYPE_DECAY_CASE imported from utils.mjs
|
|
107
107
|
const RECENCY_HALF_LIFE_MS = DEFAULT_DECAY_HALF_LIFE_MS;
|
|
108
108
|
|
|
109
109
|
// ─── MCP Server ─────────────────────────────────────────────────────────────
|
|
@@ -612,24 +612,13 @@ server.registerTool(
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
-
// Auto-find anchor via FTS (with recency decay)
|
|
615
|
+
// Auto-find anchor via FTS (with recency decay). Routes through shared
|
|
616
|
+
// findFtsAnchor so CLI `timeline --query` and MCP mem_timeline use
|
|
617
|
+
// identical AND→OR fallback semantics (paired-path per #8217).
|
|
616
618
|
if (!anchorId && args.query) {
|
|
617
619
|
const ftsQuery = sanitizeFtsQuery(args.query);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const row = db.prepare(`
|
|
621
|
-
SELECT o.id
|
|
622
|
-
FROM observations_fts
|
|
623
|
-
JOIN observations o ON observations_fts.rowid = o.id
|
|
624
|
-
WHERE observations_fts MATCH ?
|
|
625
|
-
AND (? IS NULL OR o.project = ?)
|
|
626
|
-
AND COALESCE(o.compressed_into, 0) = 0
|
|
627
|
-
ORDER BY ${OBS_BM25}
|
|
628
|
-
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${RECENCY_HALF_LIFE_MS}.0))
|
|
629
|
-
LIMIT 1
|
|
630
|
-
`).get(ftsQuery, args.project ?? null, args.project ?? null, nowT);
|
|
631
|
-
if (row) anchorId = row.id;
|
|
632
|
-
}
|
|
620
|
+
const found = findFtsAnchor(db, { ftsQuery, project: args.project ?? null });
|
|
621
|
+
if (found) anchorId = found;
|
|
633
622
|
}
|
|
634
623
|
|
|
635
624
|
// No anchor: return most recent
|