sensorium-mcp 2.7.0 → 2.8.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/dist/index.js +596 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +180 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +680 -0
- package/dist/memory.js.map +1 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import { peekThreadMessages, readThreadMessages, startDispatcher } from "./dispa
|
|
|
36
36
|
import { analyzeVoiceEmotion, analyzeVideoFrames, extractVideoFrames, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
|
|
37
37
|
import { TelegramClient } from "./telegram.js";
|
|
38
38
|
import { describeADV, errorMessage, errorResult, IMAGE_EXTENSIONS, OPENAI_TTS_MAX_CHARS } from "./utils.js";
|
|
39
|
+
import { initMemoryDb, assembleBootstrap, getRecentEpisodes, searchSemanticNotes, searchProcedures, saveSemanticNote, saveProcedure, updateSemanticNote, supersedeNote, updateProcedure, getMemoryStatus, getTopicIndex, logConsolidation, getUnconsolidatedEpisodes, markConsolidated, forgetMemory, saveEpisode, saveVoiceSignature, } from "./memory.js";
|
|
39
40
|
/**
|
|
40
41
|
* Build human-readable analysis tags from a VoiceAnalysisResult.
|
|
41
42
|
* Fields that are null / undefined / empty are silently skipped.
|
|
@@ -220,6 +221,13 @@ function removeSession(chatId, name) {
|
|
|
220
221
|
saveSessionMap(map);
|
|
221
222
|
}
|
|
222
223
|
}
|
|
224
|
+
// Memory database — initialized lazily on first use
|
|
225
|
+
let memoryDb = null;
|
|
226
|
+
function getMemoryDb() {
|
|
227
|
+
if (!memoryDb)
|
|
228
|
+
memoryDb = initMemoryDb();
|
|
229
|
+
return memoryDb;
|
|
230
|
+
}
|
|
223
231
|
// Thread ID of the active session's forum topic. Set by start_session.
|
|
224
232
|
// All sends and receives are scoped to this thread so concurrent sessions
|
|
225
233
|
// in different topics never interfere with each other.
|
|
@@ -437,6 +445,223 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
437
445
|
},
|
|
438
446
|
},
|
|
439
447
|
},
|
|
448
|
+
// ── Memory Tools ──────────────────────────────────────────────────
|
|
449
|
+
{
|
|
450
|
+
name: "memory_bootstrap",
|
|
451
|
+
description: "Load memory briefing for session start. Call this ONCE after start_session. " +
|
|
452
|
+
"Returns operator profile, recent context, active procedures, and memory health. " +
|
|
453
|
+
"~2,500 tokens. Essential for crash recovery — restores knowledge from previous sessions.",
|
|
454
|
+
inputSchema: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
threadId: {
|
|
458
|
+
type: "number",
|
|
459
|
+
description: "Active thread ID.",
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "memory_search",
|
|
466
|
+
description: "Search across all memory layers for relevant information. " +
|
|
467
|
+
"Use BEFORE starting any task to recall facts, preferences, past events, or procedures. " +
|
|
468
|
+
"Returns ranked results with source layer. Do NOT use for info already in your bootstrap briefing.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
query: {
|
|
473
|
+
type: "string",
|
|
474
|
+
description: "Natural language search query.",
|
|
475
|
+
},
|
|
476
|
+
layers: {
|
|
477
|
+
type: "array",
|
|
478
|
+
items: { type: "string" },
|
|
479
|
+
description: 'Filter layers: ["episodic", "semantic", "procedural"]. Default: all.',
|
|
480
|
+
},
|
|
481
|
+
types: {
|
|
482
|
+
type: "array",
|
|
483
|
+
items: { type: "string" },
|
|
484
|
+
description: 'Filter by type: ["fact", "preference", "pattern", "workflow", ...].',
|
|
485
|
+
},
|
|
486
|
+
maxTokens: {
|
|
487
|
+
type: "number",
|
|
488
|
+
description: "Token budget for results. Default: 1500.",
|
|
489
|
+
},
|
|
490
|
+
threadId: {
|
|
491
|
+
type: "number",
|
|
492
|
+
description: "Active thread ID.",
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
required: ["query"],
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "memory_save",
|
|
500
|
+
description: "Save a piece of knowledge to semantic memory (Layer 3). " +
|
|
501
|
+
"Use when you learn something important that should persist across sessions: " +
|
|
502
|
+
"operator preferences, corrections, facts, patterns. " +
|
|
503
|
+
"Do NOT use for routine conversation — episodic memory captures that automatically.",
|
|
504
|
+
inputSchema: {
|
|
505
|
+
type: "object",
|
|
506
|
+
properties: {
|
|
507
|
+
content: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "The fact/preference/pattern in one clear sentence.",
|
|
510
|
+
},
|
|
511
|
+
type: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description: '"fact" | "preference" | "pattern" | "entity" | "relationship".',
|
|
514
|
+
},
|
|
515
|
+
keywords: {
|
|
516
|
+
type: "array",
|
|
517
|
+
items: { type: "string" },
|
|
518
|
+
description: "3-7 keywords for retrieval.",
|
|
519
|
+
},
|
|
520
|
+
confidence: {
|
|
521
|
+
type: "number",
|
|
522
|
+
description: "0.0-1.0. Default: 0.8.",
|
|
523
|
+
},
|
|
524
|
+
threadId: {
|
|
525
|
+
type: "number",
|
|
526
|
+
description: "Active thread ID.",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
required: ["content", "type", "keywords"],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "memory_save_procedure",
|
|
534
|
+
description: "Save or update a learned workflow/procedure to procedural memory (Layer 4). " +
|
|
535
|
+
"Use after completing a multi-step task the 2nd+ time, or when the operator teaches a process.",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
name: {
|
|
540
|
+
type: "string",
|
|
541
|
+
description: "Short name for the procedure.",
|
|
542
|
+
},
|
|
543
|
+
type: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description: '"workflow" | "habit" | "tool_pattern" | "template".',
|
|
546
|
+
},
|
|
547
|
+
description: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "What this procedure accomplishes.",
|
|
550
|
+
},
|
|
551
|
+
steps: {
|
|
552
|
+
type: "array",
|
|
553
|
+
items: { type: "string" },
|
|
554
|
+
description: "Ordered steps (for workflows).",
|
|
555
|
+
},
|
|
556
|
+
triggerConditions: {
|
|
557
|
+
type: "array",
|
|
558
|
+
items: { type: "string" },
|
|
559
|
+
description: "When to use this procedure.",
|
|
560
|
+
},
|
|
561
|
+
procedureId: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Existing ID to update (omit to create new).",
|
|
564
|
+
},
|
|
565
|
+
threadId: {
|
|
566
|
+
type: "number",
|
|
567
|
+
description: "Active thread ID.",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
required: ["name", "type", "description"],
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "memory_update",
|
|
575
|
+
description: "Update or supersede an existing semantic note or procedure. " +
|
|
576
|
+
"Use when operator corrects stored information or when facts have changed.",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
memoryId: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "note_id or procedure_id to update.",
|
|
583
|
+
},
|
|
584
|
+
action: {
|
|
585
|
+
type: "string",
|
|
586
|
+
description: '"update" (modify in place) | "supersede" (expire old, create new).',
|
|
587
|
+
},
|
|
588
|
+
newContent: {
|
|
589
|
+
type: "string",
|
|
590
|
+
description: "New content (required for supersede, optional for update).",
|
|
591
|
+
},
|
|
592
|
+
newConfidence: {
|
|
593
|
+
type: "number",
|
|
594
|
+
description: "Updated confidence score.",
|
|
595
|
+
},
|
|
596
|
+
reason: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Why this is being updated.",
|
|
599
|
+
},
|
|
600
|
+
threadId: {
|
|
601
|
+
type: "number",
|
|
602
|
+
description: "Active thread ID.",
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
required: ["memoryId", "action", "reason"],
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "memory_consolidate",
|
|
610
|
+
description: "Run memory consolidation cycle (sleep process). Normally triggered automatically during idle. " +
|
|
611
|
+
"Manually call if memory_status shows many unconsolidated episodes.",
|
|
612
|
+
inputSchema: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
threadId: {
|
|
616
|
+
type: "number",
|
|
617
|
+
description: "Active thread ID.",
|
|
618
|
+
},
|
|
619
|
+
phases: {
|
|
620
|
+
type: "array",
|
|
621
|
+
items: { type: "string" },
|
|
622
|
+
description: 'Run specific phases: ["promote", "decay", "meta"]. Default: all.',
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "memory_status",
|
|
629
|
+
description: "Get memory system health and statistics. Lightweight (~300 tokens). " +
|
|
630
|
+
"Use when unsure if you have relevant memories, to check if consolidation is needed, " +
|
|
631
|
+
"or to report memory state to operator.",
|
|
632
|
+
inputSchema: {
|
|
633
|
+
type: "object",
|
|
634
|
+
properties: {
|
|
635
|
+
threadId: {
|
|
636
|
+
type: "number",
|
|
637
|
+
description: "Active thread ID.",
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "memory_forget",
|
|
644
|
+
description: "Mark a memory as expired/forgotten. Use sparingly — most forgetting happens via decay. " +
|
|
645
|
+
"Use when operator explicitly asks to forget something or info is confirmed wrong.",
|
|
646
|
+
inputSchema: {
|
|
647
|
+
type: "object",
|
|
648
|
+
properties: {
|
|
649
|
+
memoryId: {
|
|
650
|
+
type: "string",
|
|
651
|
+
description: "note_id, procedure_id, or episode_id to forget.",
|
|
652
|
+
},
|
|
653
|
+
reason: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description: "Why this is being forgotten.",
|
|
656
|
+
},
|
|
657
|
+
threadId: {
|
|
658
|
+
type: "number",
|
|
659
|
+
description: "Active thread ID.",
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
required: ["memoryId", "reason"],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
440
665
|
],
|
|
441
666
|
}));
|
|
442
667
|
// ── Tool implementations ────────────────────────────────────────────────────
|
|
@@ -462,6 +687,7 @@ function getReminders(threadId) {
|
|
|
462
687
|
"\n2. **Subagents**: Use subagents heavily — spin them up for code edits, searches, research, and reviews. Run them in parallel when tasks are independent. You plan and verify; subagents execute." +
|
|
463
688
|
"\n3. **Reporting**: Call `report_progress` after completing EACH todo item. The operator is remote and CANNOT see your work unless you explicitly report it. Silence = failure." +
|
|
464
689
|
"\n4. **Never stop**: When all work is done, call `remote_copilot_wait_for_instructions` immediately. Never summarize or stop." +
|
|
690
|
+
"\n5. **Memory**: Use `memory_save` to persist important facts/preferences. Use `memory_search` before tasks to recall context." +
|
|
465
691
|
threadHint +
|
|
466
692
|
`\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`);
|
|
467
693
|
}
|
|
@@ -570,12 +796,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
570
796
|
const threadNote = currentThreadId !== undefined
|
|
571
797
|
? ` Thread ID: ${currentThreadId} (pass this to start_session as threadId to resume this topic later).`
|
|
572
798
|
: "";
|
|
799
|
+
// Auto-bootstrap memory
|
|
800
|
+
let memoryBriefing = "";
|
|
801
|
+
try {
|
|
802
|
+
const db = getMemoryDb();
|
|
803
|
+
if (currentThreadId !== undefined) {
|
|
804
|
+
memoryBriefing = "\n\n" + assembleBootstrap(db, currentThreadId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
memoryBriefing = "\n\n_Memory system unavailable._";
|
|
809
|
+
}
|
|
573
810
|
return {
|
|
574
811
|
content: [
|
|
575
812
|
{
|
|
576
813
|
type: "text",
|
|
577
814
|
text: `Session ${resolvedPreexisting ? "resumed" : "started"}.${threadNote}` +
|
|
578
815
|
` Call the remote_copilot_wait_for_instructions tool next.` +
|
|
816
|
+
memoryBriefing +
|
|
579
817
|
getReminders(currentThreadId),
|
|
580
818
|
},
|
|
581
819
|
],
|
|
@@ -612,6 +850,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
612
850
|
}
|
|
613
851
|
const contentBlocks = [];
|
|
614
852
|
let hasVoiceMessages = false;
|
|
853
|
+
let episodeAlreadySaved = false;
|
|
615
854
|
for (const msg of stored) {
|
|
616
855
|
// Photos: download the largest size, persist to disk, and embed as base64.
|
|
617
856
|
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
@@ -699,6 +938,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
699
938
|
? `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: ${transcript}`
|
|
700
939
|
: `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: (empty — no speech detected)`,
|
|
701
940
|
});
|
|
941
|
+
// Auto-save voice signature
|
|
942
|
+
if (analysis && effectiveThreadId !== undefined) {
|
|
943
|
+
try {
|
|
944
|
+
const db = getMemoryDb();
|
|
945
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
946
|
+
const epId = saveEpisode(db, {
|
|
947
|
+
sessionId,
|
|
948
|
+
threadId: effectiveThreadId,
|
|
949
|
+
type: "operator_message",
|
|
950
|
+
modality: "voice",
|
|
951
|
+
content: { raw: transcript ?? "", duration: msg.message.voice.duration },
|
|
952
|
+
importance: 0.6,
|
|
953
|
+
});
|
|
954
|
+
saveVoiceSignature(db, {
|
|
955
|
+
episodeId: epId,
|
|
956
|
+
emotion: analysis.emotion ?? undefined,
|
|
957
|
+
arousal: analysis.arousal ?? undefined,
|
|
958
|
+
dominance: analysis.dominance ?? undefined,
|
|
959
|
+
valence: analysis.valence ?? undefined,
|
|
960
|
+
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
961
|
+
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
962
|
+
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
963
|
+
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
964
|
+
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
965
|
+
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
966
|
+
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
967
|
+
durationSec: msg.message.voice.duration,
|
|
968
|
+
});
|
|
969
|
+
episodeAlreadySaved = true;
|
|
970
|
+
}
|
|
971
|
+
catch (_) { /* non-fatal */ }
|
|
972
|
+
}
|
|
702
973
|
}
|
|
703
974
|
catch (err) {
|
|
704
975
|
contentBlocks.push({
|
|
@@ -754,6 +1025,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
754
1025
|
if (!sceneDescription && !transcript)
|
|
755
1026
|
parts.push("(no visual or audio content could be extracted)");
|
|
756
1027
|
contentBlocks.push({ type: "text", text: parts.join("\n") });
|
|
1028
|
+
// Auto-save voice signature for video notes
|
|
1029
|
+
if (analysis && effectiveThreadId !== undefined) {
|
|
1030
|
+
try {
|
|
1031
|
+
const db = getMemoryDb();
|
|
1032
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
1033
|
+
const epId = saveEpisode(db, {
|
|
1034
|
+
sessionId,
|
|
1035
|
+
threadId: effectiveThreadId,
|
|
1036
|
+
type: "operator_message",
|
|
1037
|
+
modality: "video_note",
|
|
1038
|
+
content: { raw: transcript ?? "", scene: sceneDescription ?? "", duration: vn.duration },
|
|
1039
|
+
importance: 0.6,
|
|
1040
|
+
});
|
|
1041
|
+
saveVoiceSignature(db, {
|
|
1042
|
+
episodeId: epId,
|
|
1043
|
+
emotion: analysis.emotion ?? undefined,
|
|
1044
|
+
arousal: analysis.arousal ?? undefined,
|
|
1045
|
+
dominance: analysis.dominance ?? undefined,
|
|
1046
|
+
valence: analysis.valence ?? undefined,
|
|
1047
|
+
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1048
|
+
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1049
|
+
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1050
|
+
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1051
|
+
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1052
|
+
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1053
|
+
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1054
|
+
durationSec: vn.duration,
|
|
1055
|
+
});
|
|
1056
|
+
episodeAlreadySaved = true;
|
|
1057
|
+
}
|
|
1058
|
+
catch (_) { /* non-fatal */ }
|
|
1059
|
+
}
|
|
757
1060
|
}
|
|
758
1061
|
catch (err) {
|
|
759
1062
|
contentBlocks.push({
|
|
@@ -776,6 +1079,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
776
1079
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
777
1080
|
continue;
|
|
778
1081
|
}
|
|
1082
|
+
// Auto-ingest episodes (skip if a modality-specific handler already saved)
|
|
1083
|
+
if (!episodeAlreadySaved) {
|
|
1084
|
+
try {
|
|
1085
|
+
const db = getMemoryDb();
|
|
1086
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
1087
|
+
if (effectiveThreadId !== undefined) {
|
|
1088
|
+
const textContent = contentBlocks
|
|
1089
|
+
.filter((b) => b.type === "text")
|
|
1090
|
+
.map(b => b.text)
|
|
1091
|
+
.join("\n")
|
|
1092
|
+
.slice(0, 2000);
|
|
1093
|
+
saveEpisode(db, {
|
|
1094
|
+
sessionId,
|
|
1095
|
+
threadId: effectiveThreadId,
|
|
1096
|
+
type: "operator_message",
|
|
1097
|
+
modality: hasVoiceMessages ? "voice" : "text",
|
|
1098
|
+
content: { raw: textContent },
|
|
1099
|
+
importance: 0.5,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (_) { /* memory write failures should never break the main flow */ }
|
|
1104
|
+
}
|
|
779
1105
|
return {
|
|
780
1106
|
content: [
|
|
781
1107
|
{
|
|
@@ -1174,6 +1500,276 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1174
1500
|
}],
|
|
1175
1501
|
};
|
|
1176
1502
|
}
|
|
1503
|
+
// ── memory_bootstrap ────────────────────────────────────────────────────
|
|
1504
|
+
if (name === "memory_bootstrap") {
|
|
1505
|
+
const threadId = resolveThreadId(args);
|
|
1506
|
+
if (threadId === undefined) {
|
|
1507
|
+
return { content: [{ type: "text", text: "Error: No active thread. Call start_session first." + getReminders() }] };
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
const db = getMemoryDb();
|
|
1511
|
+
const briefing = assembleBootstrap(db, threadId);
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getReminders(threadId) }],
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
catch (err) {
|
|
1517
|
+
return { content: [{ type: "text", text: `Memory bootstrap error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
// ── memory_search ───────────────────────────────────────────────────────
|
|
1521
|
+
if (name === "memory_search") {
|
|
1522
|
+
const typedArgs = (args ?? {});
|
|
1523
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1524
|
+
const query = String(typedArgs.query ?? "");
|
|
1525
|
+
if (!query) {
|
|
1526
|
+
return { content: [{ type: "text", text: "Error: query is required." + getReminders(threadId) }] };
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const db = getMemoryDb();
|
|
1530
|
+
const layers = typedArgs.layers ?? ["episodic", "semantic", "procedural"];
|
|
1531
|
+
const types = typedArgs.types;
|
|
1532
|
+
const results = [];
|
|
1533
|
+
if (layers.includes("semantic")) {
|
|
1534
|
+
const notes = searchSemanticNotes(db, query, { types, maxResults: 10 });
|
|
1535
|
+
if (notes.length > 0) {
|
|
1536
|
+
results.push("### Semantic Memory");
|
|
1537
|
+
for (const n of notes) {
|
|
1538
|
+
results.push(`- **[${n.type}]** ${n.content} _(conf: ${n.confidence}, id: ${n.noteId})_`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (layers.includes("procedural")) {
|
|
1543
|
+
const procs = searchProcedures(db, query, 5);
|
|
1544
|
+
if (procs.length > 0) {
|
|
1545
|
+
results.push("### Procedural Memory");
|
|
1546
|
+
for (const p of procs) {
|
|
1547
|
+
results.push(`- **${p.name}** (${p.type}): ${p.description} _(success: ${Math.round(p.successRate * 100)}%, id: ${p.procedureId})_`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (layers.includes("episodic") && threadId !== undefined) {
|
|
1552
|
+
const episodes = getRecentEpisodes(db, threadId, 10);
|
|
1553
|
+
const filtered = episodes.filter(ep => {
|
|
1554
|
+
const content = JSON.stringify(ep.content).toLowerCase();
|
|
1555
|
+
return query.toLowerCase().split(/\s+/).some(word => content.includes(word));
|
|
1556
|
+
});
|
|
1557
|
+
if (filtered.length > 0) {
|
|
1558
|
+
results.push("### Episodic Memory");
|
|
1559
|
+
for (const ep of filtered.slice(0, 5)) {
|
|
1560
|
+
const summary = typeof ep.content === "object" && ep.content !== null
|
|
1561
|
+
? ep.content.text ?? JSON.stringify(ep.content).slice(0, 200)
|
|
1562
|
+
: String(ep.content).slice(0, 200);
|
|
1563
|
+
results.push(`- [${ep.modality}] ${summary} _(${ep.timestamp}, id: ${ep.episodeId})_`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
const text = results.length > 0
|
|
1568
|
+
? results.join("\n")
|
|
1569
|
+
: `No memories found for "${query}".`;
|
|
1570
|
+
return { content: [{ type: "text", text: text + getReminders(threadId) }] };
|
|
1571
|
+
}
|
|
1572
|
+
catch (err) {
|
|
1573
|
+
return { content: [{ type: "text", text: `Memory search error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
// ── memory_save ─────────────────────────────────────────────────────────
|
|
1577
|
+
if (name === "memory_save") {
|
|
1578
|
+
const typedArgs = (args ?? {});
|
|
1579
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1580
|
+
try {
|
|
1581
|
+
const db = getMemoryDb();
|
|
1582
|
+
const noteId = saveSemanticNote(db, {
|
|
1583
|
+
type: String(typedArgs.type ?? "fact"),
|
|
1584
|
+
content: String(typedArgs.content ?? ""),
|
|
1585
|
+
keywords: typedArgs.keywords ?? [],
|
|
1586
|
+
confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
|
|
1587
|
+
});
|
|
1588
|
+
return {
|
|
1589
|
+
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) }],
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
catch (err) {
|
|
1593
|
+
return { content: [{ type: "text", text: `Memory save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
// ── memory_save_procedure ───────────────────────────────────────────────
|
|
1597
|
+
if (name === "memory_save_procedure") {
|
|
1598
|
+
const typedArgs = (args ?? {});
|
|
1599
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1600
|
+
try {
|
|
1601
|
+
const db = getMemoryDb();
|
|
1602
|
+
const existingId = typedArgs.procedureId;
|
|
1603
|
+
if (existingId) {
|
|
1604
|
+
updateProcedure(db, existingId, {
|
|
1605
|
+
description: typedArgs.description,
|
|
1606
|
+
steps: typedArgs.steps,
|
|
1607
|
+
triggerConditions: typedArgs.triggerConditions,
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
content: [{ type: "text", text: `Updated procedure: ${existingId}` + getReminders(threadId) }],
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
const procId = saveProcedure(db, {
|
|
1614
|
+
name: String(typedArgs.name ?? ""),
|
|
1615
|
+
type: String(typedArgs.type ?? "workflow"),
|
|
1616
|
+
description: String(typedArgs.description ?? ""),
|
|
1617
|
+
steps: typedArgs.steps,
|
|
1618
|
+
triggerConditions: typedArgs.triggerConditions,
|
|
1619
|
+
});
|
|
1620
|
+
return {
|
|
1621
|
+
content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
catch (err) {
|
|
1625
|
+
return { content: [{ type: "text", text: `Procedure save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
// ── memory_update ───────────────────────────────────────────────────────
|
|
1629
|
+
if (name === "memory_update") {
|
|
1630
|
+
const typedArgs = (args ?? {});
|
|
1631
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1632
|
+
try {
|
|
1633
|
+
const db = getMemoryDb();
|
|
1634
|
+
const memId = String(typedArgs.memoryId ?? "");
|
|
1635
|
+
const action = String(typedArgs.action ?? "update");
|
|
1636
|
+
const reason = String(typedArgs.reason ?? "");
|
|
1637
|
+
if (action === "supersede" && memId.startsWith("sn_")) {
|
|
1638
|
+
const origRow = db.prepare("SELECT type, keywords FROM semantic_notes WHERE note_id = ?").get(memId);
|
|
1639
|
+
const newId = supersedeNote(db, memId, {
|
|
1640
|
+
type: (origRow?.type ?? "fact"),
|
|
1641
|
+
content: String(typedArgs.newContent ?? ""),
|
|
1642
|
+
keywords: origRow?.keywords ? JSON.parse(origRow.keywords) : [],
|
|
1643
|
+
confidence: typeof typedArgs.newConfidence === "number" ? typedArgs.newConfidence : 0.8,
|
|
1644
|
+
});
|
|
1645
|
+
return {
|
|
1646
|
+
content: [{ type: "text", text: `Superseded ${memId} → ${newId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (memId.startsWith("sn_")) {
|
|
1650
|
+
const updates = {};
|
|
1651
|
+
if (typedArgs.newContent)
|
|
1652
|
+
updates.content = String(typedArgs.newContent);
|
|
1653
|
+
if (typeof typedArgs.newConfidence === "number")
|
|
1654
|
+
updates.confidence = typedArgs.newConfidence;
|
|
1655
|
+
updateSemanticNote(db, memId, updates);
|
|
1656
|
+
return {
|
|
1657
|
+
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
if (memId.startsWith("pr_")) {
|
|
1661
|
+
const updates = {};
|
|
1662
|
+
if (typedArgs.newContent)
|
|
1663
|
+
updates.description = String(typedArgs.newContent);
|
|
1664
|
+
if (typeof typedArgs.newConfidence === "number")
|
|
1665
|
+
updates.confidence = typedArgs.newConfidence;
|
|
1666
|
+
updateProcedure(db, memId, updates);
|
|
1667
|
+
return {
|
|
1668
|
+
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
return { content: [{ type: "text", text: `Unknown memory ID format: ${memId}` + getReminders(threadId) }] };
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
return { content: [{ type: "text", text: `Memory update error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
// ── memory_consolidate ──────────────────────────────────────────────────
|
|
1678
|
+
if (name === "memory_consolidate") {
|
|
1679
|
+
const typedArgs = (args ?? {});
|
|
1680
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1681
|
+
if (threadId === undefined) {
|
|
1682
|
+
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
const db = getMemoryDb();
|
|
1686
|
+
const startMs = Date.now();
|
|
1687
|
+
const uncons = getUnconsolidatedEpisodes(db, threadId, 50);
|
|
1688
|
+
if (uncons.length === 0) {
|
|
1689
|
+
return {
|
|
1690
|
+
content: [{ type: "text", text: "No unconsolidated episodes. Memory is up to date." + getReminders(threadId) }],
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
// Phase 1: Mark episodes as consolidated (decay phase — computational only)
|
|
1694
|
+
const episodeIds = uncons.map(ep => ep.episodeId);
|
|
1695
|
+
markConsolidated(db, episodeIds);
|
|
1696
|
+
// Phase 2: Log the consolidation
|
|
1697
|
+
logConsolidation(db, {
|
|
1698
|
+
episodesProcessed: uncons.length,
|
|
1699
|
+
notesCreated: 0,
|
|
1700
|
+
notesMerged: 0,
|
|
1701
|
+
notesSuperseded: 0,
|
|
1702
|
+
proceduresUpdated: 0,
|
|
1703
|
+
durationMs: Date.now() - startMs,
|
|
1704
|
+
});
|
|
1705
|
+
const report = [
|
|
1706
|
+
"## Consolidation Report",
|
|
1707
|
+
`- Episodes processed: ${uncons.length}`,
|
|
1708
|
+
`- Duration: ${Date.now() - startMs}ms`,
|
|
1709
|
+
"",
|
|
1710
|
+
"**Note:** For intelligent consolidation (extracting patterns, creating semantic notes), " +
|
|
1711
|
+
"review the unconsolidated episodes and use memory_save to create semantic notes.",
|
|
1712
|
+
].join("\n");
|
|
1713
|
+
return { content: [{ type: "text", text: report + getReminders(threadId) }] };
|
|
1714
|
+
}
|
|
1715
|
+
catch (err) {
|
|
1716
|
+
return { content: [{ type: "text", text: `Consolidation error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
// ── memory_status ───────────────────────────────────────────────────────
|
|
1720
|
+
if (name === "memory_status") {
|
|
1721
|
+
const typedArgs = (args ?? {});
|
|
1722
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1723
|
+
if (threadId === undefined) {
|
|
1724
|
+
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const db = getMemoryDb();
|
|
1728
|
+
const status = getMemoryStatus(db, threadId);
|
|
1729
|
+
const topics = getTopicIndex(db);
|
|
1730
|
+
const lines = [
|
|
1731
|
+
"## Memory Status",
|
|
1732
|
+
`- Episodes: ${status.totalEpisodes} (${status.unconsolidatedEpisodes} unconsolidated)`,
|
|
1733
|
+
`- Semantic notes: ${status.totalSemanticNotes}`,
|
|
1734
|
+
`- Procedures: ${status.totalProcedures}`,
|
|
1735
|
+
`- Voice signatures: ${status.totalVoiceSignatures}`,
|
|
1736
|
+
`- Last consolidation: ${status.lastConsolidation ?? "never"}`,
|
|
1737
|
+
`- DB size: ${(status.dbSizeBytes / 1024).toFixed(1)} KB`,
|
|
1738
|
+
];
|
|
1739
|
+
if (topics.length > 0) {
|
|
1740
|
+
lines.push("", "**Topics:**");
|
|
1741
|
+
for (const t of topics.slice(0, 15)) {
|
|
1742
|
+
lines.push(`- ${t.topic} (${t.semanticCount} notes, ${t.proceduralCount} procs, conf: ${t.avgConfidence.toFixed(2)})`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return { content: [{ type: "text", text: lines.join("\n") + getReminders(threadId) }] };
|
|
1746
|
+
}
|
|
1747
|
+
catch (err) {
|
|
1748
|
+
return { content: [{ type: "text", text: `Memory status error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
// ── memory_forget ───────────────────────────────────────────────────────
|
|
1752
|
+
if (name === "memory_forget") {
|
|
1753
|
+
const typedArgs = (args ?? {});
|
|
1754
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1755
|
+
try {
|
|
1756
|
+
const db = getMemoryDb();
|
|
1757
|
+
const memId = String(typedArgs.memoryId ?? "");
|
|
1758
|
+
const reason = String(typedArgs.reason ?? "");
|
|
1759
|
+
const result = forgetMemory(db, memId, reason);
|
|
1760
|
+
if (!result.deleted) {
|
|
1761
|
+
return {
|
|
1762
|
+
content: [{ type: "text", text: `Memory ${memId} not found (layer: ${result.layer}). Nothing was deleted.` + getReminders(threadId) }],
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
return {
|
|
1766
|
+
content: [{ type: "text", text: `Forgot ${result.layer} memory ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
catch (err) {
|
|
1770
|
+
return { content: [{ type: "text", text: `Memory forget error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1177
1773
|
// Unknown tool
|
|
1178
1774
|
return errorResult(`Unknown tool: ${name}`);
|
|
1179
1775
|
});
|