claude-mem-lite 3.44.0 → 3.46.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/adopt-content.mjs +13 -0
- package/cli/common.mjs +1 -0
- package/lib/id-routing.mjs +15 -10
- package/lib/search-core.mjs +23 -7
- package/lib/timeline-core.mjs +12 -0
- package/mem-cli.mjs +61 -20
- package/package.json +1 -1
- package/search-engine.mjs +11 -4
- package/server.mjs +47 -14
- package/tool-schemas.mjs +9 -9
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.46.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.46.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/adopt-content.mjs
CHANGED
|
@@ -148,6 +148,19 @@ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该
|
|
|
148
148
|
| \`${CLI_INVOKE} get 42,43\` | 按 ID 展开 |
|
|
149
149
|
| \`${CLI_INVOKE} timeline --anchor 42\` | 时间线上下文 |
|
|
150
150
|
|
|
151
|
+
## CLI 速查(写入 / 记录)
|
|
152
|
+
|
|
153
|
+
写入类工具多从 \`tools/list\` 隐藏 → 只能走 CLI。下表带**硬上限**(超限直接报错,别撞了才知道);完整 flag 见 \`${CLI_INVOKE} help\`。
|
|
154
|
+
|
|
155
|
+
| 命令 | 签名(含硬约束) |
|
|
156
|
+
|------|------------------|
|
|
157
|
+
| 存观测 | \`${CLI_INVOKE} save "<text>" --type bugfix\\|decision --lesson "<≤500 字符>" [--importance 1-3] [--closes-deferred N]\` — \`<text>\` **必填定位参数**;\`--lesson\` 超 500 直接 fail |
|
|
158
|
+
| 推迟工作 | \`${CLI_INVOKE} defer add "<title ≤200>" [--priority 1\\|2\\|3] [--detail "<约束+为何推迟>"]\` — 标题 >200 挪到 \`--detail\` |
|
|
159
|
+
| 改某条 | \`${CLI_INVOKE} update <id> [--lesson "<≤500>"] [--title T] [--type T] [--importance 1-3] [--narrative T] [--concepts "a b c"]\` |
|
|
160
|
+
| 事件日志 | \`${CLI_INVOKE} activity save --type <bugfix\\|lesson\\|bug\\|discovery\\|refactor\\|feature\\|observation\\|decision> "<title>" [--body T] [--files f1,f2]\` |
|
|
161
|
+
|
|
162
|
+
\`maintain\` / \`optimize\` / \`compress\` 见上方「维护 / 管理类工具」;\`maintain --ops\` 取值 \`cleanup,decay,boost,demote_pinned,dedup,purge_stale,rebuild_vectors,vacuum\`,\`--retain-days\` ∈ [7,365]。
|
|
163
|
+
|
|
151
164
|
## 卸载 / 关闭
|
|
152
165
|
|
|
153
166
|
- \`${CLI_INVOKE} unadopt\`:移除 CLAUDE.md 托管块 + \`.claude/plugin_claude_mem_lite.md\`;
|
package/cli/common.mjs
CHANGED
|
@@ -209,5 +209,6 @@ export function formatProbeHints(probe) {
|
|
|
209
209
|
if (probe.obs.length > 0) hints.push(`#${probe.obs.join(', #')} (obs)`);
|
|
210
210
|
if (probe.session.length > 0) hints.push(`S#${probe.session.join(', S#')} (session)`);
|
|
211
211
|
if (probe.prompt.length > 0) hints.push(`P#${probe.prompt.join(', P#')} (prompt)`);
|
|
212
|
+
if (probe.event?.length > 0) hints.push(`E#${probe.event.join(', E#')} (event)`);
|
|
212
213
|
return hints;
|
|
213
214
|
}
|
package/lib/id-routing.mjs
CHANGED
|
@@ -10,18 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Parse an ID token as it appears in search output or CLI positional args.
|
|
13
|
-
* Accepts: `123`, `#123`, `P#123` / `p123` (prompt), `S#123` / `s123` (session)
|
|
13
|
+
* Accepts: `123`, `#123`, `P#123` / `p123` (prompt), `S#123` / `s123` (session),
|
|
14
|
+
* `E#123` / `e123` (event — the canonical event-typed store surfaced as `E#N` by mem_search).
|
|
14
15
|
* @param {unknown} raw
|
|
15
|
-
* @returns {{ source: 'obs'|'session'|'prompt'|null, id: number } | null}
|
|
16
|
+
* @returns {{ source: 'obs'|'session'|'prompt'|'event'|null, id: number } | null}
|
|
16
17
|
* source===null means no explicit prefix — caller picks default (typically 'obs').
|
|
17
18
|
*/
|
|
18
19
|
export function parseIdToken(raw) {
|
|
19
|
-
const m = /^([
|
|
20
|
+
const m = /^([EePpSs]?)#?(\d+)$/.exec(String(raw).trim());
|
|
20
21
|
if (!m) return null;
|
|
21
22
|
const p = m[1].toUpperCase();
|
|
22
23
|
const id = parseInt(m[2], 10);
|
|
23
24
|
if (!Number.isFinite(id) || id <= 0) return null;
|
|
24
|
-
const source = p === 'P' ? 'prompt' : p === 'S' ? 'session' : null;
|
|
25
|
+
const source = p === 'P' ? 'prompt' : p === 'S' ? 'session' : p === 'E' ? 'event' : null;
|
|
25
26
|
return { source, id };
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -35,11 +36,11 @@ export function parseIdToken(raw) {
|
|
|
35
36
|
* per-token prefixes. Un-prefixed tokens fall back to `defaultSource`.
|
|
36
37
|
*
|
|
37
38
|
* @param {Array<string|number>} tokens Mixed input — order preserved within each bucket.
|
|
38
|
-
* @param {{explicit?: 'obs'|'session'|'prompt'|null, defaultSource?: 'obs'|'session'|'prompt'}} opts
|
|
39
|
-
* @returns {{bySrc: {obs:number[], session:number[], prompt:number[]}, invalid: string[]}}
|
|
39
|
+
* @param {{explicit?: 'obs'|'session'|'prompt'|'event'|null, defaultSource?: 'obs'|'session'|'prompt'|'event'}} opts
|
|
40
|
+
* @returns {{bySrc: {obs:number[], session:number[], prompt:number[], event:number[]}, invalid: string[]}}
|
|
40
41
|
*/
|
|
41
42
|
export function bucketIdTokens(tokens, { explicit = null, defaultSource = 'obs' } = {}) {
|
|
42
|
-
const bySrc = { obs: [], session: [], prompt: [] };
|
|
43
|
+
const bySrc = { obs: [], session: [], prompt: [], event: [] };
|
|
43
44
|
const invalid = [];
|
|
44
45
|
for (const raw of tokens) {
|
|
45
46
|
if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
|
|
@@ -60,11 +61,11 @@ export function bucketIdTokens(tokens, { explicit = null, defaultSource = 'obs'
|
|
|
60
61
|
*
|
|
61
62
|
* @param {import('better-sqlite3').Database} db
|
|
62
63
|
* @param {number[]} ids Numeric IDs to probe (non-negative ints).
|
|
63
|
-
* @param {Set<'obs'|'session'|'prompt'>} excludeSrcs Sources to skip.
|
|
64
|
-
* @returns {{obs:number[], session:number[], prompt:number[]}}
|
|
64
|
+
* @param {Set<'obs'|'session'|'prompt'|'event'>} excludeSrcs Sources to skip.
|
|
65
|
+
* @returns {{obs:number[], session:number[], prompt:number[], event:number[]}}
|
|
65
66
|
*/
|
|
66
67
|
export function probeOtherSources(db, ids, excludeSrcs) {
|
|
67
|
-
const result = { obs: [], session: [], prompt: [] };
|
|
68
|
+
const result = { obs: [], session: [], prompt: [], event: [] };
|
|
68
69
|
if (!ids || ids.length === 0) return result;
|
|
69
70
|
const placeholders = ids.map(() => '?').join(',');
|
|
70
71
|
try {
|
|
@@ -80,6 +81,10 @@ export function probeOtherSources(db, ids, excludeSrcs) {
|
|
|
80
81
|
const hits = db.prepare(`SELECT id FROM user_prompts WHERE id IN (${placeholders})`).all(...ids);
|
|
81
82
|
result.prompt = hits.map(r => r.id);
|
|
82
83
|
}
|
|
84
|
+
if (!excludeSrcs.has('event')) {
|
|
85
|
+
const hits = db.prepare(`SELECT id FROM events WHERE id IN (${placeholders})`).all(...ids);
|
|
86
|
+
result.event = hits.map(r => r.id);
|
|
87
|
+
}
|
|
83
88
|
} catch { /* best-effort hint; never block the caller */ }
|
|
84
89
|
return result;
|
|
85
90
|
}
|
package/lib/search-core.mjs
CHANGED
|
@@ -222,12 +222,15 @@ export function searchPromptsFts(db, { query, ftsQuery, project = null, epochFro
|
|
|
222
222
|
* column, so no noise gate (parity with searchEvents in lib/activity.mjs). Returns raw rows
|
|
223
223
|
* { id, event_type, title, body, project, importance, file_paths, created_at_epoch, score }.
|
|
224
224
|
*/
|
|
225
|
-
export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = null, epochFrom = null, epochTo = null, perSourceLimit, perSourceOffset = 0 }) {
|
|
225
|
+
export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = null, epochFrom = null, epochTo = null, eventType = null, importance = null, perSourceLimit, perSourceOffset = 0 }) {
|
|
226
226
|
const wheres = ['events_fts MATCH ?', 'e.superseded_at_epoch IS NULL'];
|
|
227
227
|
const params = [Date.now(), projectBoost, projectBoost, ftsQuery];
|
|
228
228
|
if (project) { wheres.push('e.project = ?'); params.push(project); }
|
|
229
229
|
if (epochFrom !== null) { wheres.push('e.created_at_epoch >= ?'); params.push(epochFrom); }
|
|
230
230
|
if (epochTo !== null) { wheres.push('e.created_at_epoch <= ?'); params.push(epochTo); }
|
|
231
|
+
// D#76: event_type filter (obs_type maps here) + importance floor (events carry importance).
|
|
232
|
+
if (eventType) { wheres.push('e.event_type = ?'); params.push(eventType); }
|
|
233
|
+
if (importance) { wheres.push('COALESCE(e.importance, 1) >= ?'); params.push(importance); }
|
|
231
234
|
params.push(perSourceLimit, perSourceOffset);
|
|
232
235
|
return db.prepare(`
|
|
233
236
|
SELECT e.id, e.event_type, e.title, e.body, e.project, e.importance, e.file_paths, e.created_at_epoch,
|
|
@@ -321,7 +324,7 @@ export function applyTierFilter(db, results, { tier, sourceKey, currentProject }
|
|
|
321
324
|
export function finalizeSearchPage(db, results, {
|
|
322
325
|
isDeep, offset, limit, effectiveSource, ftsQuery, orFallbackFired,
|
|
323
326
|
project = null, obsType = null, importance = null, branch = null,
|
|
324
|
-
epochFrom = null, epochTo = null, includeNoise = false,
|
|
327
|
+
epochFrom = null, epochTo = null, includeNoise = false, obsTypeScoped = false,
|
|
325
328
|
}) {
|
|
326
329
|
const total = isDeep
|
|
327
330
|
? results.length
|
|
@@ -332,6 +335,7 @@ export function finalizeSearchPage(db, results, {
|
|
|
332
335
|
args: { project: project || null, obs_type: obsType || null, importance: importance || null, branch: branch || null },
|
|
333
336
|
project: project || null,
|
|
334
337
|
epochFrom, epochTo, includeNoise,
|
|
338
|
+
obsTypeScoped, // D#76: count obs + type-filtered events only (skip type-less sessions/prompts)
|
|
335
339
|
}), results.length);
|
|
336
340
|
const page = results.slice(offset, offset + limit);
|
|
337
341
|
attachBodyTokens(db, page);
|
|
@@ -371,6 +375,9 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
371
375
|
limit, offset, project = null, obsType = null, importance = null, branch = null,
|
|
372
376
|
includeNoise = false, epochFrom = null, epochTo = null, sort = 'relevance', tier = null,
|
|
373
377
|
// ── surface policy (strict behavior-preservation; the two surfaces differ) ──
|
|
378
|
+
obsTypeScoped = false, // D#76: obs_type given (no branch/tier) ⇒ scope to obs+events (both carry the type
|
|
379
|
+
// vocabulary), NOT observations-only. Skips the type-less sessions/prompts legs
|
|
380
|
+
// and their counts; the events leg filters by event_type = obsType. Both surfaces.
|
|
374
381
|
obsTypeFallback = false, // A5: list-recent-by-type when 0 matches — MCP true, CLI false (#8217 removed it from CLI)
|
|
375
382
|
crossSourceEpochSortNoFts = false, // A3: epoch-sort the cross-source set when no ftsQuery — MCP true, CLI false
|
|
376
383
|
rerankPolicy = 'mcp', // A4: re-rank/supersede gate + re-sort condition — 'mcp' | 'cli'
|
|
@@ -435,7 +442,11 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
435
442
|
}
|
|
436
443
|
|
|
437
444
|
// ── Sessions (FTS via shared helper; optional recent-listing when no ftsQuery) ──
|
|
438
|
-
|
|
445
|
+
// D#74: `!isDeep || escalated` — auto-escalation replaces the obs leg with a deep fuse but must
|
|
446
|
+
// NOT silently drop the cross-source complement (it did: escalated → isDeep → every leg below
|
|
447
|
+
// skipped). Explicit deep=true (escalated stays false) still dives obs-only. D#76: obsTypeScoped
|
|
448
|
+
// skips the type-less sessions/prompts legs (they can't honor an obs_type/event_type filter).
|
|
449
|
+
if ((!effectiveSource || effectiveSource === 'sessions') && (!isDeep || escalated) && !obsTypeScoped) {
|
|
439
450
|
const pushSessions = () => {
|
|
440
451
|
if (ftsQuery) {
|
|
441
452
|
const rows = searchSessionsFts(db, { ftsQuery, project, projectBoost: project ? null : currentProject, epochFrom, epochTo, perSourceLimit, perSourceOffset });
|
|
@@ -460,7 +471,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
460
471
|
}
|
|
461
472
|
|
|
462
473
|
// ── Prompts (FTS via shared helper incl. CJK gate; optional recent-listing) ──
|
|
463
|
-
if ((!effectiveSource || effectiveSource === 'prompts') && !isDeep) {
|
|
474
|
+
if ((!effectiveSource || effectiveSource === 'prompts') && (!isDeep || escalated) && !obsTypeScoped) {
|
|
464
475
|
const pushPrompts = () => {
|
|
465
476
|
if (ftsQuery) {
|
|
466
477
|
const rows = searchPromptsFts(db, { query, ftsQuery, project, epochFrom, epochTo, perSourceLimit, perSourceOffset });
|
|
@@ -487,7 +498,9 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
487
498
|
}
|
|
488
499
|
|
|
489
500
|
// ── Events (FTS via shared helper; events are the CANONICAL event-typed store) ──
|
|
490
|
-
|
|
501
|
+
// Runs on auto-escalation too (D#74) and participates in obsTypeScoped (D#76) — events carry the
|
|
502
|
+
// same type vocabulary as observations, so an obs_type filter maps to e.event_type here.
|
|
503
|
+
if ((!effectiveSource || effectiveSource === 'events') && (!isDeep || escalated)) {
|
|
491
504
|
const toIso = (epoch) => (epoch ? new Date(epoch).toISOString() : null);
|
|
492
505
|
// events has no created_at ISO column (only *_epoch) and no subtitle; body carries the
|
|
493
506
|
// distilled lesson (persistHaikuSummary writes lesson_learned||narrative here), surfaced
|
|
@@ -500,13 +513,16 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
500
513
|
});
|
|
501
514
|
const pushEvents = () => {
|
|
502
515
|
if (ftsQuery) {
|
|
503
|
-
|
|
516
|
+
// D#76: obsType maps to e.event_type (shared vocab); importance filters events too (they carry it).
|
|
517
|
+
const rows = searchEventsFts(db, { ftsQuery, project, projectBoost: project ? null : currentProject, epochFrom, epochTo, eventType: obsType, importance, perSourceLimit, perSourceOffset });
|
|
504
518
|
for (const r of rows) results.push(shapeEvent(r));
|
|
505
519
|
} else if (recentListingNoFts && effectiveSource === 'events') {
|
|
506
520
|
const params = []; const wheres = ['superseded_at_epoch IS NULL'];
|
|
507
521
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
508
522
|
if (epochFrom !== null) { wheres.push('created_at_epoch >= ?'); params.push(epochFrom); }
|
|
509
523
|
if (epochTo !== null) { wheres.push('created_at_epoch <= ?'); params.push(epochTo); }
|
|
524
|
+
if (obsType) { wheres.push('event_type = ?'); params.push(obsType); }
|
|
525
|
+
if (importance) { wheres.push('COALESCE(importance, 1) >= ?'); params.push(importance); }
|
|
510
526
|
params.push(perSourceLimit, perSourceOffset);
|
|
511
527
|
const rows = db.prepare(`
|
|
512
528
|
SELECT id, event_type, title, body, project, importance, file_paths, created_at_epoch
|
|
@@ -581,7 +597,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
581
597
|
const preFinalizeCount = results.length;
|
|
582
598
|
const { total, page } = finalizeSearchPage(db, results, {
|
|
583
599
|
isDeep, offset, limit, effectiveSource, ftsQuery, orFallbackFired,
|
|
584
|
-
project, obsType, importance, branch, epochFrom, epochTo, includeNoise,
|
|
600
|
+
project, obsType, importance, branch, epochFrom, epochTo, includeNoise, obsTypeScoped,
|
|
585
601
|
});
|
|
586
602
|
|
|
587
603
|
return {
|
package/lib/timeline-core.mjs
CHANGED
|
@@ -53,6 +53,18 @@ export function resolveAnchorToken(db, rawAnchor, { project = null } = {}) {
|
|
|
53
53
|
return { ok: true, anchorId: nearest.id, anchorNote: `(anchored to #${nearest.id}, closest obs to ${srcPrefix}${parsed.id})` };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Events are the canonical event-typed store (E#N in mem_search output). They have no
|
|
57
|
+
// place in the obs-centric before/after window, so — like prompt/session — anchor to the
|
|
58
|
+
// nearest observation by epoch. Without this explicit branch an E#N token would fall through
|
|
59
|
+
// to the bare-int obs lookup below and silently resolve to the COLLIDING observation id.
|
|
60
|
+
if (parsed.source === 'event') {
|
|
61
|
+
const row = db.prepare('SELECT created_at_epoch FROM events WHERE id = ?').get(parsed.id);
|
|
62
|
+
if (!row) return { ok: false, error: { code: 'source-not-found', name: 'Event', prefix: 'E#', id: parsed.id } };
|
|
63
|
+
const nearest = nearestObservation(db, row.created_at_epoch, project);
|
|
64
|
+
if (!nearest) return { ok: false, error: { code: 'no-obs-near', prefix: 'E#', id: parsed.id } };
|
|
65
|
+
return { ok: true, anchorId: nearest.id, anchorNote: `(anchored to #${nearest.id}, closest obs to E#${parsed.id})` };
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
// Bare "#N" or "N" — observation first. Route compressed obs to its live
|
|
57
69
|
// parent so the window (which filters compressed) isn't shown around a dead
|
|
58
70
|
// record; negative sentinels (-1 dropped, -2 pending purge) have no parent.
|
package/mem-cli.mjs
CHANGED
|
@@ -76,7 +76,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
76
76
|
fail(`[mem] Invalid --type "${type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
-
const source = flags.source || null; // observations|sessions|prompts (null = all)
|
|
79
|
+
const source = flags.source || null; // observations|sessions|prompts|events (null = all)
|
|
80
80
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
81
81
|
const bounds = parseDateBounds(flags.from, flags.to, flags.since);
|
|
82
82
|
if (!bounds.ok) {
|
|
@@ -176,9 +176,23 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
176
176
|
if (deepMode === 'deep' && source && source !== 'observations') {
|
|
177
177
|
process.stderr.write(`[mem] Note: --deep searches observations only; ignoring --source ${source}\n`);
|
|
178
178
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
// branch/tier are obs-exclusive columns → force observations. --type (obs_type) maps to both
|
|
180
|
+
// observations.type AND events.event_type, so scope to obs+events and skip the type-less
|
|
181
|
+
// sessions/prompts legs (D#76). --importance rides the obsTypeScoped path (events carry importance).
|
|
182
|
+
let effectiveSource;
|
|
183
|
+
let obsTypeScoped = false;
|
|
184
|
+
if (deepMode === 'deep') {
|
|
185
|
+
effectiveSource = 'observations';
|
|
186
|
+
} else if (source) {
|
|
187
|
+
effectiveSource = source;
|
|
188
|
+
} else if (type && !branch && !tier) {
|
|
189
|
+
effectiveSource = null;
|
|
190
|
+
obsTypeScoped = true;
|
|
191
|
+
} else if (minImportance || branch || tier) {
|
|
192
|
+
effectiveSource = 'observations';
|
|
193
|
+
} else {
|
|
194
|
+
effectiveSource = null;
|
|
195
|
+
}
|
|
182
196
|
|
|
183
197
|
const res = await coreRunSearchPipeline(
|
|
184
198
|
{
|
|
@@ -191,6 +205,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
191
205
|
limit, offset, project: project || null, obsType: type, importance: minImportance,
|
|
192
206
|
branch, includeNoise, epochFrom: dateFrom, epochTo: dateTo, sort, tier,
|
|
193
207
|
// ── CLI surface policy ──
|
|
208
|
+
obsTypeScoped, // D#76: obs_type ⇒ obs+events (skip type-less sessions/prompts)
|
|
194
209
|
obsTypeFallback: false, // #8217 removed list-by-type fallback from the CLI
|
|
195
210
|
crossSourceEpochSortNoFts: false, // CLI never reaches cross-source with empty ftsQuery (fails earlier)
|
|
196
211
|
rerankPolicy: 'cli', // re-rank/supersede on any obs; re-sort gated on cross-source
|
|
@@ -532,12 +547,31 @@ function renderPromptRows(db, ids) {
|
|
|
532
547
|
return { text: parts.join('\n\n'), count: rows.length };
|
|
533
548
|
}
|
|
534
549
|
|
|
550
|
+
function renderEventRows(db, ids) {
|
|
551
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
552
|
+
const rows = db.prepare(`SELECT * FROM events WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`).all(...ids);
|
|
553
|
+
if (rows.length === 0) return null;
|
|
554
|
+
const parts = [];
|
|
555
|
+
for (const r of rows) {
|
|
556
|
+
// events store the distilled lesson in `body`; only *_epoch is available for the date.
|
|
557
|
+
const lines = [`E#${r.id} [${r.event_type}] ${r.created_at_epoch ? fmtDateShort(new Date(r.created_at_epoch).toISOString()) : ''}`];
|
|
558
|
+
if (r.title) lines.push(`Title: ${r.title}`);
|
|
559
|
+
if (r.body) lines.push(`Body: ${r.body}`);
|
|
560
|
+
if (r.project) lines.push(`Project: ${r.project}`);
|
|
561
|
+
if (r.importance !== null && r.importance !== undefined) lines.push(`Importance: ${r.importance}`);
|
|
562
|
+
if (r.file_paths) lines.push(`Files: ${r.file_paths}`);
|
|
563
|
+
if (r.git_sha) lines.push(`Git: ${r.git_sha}`);
|
|
564
|
+
parts.push(lines.join('\n'));
|
|
565
|
+
}
|
|
566
|
+
return { text: parts.join('\n\n'), count: rows.length };
|
|
567
|
+
}
|
|
568
|
+
|
|
535
569
|
function cmdGet(db, args) {
|
|
536
570
|
const { positional, flags } = parseArgs(args);
|
|
537
571
|
const idStr = positional.join(',');
|
|
538
572
|
if (!idStr) {
|
|
539
|
-
fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt] [--fields f1,f2,...]\n' +
|
|
540
|
-
' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session).');
|
|
573
|
+
fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt|event] [--fields f1,f2,...]\n' +
|
|
574
|
+
' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session), E#123 (event).');
|
|
541
575
|
return;
|
|
542
576
|
}
|
|
543
577
|
|
|
@@ -545,18 +579,18 @@ function cmdGet(db, args) {
|
|
|
545
579
|
|
|
546
580
|
// Explicit --source overrides any prefix; otherwise each token's prefix routes individually.
|
|
547
581
|
const explicit = flags.source;
|
|
548
|
-
const validSources = new Set(['obs', 'session', 'prompt']);
|
|
582
|
+
const validSources = new Set(['obs', 'session', 'prompt', 'event']);
|
|
549
583
|
if (explicit && !validSources.has(explicit)) {
|
|
550
|
-
fail(`[mem] Invalid --source "${explicit}". Use: obs, session, prompt`);
|
|
584
|
+
fail(`[mem] Invalid --source "${explicit}". Use: obs, session, prompt, event`);
|
|
551
585
|
return;
|
|
552
586
|
}
|
|
553
587
|
|
|
554
|
-
// Shared bucketing with MCP mem_get — single source of truth for P#/S#/# routing (#8050).
|
|
588
|
+
// Shared bucketing with MCP mem_get — single source of truth for P#/S#/E#/# routing (#8050).
|
|
555
589
|
const { bySrc, invalid: unparseable } = bucketIdTokens(tokens, { explicit, defaultSource: 'obs' });
|
|
556
590
|
if (unparseable.length > 0) {
|
|
557
591
|
process.stderr.write(`[mem] Ignoring unparseable ID token(s): ${unparseable.join(', ')}\n`);
|
|
558
592
|
}
|
|
559
|
-
if (bySrc.obs.length + bySrc.session.length + bySrc.prompt.length === 0) {
|
|
593
|
+
if (bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length === 0) {
|
|
560
594
|
fail('[mem] No valid IDs provided');
|
|
561
595
|
return;
|
|
562
596
|
}
|
|
@@ -591,11 +625,15 @@ function cmdGet(db, args) {
|
|
|
591
625
|
const s = renderPromptRows(db, bySrc.prompt);
|
|
592
626
|
if (s) { sections.push(s.text); totalFound += s.count; }
|
|
593
627
|
}
|
|
628
|
+
if (bySrc.event.length > 0) {
|
|
629
|
+
const s = renderEventRows(db, bySrc.event);
|
|
630
|
+
if (s) { sections.push(s.text); totalFound += s.count; }
|
|
631
|
+
}
|
|
594
632
|
|
|
595
633
|
if (totalFound === 0) {
|
|
596
634
|
// Probe the OTHER sources so the caller can retry with the right prefix.
|
|
597
635
|
const queried = new Set(Object.entries(bySrc).filter(([, v]) => v.length > 0).map(([k]) => k));
|
|
598
|
-
const allIds = [...bySrc.obs, ...bySrc.session, ...bySrc.prompt];
|
|
636
|
+
const allIds = [...bySrc.obs, ...bySrc.session, ...bySrc.prompt, ...bySrc.event];
|
|
599
637
|
const probe = probeIdSources(db, allIds, queried);
|
|
600
638
|
const hits = formatProbeHints(probe);
|
|
601
639
|
const hint = hits.length > 0 ? ` Try: ${hits.join('; ')}.` : '';
|
|
@@ -1437,10 +1475,10 @@ function cmdDelete(db, args) {
|
|
|
1437
1475
|
// delete operates on observations only. Reject P#/S# explicitly so callers aren't
|
|
1438
1476
|
// surprised by silent NaN filtering when they paste search-output IDs.
|
|
1439
1477
|
const tokens = idStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1440
|
-
const nonObs = tokens.filter(t => /^[
|
|
1478
|
+
const nonObs = tokens.filter(t => /^[EePpSs]#?\d+$/.test(t));
|
|
1441
1479
|
if (nonObs.length > 0) {
|
|
1442
1480
|
fail(`[mem] delete only works on observations. Rejected: ${nonObs.join(', ')}. ` +
|
|
1443
|
-
`Prompts and
|
|
1481
|
+
`Prompts, sessions, and events are not deletable here — inspect with \`claude-mem-lite get P#N --source prompt\` / \`--source session\` / \`--source event\`.`);
|
|
1444
1482
|
return;
|
|
1445
1483
|
}
|
|
1446
1484
|
const ids = tokens.map(t => {
|
|
@@ -1514,9 +1552,9 @@ function cmdDelete(db, args) {
|
|
|
1514
1552
|
function cmdUpdate(db, args) {
|
|
1515
1553
|
const { positional, flags } = parseArgs(args);
|
|
1516
1554
|
const raw = positional[0];
|
|
1517
|
-
if (raw && /^[
|
|
1555
|
+
if (raw && /^[EePpSs]#?\d+$/.test(String(raw).trim())) {
|
|
1518
1556
|
fail(`[mem] update only works on observations. Rejected: ${raw}. ` +
|
|
1519
|
-
`Prompts and
|
|
1557
|
+
`Prompts, sessions, and events are not editable here.`);
|
|
1520
1558
|
return;
|
|
1521
1559
|
}
|
|
1522
1560
|
// Strict parseIdToken gate (aligned with cmdDelete): a bare parseInt fallback
|
|
@@ -2782,11 +2820,14 @@ Commands:
|
|
|
2782
2820
|
--files (plural, comma-split) preferred; --file (singular) kept for back-compat.
|
|
2783
2821
|
Use /lesson or /bug slash commands for faster capture (T8).
|
|
2784
2822
|
|
|
2785
|
-
adopt
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
--
|
|
2823
|
+
adopt Write the claude-mem-lite managed block into this project's
|
|
2824
|
+
CLAUDE.md + a plugin_claude_mem_lite.md detail doc under
|
|
2825
|
+
.claude/ (loaded as project instructions). Runs automatically
|
|
2826
|
+
on each SessionStart; use this to force it now.
|
|
2827
|
+
--all Legacy sweep: strip old memory-dir (MEMORY.md) sentinels from
|
|
2828
|
+
known projects. Does NOT adopt — CLAUDE.md adoption is
|
|
2829
|
+
per-project, on each project's next SessionStart.
|
|
2830
|
+
--force Overwrite a manually-edited managed block
|
|
2790
2831
|
--dry-run Print intended writes without touching disk
|
|
2791
2832
|
--status List adopted projects + version
|
|
2792
2833
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.46.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-engine.mjs
CHANGED
|
@@ -167,7 +167,7 @@ export function countPromptFtsMatches(db, { ftsQuery, project = null, epochFrom
|
|
|
167
167
|
} catch { return 0; }
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
export function countEventFtsMatches(db, { ftsQuery, project = null, epochFrom = null, epochTo = null }) {
|
|
170
|
+
export function countEventFtsMatches(db, { ftsQuery, project = null, epochFrom = null, epochTo = null, eventType = null, importance = null }) {
|
|
171
171
|
if (!ftsQuery) return 0;
|
|
172
172
|
try {
|
|
173
173
|
const wheres = ['events_fts MATCH ?', 'e.superseded_at_epoch IS NULL'];
|
|
@@ -175,6 +175,10 @@ export function countEventFtsMatches(db, { ftsQuery, project = null, epochFrom =
|
|
|
175
175
|
if (project) { wheres.push('e.project = ?'); params.push(project); }
|
|
176
176
|
if (epochFrom) { wheres.push('e.created_at_epoch >= ?'); params.push(epochFrom); }
|
|
177
177
|
if (epochTo) { wheres.push('e.created_at_epoch <= ?'); params.push(epochTo); }
|
|
178
|
+
// D#76: keep the count in lockstep with searchEventsFts's event_type/importance filters,
|
|
179
|
+
// else the "N of M" population diverges from the rows actually shown.
|
|
180
|
+
if (eventType) { wheres.push('e.event_type = ?'); params.push(eventType); }
|
|
181
|
+
if (importance) { wheres.push('COALESCE(e.importance, 1) >= ?'); params.push(importance); }
|
|
178
182
|
const row = db.prepare(`
|
|
179
183
|
SELECT COUNT(*) as c
|
|
180
184
|
FROM events_fts
|
|
@@ -194,19 +198,22 @@ export function countEventFtsMatches(db, { ftsQuery, project = null, epochFrom =
|
|
|
194
198
|
export function countSearchTotal(db, {
|
|
195
199
|
effectiveSource = null, ftsQuery, obsFtsQuery = null,
|
|
196
200
|
args = {}, project = null, epochFrom = null, epochTo = null, includeNoise = false,
|
|
201
|
+
obsTypeScoped = false,
|
|
197
202
|
}) {
|
|
198
203
|
let total = 0;
|
|
199
204
|
if (!effectiveSource || effectiveSource === 'observations') {
|
|
200
205
|
total += countObsFtsMatches(db, { ftsQuery: obsFtsQuery || ftsQuery, args, epochFrom, epochTo, includeNoise });
|
|
201
206
|
}
|
|
202
|
-
|
|
207
|
+
// D#76: obsTypeScoped (obs_type given, no branch/tier) counts obs + type-filtered events only —
|
|
208
|
+
// sessions/prompts have no type column, so they are excluded from both the results and the count.
|
|
209
|
+
if (!obsTypeScoped && (!effectiveSource || effectiveSource === 'sessions')) {
|
|
203
210
|
total += countSessionFtsMatches(db, { ftsQuery, project, epochFrom, epochTo });
|
|
204
211
|
}
|
|
205
|
-
if (!effectiveSource || effectiveSource === 'prompts') {
|
|
212
|
+
if (!obsTypeScoped && (!effectiveSource || effectiveSource === 'prompts')) {
|
|
206
213
|
total += countPromptFtsMatches(db, { ftsQuery, project, epochFrom, epochTo });
|
|
207
214
|
}
|
|
208
215
|
if (!effectiveSource || effectiveSource === 'events') {
|
|
209
|
-
total += countEventFtsMatches(db, { ftsQuery, project, epochFrom, epochTo });
|
|
216
|
+
total += countEventFtsMatches(db, { ftsQuery, project, epochFrom, epochTo, eventType: args.obs_type || null, importance: args.importance || null });
|
|
210
217
|
}
|
|
211
218
|
return total;
|
|
212
219
|
}
|
package/server.mjs
CHANGED
|
@@ -289,15 +289,26 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
289
289
|
return { ...formatSearchOutput([], args, ftsQuery, 0), escalated: false, results: [], total: 0, variants: null };
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//
|
|
293
|
-
// (
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
292
|
+
// Source scoping. deep is observations-only (deepSearch fuses hybrid-obs lists). branch/tier are
|
|
293
|
+
// obs-EXCLUSIVE columns (sessions/prompts/events lack them) → force observations, else those legs
|
|
294
|
+
// return UNFILTERED and leak rows that can't be scoped (the pre-fix cross-source branch/tier leak).
|
|
295
|
+
// obs_type is DIFFERENT: events carry the same type vocabulary (events.event_type), so an obs_type
|
|
296
|
+
// filter maps to obs + events both — scope to those two and skip the type-less sessions/prompts
|
|
297
|
+
// legs (D#76). importance filters obs and events (both carry it), so it rides the obsTypeScoped path.
|
|
298
|
+
let effectiveType;
|
|
299
|
+
let obsTypeScoped = false;
|
|
300
|
+
if (deepMode === 'deep') {
|
|
301
|
+
effectiveType = 'observations';
|
|
302
|
+
} else if (args.type) {
|
|
303
|
+
effectiveType = args.type;
|
|
304
|
+
} else if (args.obs_type && !args.branch && !args.tier) {
|
|
305
|
+
effectiveType = undefined; // cross-source gate open; obsTypeScoped narrows it to obs+events
|
|
306
|
+
obsTypeScoped = true;
|
|
307
|
+
} else if (args.importance || args.branch || args.tier) {
|
|
308
|
+
effectiveType = 'observations';
|
|
309
|
+
} else {
|
|
310
|
+
effectiveType = undefined;
|
|
311
|
+
}
|
|
301
312
|
|
|
302
313
|
const r = await coreRunSearchPipeline(
|
|
303
314
|
{
|
|
@@ -312,6 +323,7 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
312
323
|
includeNoise: args.include_noise === true, epochFrom, epochTo,
|
|
313
324
|
sort: args.sort || 'relevance', tier: args.tier ?? null,
|
|
314
325
|
// ── MCP surface policy ──
|
|
326
|
+
obsTypeScoped, // D#76: obs_type ⇒ obs+events (skip type-less sessions/prompts)
|
|
315
327
|
obsTypeFallback: true, // list-recent-by-type when 0 matches
|
|
316
328
|
crossSourceEpochSortNoFts: true, // epoch-sort cross-source with no ftsQuery
|
|
317
329
|
rerankPolicy: 'mcp', // (ftsQuery||isDeep) gate; re-rank/re-sort on ftsQuery&&!reranked
|
|
@@ -497,9 +509,9 @@ server.registerTool(
|
|
|
497
509
|
const { bySrc, invalid } = bucketIdTokens(args.ids, { explicit: args.source || null, defaultSource: 'obs' });
|
|
498
510
|
if (invalid.length > 0) {
|
|
499
511
|
// Should not happen — schema regex already rejected bad tokens — but guard defensively.
|
|
500
|
-
return { content: [{ type: 'text', text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, or
|
|
512
|
+
return { content: [{ type: 'text', text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, S#N, or E#N.` }] };
|
|
501
513
|
}
|
|
502
|
-
const totalRequested = bySrc.obs.length + bySrc.session.length + bySrc.prompt.length;
|
|
514
|
+
const totalRequested = bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length;
|
|
503
515
|
if (totalRequested === 0) {
|
|
504
516
|
return { content: [{ type: 'text', text: 'No valid IDs provided.' }] };
|
|
505
517
|
}
|
|
@@ -524,7 +536,7 @@ server.registerTool(
|
|
|
524
536
|
|
|
525
537
|
// Per-source fetchers — each returns { rows, foundIds:Set, prefix }.
|
|
526
538
|
const sections = [];
|
|
527
|
-
const foundBySource = { obs: new Set(), session: new Set(), prompt: new Set() };
|
|
539
|
+
const foundBySource = { obs: new Set(), session: new Set(), prompt: new Set(), event: new Set() };
|
|
528
540
|
|
|
529
541
|
if (bySrc.obs.length > 0) {
|
|
530
542
|
const ph = bySrc.obs.map(() => '?').join(',');
|
|
@@ -583,17 +595,37 @@ server.registerTool(
|
|
|
583
595
|
}
|
|
584
596
|
}
|
|
585
597
|
|
|
586
|
-
|
|
598
|
+
if (bySrc.event.length > 0) {
|
|
599
|
+
const ph = bySrc.event.map(() => '?').join(',');
|
|
600
|
+
const rows = db.prepare(`SELECT * FROM events WHERE id IN (${ph}) ORDER BY created_at_epoch ASC`).all(...bySrc.event);
|
|
601
|
+
// events carry the distilled lesson in `body` (persistHaikuSummary writes lesson_learned||narrative).
|
|
602
|
+
// No created_at ISO column (only *_epoch) — render the epoch as an ISO string for a scannable date.
|
|
603
|
+
for (const row of rows) {
|
|
604
|
+
foundBySource.event.add(row.id);
|
|
605
|
+
const lines = [`── E#${row.id} [${row.event_type}] ──`];
|
|
606
|
+
if (row.title) lines.push(`title: ${row.title}`);
|
|
607
|
+
if (row.body) lines.push(`body: ${row.body.length > 500 ? row.body.slice(0, 500) + '…' : row.body}`);
|
|
608
|
+
if (row.project) lines.push(`project: ${row.project}`);
|
|
609
|
+
if (row.importance !== null && row.importance !== undefined) lines.push(`importance: ${row.importance}`);
|
|
610
|
+
if (row.file_paths) lines.push(`file_paths: ${row.file_paths}`);
|
|
611
|
+
if (row.git_sha) lines.push(`git_sha: ${row.git_sha}`);
|
|
612
|
+
if (row.created_at_epoch) lines.push(`created_at: ${new Date(row.created_at_epoch).toISOString()}`);
|
|
613
|
+
sections.push(lines.join('\n'));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const totalFound = foundBySource.obs.size + foundBySource.session.size + foundBySource.prompt.size + foundBySource.event.size;
|
|
587
618
|
|
|
588
619
|
if (totalFound === 0) {
|
|
589
620
|
// Probe other sources so callers can retry with the right prefix/source override.
|
|
590
621
|
const queried = new Set(Object.entries(bySrc).filter(([, v]) => v.length > 0).map(([k]) => k));
|
|
591
|
-
const allNumericIds = [...bySrc.obs, ...bySrc.session, ...bySrc.prompt];
|
|
622
|
+
const allNumericIds = [...bySrc.obs, ...bySrc.session, ...bySrc.prompt, ...bySrc.event];
|
|
592
623
|
const probe = probeIdSources(db, allNumericIds, queried);
|
|
593
624
|
const hints = [];
|
|
594
625
|
if (probe.obs.length > 0) hints.push(`#${probe.obs.join(', #')} (obs — use source='obs' or bare #N)`);
|
|
595
626
|
if (probe.session.length > 0) hints.push(`S#${probe.session.join(', S#')} (session — use source='session' or S#N)`);
|
|
596
627
|
if (probe.prompt.length > 0) hints.push(`P#${probe.prompt.join(', P#')} (prompt — use source='prompt' or P#N)`);
|
|
628
|
+
if (probe.event.length > 0) hints.push(`E#${probe.event.join(', E#')} (event — use source='event' or E#N)`);
|
|
597
629
|
const hint = hints.length > 0 ? ` Try: ${hints.join('; ')}.` : '';
|
|
598
630
|
const queriedList = [...queried].join(', ');
|
|
599
631
|
const msg = `No records found in source(s) [${queriedList}] for the given ID(s).${hint}`;
|
|
@@ -607,6 +639,7 @@ server.registerTool(
|
|
|
607
639
|
missingHints.push(...miss(bySrc.obs, foundBySource.obs, '#'));
|
|
608
640
|
missingHints.push(...miss(bySrc.session, foundBySource.session, 'S#'));
|
|
609
641
|
missingHints.push(...miss(bySrc.prompt, foundBySource.prompt, 'P#'));
|
|
642
|
+
missingHints.push(...miss(bySrc.event, foundBySource.event, 'E#'));
|
|
610
643
|
|
|
611
644
|
const parts = [];
|
|
612
645
|
if (fieldsNote) parts.push(fieldsNote);
|
package/tool-schemas.mjs
CHANGED
|
@@ -75,7 +75,7 @@ const coerceMixedIdTokens = z.preprocess(
|
|
|
75
75
|
}
|
|
76
76
|
return v;
|
|
77
77
|
},
|
|
78
|
-
z.array(z.string().regex(/^[
|
|
78
|
+
z.array(z.string().regex(/^[EePpSs]?#?\d+$/, 'Expected N, #N, P#N, S#N, or E#N')).min(1).max(20)
|
|
79
79
|
);
|
|
80
80
|
|
|
81
81
|
export const memSearchSchema = {
|
|
@@ -120,12 +120,12 @@ const coerceAnchor = z.preprocess(
|
|
|
120
120
|
},
|
|
121
121
|
z.union([
|
|
122
122
|
z.number().int(),
|
|
123
|
-
z.string().regex(/^[
|
|
123
|
+
z.string().regex(/^[EePpSs]?#?\d+$/, 'Expected N, #N, P#N, S#N, or E#N'),
|
|
124
124
|
])
|
|
125
125
|
);
|
|
126
126
|
|
|
127
127
|
export const memTimelineSchema = {
|
|
128
|
-
anchor: coerceAnchor.optional().describe('Anchor as observation ID (int) or prefixed token string: "#123", "P#123" (prompt → nearest obs), "S#123" (session → nearest obs). Takes precedence over query.'),
|
|
128
|
+
anchor: coerceAnchor.optional().describe('Anchor as observation ID (int) or prefixed token string: "#123", "P#123" (prompt → nearest obs), "S#123" (session → nearest obs), "E#123" (event → nearest obs). Takes precedence over query.'),
|
|
129
129
|
query: z.string().optional().describe('FTS5 query to auto-find anchor. Ignored when anchor is also given; use one or the other.'),
|
|
130
130
|
before: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items before anchor (default 5)'),
|
|
131
131
|
after: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items after anchor (default 5)'),
|
|
@@ -136,8 +136,8 @@ export const memGetSchema = {
|
|
|
136
136
|
// Accepts mixed tokens so pasted search results work verbatim: [1], [1, "P#2"], "1,P#2,S#3",
|
|
137
137
|
// or the JSON-stringified form ["1","P#2"]. Each token's prefix routes to its source bucket
|
|
138
138
|
// in server.mjs via lib/id-routing.bucketIdTokens. An explicit `source` override still wins.
|
|
139
|
-
ids: coerceMixedIdTokens.describe('Mixed observation/prompt/session IDs — accepts N, #N, P#N, S#N; comma-strings and JSON arrays also coerced'),
|
|
140
|
-
source: z.enum(['obs', 'session', 'prompt']).optional().describe('Force all IDs to this source (overrides per-token prefixes). Omit to let P#/S#/# prefixes route individually.'),
|
|
139
|
+
ids: coerceMixedIdTokens.describe('Mixed observation/prompt/session/event IDs — accepts N, #N, P#N, S#N, E#N; comma-strings and JSON arrays also coerced'),
|
|
140
|
+
source: z.enum(['obs', 'session', 'prompt', 'event']).optional().describe('Force all IDs to this source (overrides per-token prefixes). Omit to let P#/S#/E#/# prefixes route individually.'),
|
|
141
141
|
fields: coerceStringArray.optional().describe('Specific fields to return (default: all; validated against obs schema — session/prompt sources ignore this filter)'),
|
|
142
142
|
};
|
|
143
143
|
|
|
@@ -187,7 +187,7 @@ export const memSaveSchema = {
|
|
|
187
187
|
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|
|
188
188
|
importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
|
|
189
189
|
files: coerceStringArray.optional().describe('File paths associated with this observation'),
|
|
190
|
-
lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway (for bugfix: root cause & fix; for decision: rationale)'),
|
|
190
|
+
lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway, ≤500 chars (for bugfix: root cause & fix; for decision: rationale)'),
|
|
191
191
|
closes_deferred: coerceDeferredTokens.optional().describe('Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert — a single invalid id rolls back the whole save.'),
|
|
192
192
|
supersedes: coerceSupersedes.optional().describe('Observation ids (same project) that this save overturns. They are marked superseded — dropped from live search — and linked to the new row (superseded_by). Use ONLY when this genuinely replaces a prior conclusion; do NOT use for merely-related or updated-but-still-valid memories.'),
|
|
193
193
|
};
|
|
@@ -355,7 +355,8 @@ export const tools = [
|
|
|
355
355
|
{
|
|
356
356
|
name: 'mem_search',
|
|
357
357
|
description:
|
|
358
|
-
'Full-text search across observations, sessions, and
|
|
358
|
+
'Full-text search across observations, sessions, prompts, and events — the canonical\n' +
|
|
359
|
+
'bugfix/feature/decision/lesson history (FTS5, BM25-ranked).\n' +
|
|
359
360
|
'\n' +
|
|
360
361
|
'DO NOT use when:\n' +
|
|
361
362
|
' - The SessionStart context or hook-injected memory already shows #NN entries that answer the question\n' +
|
|
@@ -363,9 +364,8 @@ export const tools = [
|
|
|
363
364
|
' - You want everything about a specific file (use mem_recall — cheaper and file-scoped)\n' +
|
|
364
365
|
'\n' +
|
|
365
366
|
'USE when:\n' +
|
|
366
|
-
' - Investigating
|
|
367
|
+
' - Investigating an error keyword with obs_type="bugfix" (matches observations AND events)\n' +
|
|
367
368
|
' - Looking for prior art on a module/feature before refactoring\n' +
|
|
368
|
-
' - User asks "have we seen this before" or references something not in visible context\n' +
|
|
369
369
|
' - A normal search missed — weak results auto-escalate to deep (set deep=false to opt out)\n' +
|
|
370
370
|
'\n' +
|
|
371
371
|
'Equivalent CLI: ' + CLI_INVOKE + ' search "<query>" [--type bugfix] [--deep]',
|