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 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
  });