claude-mem-lite 3.48.0 → 3.50.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/hook-llm.mjs +2 -1
- package/hook-optimize.mjs +27 -13
- package/lib/activity.mjs +2 -1
- package/lib/deferred-work.mjs +137 -0
- package/lib/events-injection.mjs +12 -3
- package/lib/export-columns.mjs +4 -1
- package/lib/id-routing.mjs +26 -0
- package/lib/obs-types.mjs +12 -0
- package/lib/save-nudge.mjs +22 -0
- package/lib/search-core.mjs +41 -15
- package/lib/stats-core.mjs +113 -0
- package/mem-cli.mjs +72 -117
- package/package.json +4 -1
- package/registry.mjs +1 -1
- package/schema.mjs +1 -1
- package/scripts/prompt-search-utils.mjs +27 -0
- package/scripts/user-prompt-search.js +72 -13
- package/server.mjs +66 -102
- package/source-files.mjs +13 -0
- package/tool-schemas.mjs +9 -5
package/server.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import { reRankWithContext, autoBoostIfNeeded, runIdleCleanup, buildServerInstru
|
|
|
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';
|
|
15
|
-
import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
16
15
|
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
17
16
|
import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
18
17
|
import {
|
|
@@ -26,6 +25,8 @@ import { snapshotDb } from './lib/db-backup.mjs';
|
|
|
26
25
|
import { deleteObservations } from './lib/delete-core.mjs';
|
|
27
26
|
import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
|
|
28
27
|
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
28
|
+
import { computeStatsFeed } from './lib/stats-core.mjs';
|
|
29
|
+
import { buildLessonNudge } from './lib/save-nudge.mjs';
|
|
29
30
|
import { formatObsFieldValue } from './cli/common.mjs';
|
|
30
31
|
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
31
32
|
|
|
@@ -42,16 +43,17 @@ import { join, sep } from 'path';
|
|
|
42
43
|
import { homedir } from 'os';
|
|
43
44
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
44
45
|
import { searchResources } from './registry-retriever.mjs';
|
|
45
|
-
import { probeOtherSources as probeIdSources, bucketIdTokens } from './lib/id-routing.mjs';
|
|
46
|
+
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
46
47
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
47
48
|
import { rebuildObservationDerived } from './lib/observation-write.mjs';
|
|
48
49
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
49
|
-
import { computeNoiseGauge } from './lib/stats-quality.mjs';
|
|
50
50
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
51
51
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
52
52
|
import {
|
|
53
53
|
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
54
54
|
resolveDeferredIds, closeDeferredItems,
|
|
55
|
+
getDeferredByIds, formatDeferredDetail,
|
|
56
|
+
searchDeferredWork, formatDeferredSearchTrailer,
|
|
55
57
|
} from './lib/deferred-work.mjs';
|
|
56
58
|
import { _resetVocabCache } from './tfidf.mjs';
|
|
57
59
|
import { createRequire } from 'module';
|
|
@@ -283,11 +285,30 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
283
285
|
const deepMode = resolveDeepMode(args.deep, { surface: 'mcp' });
|
|
284
286
|
const rerank = args.rerank === true && deepMode === 'deep';
|
|
285
287
|
|
|
288
|
+
// P2: deferred trailer — open deferred items matching the query, appended to
|
|
289
|
+
// the text blob AFTER (and never counted in) the main results/total. Parity
|
|
290
|
+
// with CLI cmdSearch's emitDeferredTrailer; unfiltered first-page searches
|
|
291
|
+
// only. Structured fields (results/total) stay untouched — the trailer is a
|
|
292
|
+
// text affordance, not a result source.
|
|
293
|
+
const wantDeferredTrailer = !args.type && !args.obs_type && !args.branch && !args.tier && !args.importance && offset === 0;
|
|
294
|
+
const appendDeferredTrailer = (result) => {
|
|
295
|
+
if (!wantDeferredTrailer) return result;
|
|
296
|
+
try {
|
|
297
|
+
const rows = searchDeferredWork(db, args.query || '', args.project || currentProject);
|
|
298
|
+
const lines = formatDeferredSearchTrailer(rows, 'mem_get ids=["D#<id>"]');
|
|
299
|
+
if (lines.length > 0 && result.content?.[0]?.type === 'text') {
|
|
300
|
+
result.content[0].text += `\n\n${lines.join('\n')}`;
|
|
301
|
+
}
|
|
302
|
+
} catch { /* trailer is best-effort; never break search */ }
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
|
|
286
306
|
// Early return when query was provided but sanitized to nothing (all FTS5
|
|
287
307
|
// keywords/special chars). Skipped for deep/auto (the LLM rewrite may still
|
|
288
308
|
// produce variants) and for filter-only listings (date/obs_type/importance).
|
|
309
|
+
// A pure "D#92" query lands here — the trailer still reaches the item.
|
|
289
310
|
if (args.query && !ftsQuery && !epochFrom && !epochTo && !args.obs_type && !args.importance && deepMode === 'normal') {
|
|
290
|
-
return { ...formatSearchOutput([], args, ftsQuery, 0), escalated: false, results: [], total: 0, variants: null };
|
|
311
|
+
return { ...appendDeferredTrailer(formatSearchOutput([], args, ftsQuery, 0)), escalated: false, results: [], total: 0, variants: null };
|
|
291
312
|
}
|
|
292
313
|
|
|
293
314
|
// Source scoping. deep is observations-only (deepSearch fuses hybrid-obs lists). branch/tier are
|
|
@@ -349,6 +370,7 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
349
370
|
if (r.reranked && output.content?.[0]?.type === 'text') {
|
|
350
371
|
output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
|
|
351
372
|
}
|
|
373
|
+
appendDeferredTrailer(output);
|
|
352
374
|
|
|
353
375
|
// Expose structured fields for tests + the MCP content blob.
|
|
354
376
|
return { ...output, results: r.page, total: r.total, escalated: r.escalated, variants: r.variants, reranked: r.reranked };
|
|
@@ -505,14 +527,18 @@ server.registerTool(
|
|
|
505
527
|
inputSchema: memGetSchema,
|
|
506
528
|
},
|
|
507
529
|
safeHandler(async (args) => {
|
|
530
|
+
// D#N deferred tokens are peeled off BEFORE bucketing/source-forcing —
|
|
531
|
+
// get-only read surface into deferred_work (parity with CLI cmdGet; the
|
|
532
|
+
// fetch+render live in lib/deferred-work.mjs so the twins cannot drift).
|
|
533
|
+
const { deferredIds, rest } = splitDeferredTokens(args.ids);
|
|
508
534
|
// Bucket by per-token prefix (or force all to `args.source` when explicit).
|
|
509
535
|
// coerceMixedIdTokens has already stringified + regex-validated each token.
|
|
510
|
-
const { bySrc, invalid } = bucketIdTokens(
|
|
536
|
+
const { bySrc, invalid } = bucketIdTokens(rest, { explicit: args.source || null, defaultSource: 'obs' });
|
|
511
537
|
if (invalid.length > 0) {
|
|
512
538
|
// Should not happen — schema regex already rejected bad tokens — but guard defensively.
|
|
513
|
-
return { content: [{ type: 'text', text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, S#N, or
|
|
539
|
+
return { content: [{ type: 'text', text: `Invalid ID token(s): ${invalid.join(', ')}. Expected N, #N, P#N, S#N, E#N, or D#N.` }] };
|
|
514
540
|
}
|
|
515
|
-
const totalRequested = bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length;
|
|
541
|
+
const totalRequested = bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length + deferredIds.length;
|
|
516
542
|
if (totalRequested === 0) {
|
|
517
543
|
return { content: [{ type: 'text', text: 'No valid IDs provided.' }] };
|
|
518
544
|
}
|
|
@@ -615,7 +641,26 @@ server.registerTool(
|
|
|
615
641
|
}
|
|
616
642
|
}
|
|
617
643
|
|
|
618
|
-
|
|
644
|
+
// Deferred sections — prepended below (explicit D# requests are rare; the
|
|
645
|
+
// FULL untruncated detail is the point of this surface).
|
|
646
|
+
let deferredSections = [];
|
|
647
|
+
let deferredFound = 0;
|
|
648
|
+
let deferredMissing = [];
|
|
649
|
+
if (deferredIds.length > 0) {
|
|
650
|
+
const dRows = getDeferredByIds(db, deferredIds);
|
|
651
|
+
const found = new Set(dRows.map(r => r.id));
|
|
652
|
+
deferredMissing = deferredIds.filter(id => !found.has(id));
|
|
653
|
+
deferredSections = dRows.map(formatDeferredDetail);
|
|
654
|
+
deferredFound = dRows.length;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const totalFound = foundBySource.obs.size + foundBySource.session.size + foundBySource.prompt.size + foundBySource.event.size + deferredFound;
|
|
658
|
+
|
|
659
|
+
if (totalFound === 0 && deferredIds.length > 0 && bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length === 0) {
|
|
660
|
+
// Deferred-only request, nothing found — the source-probe below is about
|
|
661
|
+
// obs/session/prompt/event and would render an empty source list.
|
|
662
|
+
return { content: [{ type: 'text', text: `Deferred item(s) not found: ${deferredMissing.map(i => `D#${i}`).join(', ')}. List open items: mem_defer_list.` }] };
|
|
663
|
+
}
|
|
619
664
|
|
|
620
665
|
if (totalFound === 0) {
|
|
621
666
|
// Probe other sources so callers can retry with the right prefix/source override.
|
|
@@ -629,7 +674,8 @@ server.registerTool(
|
|
|
629
674
|
if (probe.event.length > 0) hints.push(`E#${probe.event.join(', E#')} (event — use source='event' or E#N)`);
|
|
630
675
|
const hint = hints.length > 0 ? ` Try: ${hints.join('; ')}.` : '';
|
|
631
676
|
const queriedList = [...queried].join(', ');
|
|
632
|
-
const
|
|
677
|
+
const deferredNote = deferredMissing.length > 0 ? ` Deferred item(s) not found: ${deferredMissing.map(i => `D#${i}`).join(', ')}.` : '';
|
|
678
|
+
const msg = `No records found in source(s) [${queriedList}] for the given ID(s).${deferredNote}${hint}`;
|
|
633
679
|
return { content: [{ type: 'text', text: fieldsNote ? `${msg}\n\n${fieldsNote}` : msg }] };
|
|
634
680
|
}
|
|
635
681
|
|
|
@@ -641,10 +687,11 @@ server.registerTool(
|
|
|
641
687
|
missingHints.push(...miss(bySrc.session, foundBySource.session, 'S#'));
|
|
642
688
|
missingHints.push(...miss(bySrc.prompt, foundBySource.prompt, 'P#'));
|
|
643
689
|
missingHints.push(...miss(bySrc.event, foundBySource.event, 'E#'));
|
|
690
|
+
missingHints.push(...deferredMissing.map(id => `D#${id}`));
|
|
644
691
|
|
|
645
692
|
const parts = [];
|
|
646
693
|
if (fieldsNote) parts.push(fieldsNote);
|
|
647
|
-
parts.push(...sections);
|
|
694
|
+
parts.push(...deferredSections, ...sections);
|
|
648
695
|
if (missingHints.length > 0) {
|
|
649
696
|
parts.push(`Note: ID(s) ${missingHints.join(', ')} not found.`);
|
|
650
697
|
}
|
|
@@ -750,7 +797,8 @@ server.registerTool(
|
|
|
750
797
|
const supersededNote = result.supersededIds && result.supersededIds.length > 0
|
|
751
798
|
? ` Superseded: ${result.supersededIds.map(i => `#${i}`).join(', ')}.`
|
|
752
799
|
: '';
|
|
753
|
-
|
|
800
|
+
const nudge = buildLessonNudge({ type: result.type, id: result.id, lessonCaptured: result.lessonCaptured, surface: 'mcp' });
|
|
801
|
+
return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}${nudge}` }] };
|
|
754
802
|
})
|
|
755
803
|
);
|
|
756
804
|
|
|
@@ -801,6 +849,8 @@ server.registerTool(
|
|
|
801
849
|
const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
|
|
802
850
|
lines.push(`${r.ordinal}. ${pTag} [P${r.priority}] ${r.title} (D#${r.id})`);
|
|
803
851
|
}
|
|
852
|
+
// Affordance for the detail field — list stays title-only by design.
|
|
853
|
+
lines.push(`Full detail: mem_get ids=["D#<id>"]`);
|
|
804
854
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
805
855
|
})
|
|
806
856
|
);
|
|
@@ -848,97 +898,11 @@ server.registerTool(
|
|
|
848
898
|
return { content: [{ type: 'text', text: formatQualityReport(data) }] };
|
|
849
899
|
}
|
|
850
900
|
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const obsTotal = db.prepare(`SELECT COUNT(*) as c FROM observations WHERE 1=1 ${projectFilter}`).get(...baseParams);
|
|
857
|
-
const sessTotal = db.prepare(`SELECT COUNT(*) as c FROM session_summaries WHERE 1=1 ${projectFilter}`).get(...baseParams);
|
|
858
|
-
const promptTotal = args.project
|
|
859
|
-
? db.prepare(`SELECT COUNT(*) as c FROM user_prompts p JOIN sdk_sessions s ON p.content_session_id = s.content_session_id WHERE s.project = ?`).get(args.project)
|
|
860
|
-
: db.prepare(`SELECT COUNT(*) as c FROM user_prompts`).get();
|
|
861
|
-
|
|
862
|
-
// Recent counts
|
|
863
|
-
const obsRecent = db.prepare(`SELECT COUNT(*) as c FROM observations WHERE created_at_epoch >= ? ${projectFilter}`).get(cutoff, ...baseParams);
|
|
864
|
-
const sessRecent = db.prepare(`SELECT COUNT(*) as c FROM session_summaries WHERE created_at_epoch >= ? ${projectFilter}`).get(cutoff, ...baseParams);
|
|
865
|
-
|
|
866
|
-
// Type distribution (recent)
|
|
867
|
-
const types = db.prepare(`
|
|
868
|
-
SELECT type, COUNT(*) as c FROM observations
|
|
869
|
-
WHERE created_at_epoch >= ? ${projectFilter}
|
|
870
|
-
GROUP BY type ORDER BY c DESC
|
|
871
|
-
`).all(cutoff, ...baseParams);
|
|
872
|
-
|
|
873
|
-
// Projects (global view — skipped when filtering by single project)
|
|
874
|
-
const projects = args.project ? [] : db.prepare(`
|
|
875
|
-
SELECT project, COUNT(*) as c FROM observations
|
|
876
|
-
GROUP BY project ORDER BY c DESC
|
|
877
|
-
LIMIT 20
|
|
878
|
-
`).all();
|
|
879
|
-
|
|
880
|
-
// Daily activity (last 7 days)
|
|
881
|
-
const daily = db.prepare(`
|
|
882
|
-
SELECT date(created_at) as day, COUNT(*) as c FROM observations
|
|
883
|
-
WHERE created_at_epoch >= ? ${projectFilter}
|
|
884
|
-
GROUP BY day ORDER BY day DESC
|
|
885
|
-
LIMIT 7
|
|
886
|
-
`).all(Date.now() - 7 * 86400000, ...baseParams);
|
|
887
|
-
|
|
888
|
-
// Health metrics
|
|
889
|
-
const tokenEst = db.prepare(`
|
|
890
|
-
SELECT SUM(LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(narrative,'')) + LENGTH(COALESCE(text,''))) / 4 as t
|
|
891
|
-
FROM observations WHERE 1=1 ${projectFilter}
|
|
892
|
-
`).get(...baseParams);
|
|
893
|
-
|
|
894
|
-
const avgImp = db.prepare(`
|
|
895
|
-
SELECT AVG(COALESCE(importance,1)) as v FROM observations WHERE 1=1 ${projectFilter}
|
|
896
|
-
`).get(...baseParams);
|
|
897
|
-
|
|
898
|
-
const thirtyDaysAgo = Date.now() - 30 * 86400000;
|
|
899
|
-
// v3.23 noise-gauge de-blinding (mirrors mem-cli cmdStats): `<= 1` makes the
|
|
900
|
-
// imp=0 dormant population visible (decay floor + LLM low-signal filter push
|
|
901
|
-
// ~half the live corpus to 0); injection_count=0 mirrors decay's NEVER-INJECTED
|
|
902
|
-
// guard so injected-but-decayed pinned noise isn't miscounted as "never used".
|
|
903
|
-
const lowVal = db.prepare(`
|
|
904
|
-
SELECT COUNT(*) as c FROM observations
|
|
905
|
-
WHERE COALESCE(importance,1) <= 1 AND COALESCE(access_count,0) = 0
|
|
906
|
-
AND COALESCE(injection_count,0) = 0
|
|
907
|
-
AND COALESCE(compressed_into, 0) = 0
|
|
908
|
-
AND created_at_epoch < ? ${projectFilter}
|
|
909
|
-
`).get(thirtyDaysAgo, ...baseParams);
|
|
910
|
-
|
|
911
|
-
// Low-signal-title population (template / tool-log titles the read-side filter
|
|
912
|
-
// already excludes). The imp=1 "Low-value" metric can't see these, so the
|
|
913
|
-
// gauge under-reports real noise without it. See lib/low-signal-patterns.mjs.
|
|
914
|
-
const lowSignalTitle = db.prepare(`
|
|
915
|
-
SELECT COUNT(*) as c FROM observations
|
|
916
|
-
WHERE NOT ${buildNotLowSignalSql()}
|
|
917
|
-
AND COALESCE(compressed_into, 0) = 0 ${projectFilter}
|
|
918
|
-
`).get(...baseParams);
|
|
919
|
-
// F7: both noise numerators exclude compressed rows → divide by the LIVE count, not
|
|
920
|
-
// obsTotal (all rows), so a compress-heavy store isn't reported cleaner than it is.
|
|
921
|
-
const liveTotal = db.prepare(
|
|
922
|
-
`SELECT COUNT(*) as c FROM observations WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}`
|
|
923
|
-
).get(...baseParams);
|
|
924
|
-
const { noiseRatio, lowSignalRatio } = computeNoiseGauge({ liveTotal: liveTotal.c, lowValCount: lowVal.c, lowSignalCount: lowSignalTitle.c });
|
|
925
|
-
const compressedCount = db.prepare(`
|
|
926
|
-
SELECT COUNT(*) as c FROM observations WHERE compressed_into IS NOT NULL ${projectFilter}
|
|
927
|
-
`).get(...baseParams);
|
|
928
|
-
const supersededOnlyCount = db.prepare(`
|
|
929
|
-
SELECT COUNT(*) as c FROM observations WHERE superseded_at IS NOT NULL AND compressed_into IS NULL ${projectFilter}
|
|
930
|
-
`).get(...baseParams);
|
|
931
|
-
|
|
932
|
-
// Tier distribution
|
|
933
|
-
const tierCtx = { now: Date.now(), currentProject: args.project || inferProject(), currentSessionId: '' };
|
|
934
|
-
const tdParams = tierSqlParams(tierCtx);
|
|
935
|
-
const tierDist = db.prepare(`
|
|
936
|
-
SELECT tier, COUNT(*) as c FROM (
|
|
937
|
-
SELECT ${TIER_CASE_SQL} as tier FROM observations
|
|
938
|
-
WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
|
|
939
|
-
) GROUP BY tier ORDER BY tier
|
|
940
|
-
`).all(...tdParams, ...baseParams);
|
|
941
|
-
const tierMap = Object.fromEntries(tierDist.map(r => [r.tier, r.c]));
|
|
901
|
+
const {
|
|
902
|
+
obsTotal, sessTotal, promptTotal, obsRecent, sessRecent,
|
|
903
|
+
types, projects, daily, tokenEst, avgImp, lowVal, lowSignalTitle,
|
|
904
|
+
noiseRatio, lowSignalRatio, compressedCount, supersededOnlyCount, tierMap,
|
|
905
|
+
} = computeStatsFeed(db, { project: args.project || null, days });
|
|
942
906
|
|
|
943
907
|
const lines = [
|
|
944
908
|
`Memory Statistics${args.project ? ` (project: ${args.project})` : ''}:`,
|
package/source-files.mjs
CHANGED
|
@@ -162,6 +162,19 @@ export const SOURCE_FILES = [
|
|
|
162
162
|
// (cmdDelete) — extracted to kill the byte-duplicated twin. Missing it from the
|
|
163
163
|
// manifest would leave both delete surfaces unsigned/broken on auto-update.
|
|
164
164
|
'lib/delete-core.mjs',
|
|
165
|
+
// Shared primary stats feed (audit 2026-07-17 MED-4) — statically imported by
|
|
166
|
+
// server.mjs (mem_stats) and mem-cli.mjs (cmdStats); killed the ~80-line
|
|
167
|
+
// byte-duplicated twin. Missing it breaks both stats surfaces on auto-update.
|
|
168
|
+
'lib/stats-core.mjs',
|
|
169
|
+
// Observation `type` vocabulary single source (audit 2026-07-17 MED-3) —
|
|
170
|
+
// statically imported by tool-schemas.mjs, mem-cli.mjs, hook-llm.mjs,
|
|
171
|
+
// hook-optimize.mjs, lib/activity.mjs. Missing it breaks every save/validate
|
|
172
|
+
// path on auto-update.
|
|
173
|
+
'lib/obs-types.mjs',
|
|
174
|
+
// Save-time lesson nudge (audit 2026-07-17 P4) — statically imported by server.mjs
|
|
175
|
+
// (mem_save) and mem-cli.mjs (cmdSave). Missing it breaks both save surfaces on
|
|
176
|
+
// auto-update.
|
|
177
|
+
'lib/save-nudge.mjs',
|
|
165
178
|
// P10 dedup/merge threshold constants — single source of truth for the Jaccard
|
|
166
179
|
// dedup/merge cutoffs. Statically imported by hook.mjs, hook-llm.mjs,
|
|
167
180
|
// hook-optimize.mjs, mem-cli.mjs, server.mjs, and the save/maintain cores;
|
package/tool-schemas.mjs
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { CLI_INVOKE } from './cli-path.mjs';
|
|
6
|
+
import { OBS_TYPES } from './lib/obs-types.mjs';
|
|
6
7
|
|
|
7
|
-
export const OBS_TYPE_ENUM = z.enum([
|
|
8
|
+
export const OBS_TYPE_ENUM = z.enum([...OBS_TYPES]);
|
|
8
9
|
|
|
9
10
|
// LLM-friendly coercion: accept string numbers and normalize to proper types
|
|
10
11
|
const coerceInt = z.preprocess(
|
|
@@ -51,7 +52,7 @@ const coerceStringArray = z.preprocess(
|
|
|
51
52
|
z.array(z.string())
|
|
52
53
|
);
|
|
53
54
|
|
|
54
|
-
// Coerce mixed ID tokens (#N / P#N / S#N / bare N) for mem_get. Accepts:
|
|
55
|
+
// Coerce mixed ID tokens (#N / P#N / S#N / D#N / bare N) for mem_get. Accepts:
|
|
55
56
|
// - native arrays: [1, "P#2", "#3"]
|
|
56
57
|
// - single number: 1
|
|
57
58
|
// - single/comma string: "1,P#2,S#3"
|
|
@@ -75,7 +76,10 @@ const coerceMixedIdTokens = z.preprocess(
|
|
|
75
76
|
}
|
|
76
77
|
return v;
|
|
77
78
|
},
|
|
78
|
-
|
|
79
|
+
// D#N (deferred_work) requires the `#` — bare "D92" is prose, not a token.
|
|
80
|
+
// Round-trip rule: `defer list` renders "(D#92)", so mem_get must accept it
|
|
81
|
+
// back (tests/schema-roundtrip.test.mjs). Delete/timeline keep rejecting D#.
|
|
82
|
+
z.array(z.string().regex(/^(?:[Dd]#|[EePpSs]?#?)\d+$/, 'Expected N, #N, P#N, S#N, E#N, or D#N')).min(1).max(20)
|
|
79
83
|
);
|
|
80
84
|
|
|
81
85
|
export const memSearchSchema = {
|
|
@@ -136,8 +140,8 @@ export const memGetSchema = {
|
|
|
136
140
|
// Accepts mixed tokens so pasted search results work verbatim: [1], [1, "P#2"], "1,P#2,S#3",
|
|
137
141
|
// or the JSON-stringified form ["1","P#2"]. Each token's prefix routes to its source bucket
|
|
138
142
|
// in server.mjs via lib/id-routing.bucketIdTokens. An explicit `source` override still wins.
|
|
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.'),
|
|
143
|
+
ids: coerceMixedIdTokens.describe('Mixed observation/prompt/session/event/deferred IDs — accepts N, #N, P#N, S#N, E#N, D#N; comma-strings and JSON arrays also coerced. D#N reads a deferred_work item with FULL detail (defer list is title-only)'),
|
|
144
|
+
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. D#N tokens are exempt — they always read deferred_work.'),
|
|
141
145
|
fields: coerceStringArray.optional().describe('Specific fields to return (default: all; validated against obs schema — session/prompt sources ignore this filter)'),
|
|
142
146
|
};
|
|
143
147
|
|