sensorium-mcp 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,7 +33,9 @@ import { createRequire } from "module";
33
33
  import { homedir } from "os";
34
34
  import { basename, join } from "path";
35
35
  import { peekThreadMessages, readThreadMessages, startDispatcher } from "./dispatcher.js";
36
- import { analyzeVoiceEmotion, analyzeVideoFrames, extractVideoFrames, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
36
+ import { assembleBootstrap, forgetMemory, getMemoryStatus, getRecentEpisodes, getTopicIndex, initMemoryDb, runIntelligentConsolidation, saveEpisode, saveProcedure, saveSemanticNote, saveVoiceSignature, searchProcedures, searchSemanticNotes, supersedeNote, updateProcedure, updateSemanticNote, } from "./memory.js";
37
+ import { analyzeVideoFrames, analyzeVoiceEmotion, extractVideoFrames, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
38
+ import { addSchedule, checkDueTasks, generateTaskId, listSchedules, purgeSchedules, removeSchedule } from "./scheduler.js";
37
39
  import { TelegramClient } from "./telegram.js";
38
40
  import { describeADV, errorMessage, errorResult, IMAGE_EXTENSIONS, OPENAI_TTS_MAX_CHARS } from "./utils.js";
39
41
  /**
@@ -71,7 +73,6 @@ function buildAnalysisTags(analysis) {
71
73
  }
72
74
  return tags;
73
75
  }
74
- import { addSchedule, checkDueTasks, generateTaskId, listSchedules, purgeSchedules, removeSchedule } from "./scheduler.js";
75
76
  const esmRequire = createRequire(import.meta.url);
76
77
  const { version: PKG_VERSION } = esmRequire("../package.json");
77
78
  const telegramifyMarkdown = esmRequire("telegramify-markdown");
@@ -155,6 +156,21 @@ const telegram = new TelegramClient(TELEGRAM_TOKEN);
155
156
  // ensures no updates are lost between concurrent sessions.
156
157
  // ---------------------------------------------------------------------------
157
158
  await startDispatcher(telegram, TELEGRAM_CHAT_ID);
159
+ // Dead session detector — runs every 2 minutes
160
+ setInterval(async () => {
161
+ if (!currentThreadId)
162
+ return;
163
+ const elapsed = Date.now() - lastToolCallAt;
164
+ if (elapsed > DEAD_SESSION_TIMEOUT_MS && !deadSessionAlerted) {
165
+ deadSessionAlerted = true;
166
+ try {
167
+ const tg = new TelegramClient(TELEGRAM_TOKEN);
168
+ const minutes = Math.round(elapsed / 60000);
169
+ await tg.sendMessage(TELEGRAM_CHAT_ID, `⚠️ *Session appears down* — no tool calls in ${minutes} minutes\\. The agent may have crashed or the VS Code window compacted the context\\. Please check and restart if needed\\.`, "MarkdownV2", currentThreadId);
170
+ }
171
+ catch (_) { /* non-fatal */ }
172
+ }
173
+ }, 2 * 60 * 1000);
158
174
  // Directory for persisting downloaded images and documents to disk.
159
175
  const FILES_DIR = join(homedir(), ".remote-copilot-mcp", "files");
160
176
  mkdirSync(FILES_DIR, { recursive: true });
@@ -220,6 +236,13 @@ function removeSession(chatId, name) {
220
236
  saveSessionMap(map);
221
237
  }
222
238
  }
239
+ // Memory database — initialized lazily on first use
240
+ let memoryDb = null;
241
+ function getMemoryDb() {
242
+ if (!memoryDb)
243
+ memoryDb = initMemoryDb();
244
+ return memoryDb;
245
+ }
223
246
  // Thread ID of the active session's forum topic. Set by start_session.
224
247
  // All sends and receives are scoped to this thread so concurrent sessions
225
248
  // in different topics never interfere with each other.
@@ -243,11 +266,10 @@ function resolveThreadId(args) {
243
266
  }
244
267
  return currentThreadId;
245
268
  }
246
- // Timestamp of the last keep-alive ping sent to Telegram.
247
- // Used to send periodic "session still alive" messages so the operator knows
248
- // the agent hasn't silently died.
249
- let lastKeepAliveSentAt = Date.now();
250
- const KEEP_ALIVE_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
269
+ // Dead session detection — tracks when the last tool call was made
270
+ let lastToolCallAt = Date.now();
271
+ let deadSessionAlerted = false;
272
+ const DEAD_SESSION_TIMEOUT_MS = 60 * 60 * 1000; // 60 minutes (2× wait_for_instructions timeout)
251
273
  // Timestamp of the last message received from the operator.
252
274
  // Used by the scheduler to detect idle periods.
253
275
  let lastOperatorMessageAt = Date.now();
@@ -437,6 +459,223 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
437
459
  },
438
460
  },
439
461
  },
462
+ // ── Memory Tools ──────────────────────────────────────────────────
463
+ {
464
+ name: "memory_bootstrap",
465
+ description: "Load memory briefing for session start. Call this ONCE after start_session. " +
466
+ "Returns operator profile, recent context, active procedures, and memory health. " +
467
+ "~2,500 tokens. Essential for crash recovery — restores knowledge from previous sessions.",
468
+ inputSchema: {
469
+ type: "object",
470
+ properties: {
471
+ threadId: {
472
+ type: "number",
473
+ description: "Active thread ID.",
474
+ },
475
+ },
476
+ },
477
+ },
478
+ {
479
+ name: "memory_search",
480
+ description: "Search across all memory layers for relevant information. " +
481
+ "Use BEFORE starting any task to recall facts, preferences, past events, or procedures. " +
482
+ "Returns ranked results with source layer. Do NOT use for info already in your bootstrap briefing.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ query: {
487
+ type: "string",
488
+ description: "Natural language search query.",
489
+ },
490
+ layers: {
491
+ type: "array",
492
+ items: { type: "string" },
493
+ description: 'Filter layers: ["episodic", "semantic", "procedural"]. Default: all.',
494
+ },
495
+ types: {
496
+ type: "array",
497
+ items: { type: "string" },
498
+ description: 'Filter by type: ["fact", "preference", "pattern", "workflow", ...].',
499
+ },
500
+ maxTokens: {
501
+ type: "number",
502
+ description: "Token budget for results. Default: 1500.",
503
+ },
504
+ threadId: {
505
+ type: "number",
506
+ description: "Active thread ID.",
507
+ },
508
+ },
509
+ required: ["query"],
510
+ },
511
+ },
512
+ {
513
+ name: "memory_save",
514
+ description: "Save a piece of knowledge to semantic memory (Layer 3). " +
515
+ "Use when you learn something important that should persist across sessions: " +
516
+ "operator preferences, corrections, facts, patterns. " +
517
+ "Do NOT use for routine conversation — episodic memory captures that automatically.",
518
+ inputSchema: {
519
+ type: "object",
520
+ properties: {
521
+ content: {
522
+ type: "string",
523
+ description: "The fact/preference/pattern in one clear sentence.",
524
+ },
525
+ type: {
526
+ type: "string",
527
+ description: '"fact" | "preference" | "pattern" | "entity" | "relationship".',
528
+ },
529
+ keywords: {
530
+ type: "array",
531
+ items: { type: "string" },
532
+ description: "3-7 keywords for retrieval.",
533
+ },
534
+ confidence: {
535
+ type: "number",
536
+ description: "0.0-1.0. Default: 0.8.",
537
+ },
538
+ threadId: {
539
+ type: "number",
540
+ description: "Active thread ID.",
541
+ },
542
+ },
543
+ required: ["content", "type", "keywords"],
544
+ },
545
+ },
546
+ {
547
+ name: "memory_save_procedure",
548
+ description: "Save or update a learned workflow/procedure to procedural memory (Layer 4). " +
549
+ "Use after completing a multi-step task the 2nd+ time, or when the operator teaches a process.",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ name: {
554
+ type: "string",
555
+ description: "Short name for the procedure.",
556
+ },
557
+ type: {
558
+ type: "string",
559
+ description: '"workflow" | "habit" | "tool_pattern" | "template".',
560
+ },
561
+ description: {
562
+ type: "string",
563
+ description: "What this procedure accomplishes.",
564
+ },
565
+ steps: {
566
+ type: "array",
567
+ items: { type: "string" },
568
+ description: "Ordered steps (for workflows).",
569
+ },
570
+ triggerConditions: {
571
+ type: "array",
572
+ items: { type: "string" },
573
+ description: "When to use this procedure.",
574
+ },
575
+ procedureId: {
576
+ type: "string",
577
+ description: "Existing ID to update (omit to create new).",
578
+ },
579
+ threadId: {
580
+ type: "number",
581
+ description: "Active thread ID.",
582
+ },
583
+ },
584
+ required: ["name", "type", "description"],
585
+ },
586
+ },
587
+ {
588
+ name: "memory_update",
589
+ description: "Update or supersede an existing semantic note or procedure. " +
590
+ "Use when operator corrects stored information or when facts have changed.",
591
+ inputSchema: {
592
+ type: "object",
593
+ properties: {
594
+ memoryId: {
595
+ type: "string",
596
+ description: "note_id or procedure_id to update.",
597
+ },
598
+ action: {
599
+ type: "string",
600
+ description: '"update" (modify in place) | "supersede" (expire old, create new).',
601
+ },
602
+ newContent: {
603
+ type: "string",
604
+ description: "New content (required for supersede, optional for update).",
605
+ },
606
+ newConfidence: {
607
+ type: "number",
608
+ description: "Updated confidence score.",
609
+ },
610
+ reason: {
611
+ type: "string",
612
+ description: "Why this is being updated.",
613
+ },
614
+ threadId: {
615
+ type: "number",
616
+ description: "Active thread ID.",
617
+ },
618
+ },
619
+ required: ["memoryId", "action", "reason"],
620
+ },
621
+ },
622
+ {
623
+ name: "memory_consolidate",
624
+ description: "Run memory consolidation cycle (sleep process). Normally triggered automatically during idle. " +
625
+ "Manually call if memory_status shows many unconsolidated episodes.",
626
+ inputSchema: {
627
+ type: "object",
628
+ properties: {
629
+ threadId: {
630
+ type: "number",
631
+ description: "Active thread ID.",
632
+ },
633
+ phases: {
634
+ type: "array",
635
+ items: { type: "string" },
636
+ description: 'Run specific phases: ["promote", "decay", "meta"]. Default: all.',
637
+ },
638
+ },
639
+ },
640
+ },
641
+ {
642
+ name: "memory_status",
643
+ description: "Get memory system health and statistics. Lightweight (~300 tokens). " +
644
+ "Use when unsure if you have relevant memories, to check if consolidation is needed, " +
645
+ "or to report memory state to operator.",
646
+ inputSchema: {
647
+ type: "object",
648
+ properties: {
649
+ threadId: {
650
+ type: "number",
651
+ description: "Active thread ID.",
652
+ },
653
+ },
654
+ },
655
+ },
656
+ {
657
+ name: "memory_forget",
658
+ description: "Mark a memory as expired/forgotten. Use sparingly — most forgetting happens via decay. " +
659
+ "Use when operator explicitly asks to forget something or info is confirmed wrong.",
660
+ inputSchema: {
661
+ type: "object",
662
+ properties: {
663
+ memoryId: {
664
+ type: "string",
665
+ description: "note_id, procedure_id, or episode_id to forget.",
666
+ },
667
+ reason: {
668
+ type: "string",
669
+ description: "Why this is being forgotten.",
670
+ },
671
+ threadId: {
672
+ type: "number",
673
+ description: "Active thread ID.",
674
+ },
675
+ },
676
+ required: ["memoryId", "reason"],
677
+ },
678
+ },
440
679
  ],
441
680
  }));
442
681
  // ── Tool implementations ────────────────────────────────────────────────────
@@ -459,14 +698,18 @@ function getReminders(threadId) {
459
698
  : "";
460
699
  return ("\n\n## MANDATORY WORKFLOW" +
461
700
  "\n1. **Plan**: Use the todo list tool to break work into discrete items BEFORE starting. Non-negotiable." +
462
- "\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." +
701
+ "\n2. **Subagents**: Use subagents heavily — spin them up for code edits, searches, research, reviews, and terminal commands. Subagents have full access to ALL MCP tools including terminal, file system, and web search. Run them in parallel when tasks are independent. You plan and verify; subagents execute." +
463
702
  "\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
703
  "\n4. **Never stop**: When all work is done, call `remote_copilot_wait_for_instructions` immediately. Never summarize or stop." +
704
+ "\n5. **Memory**: (a) Call `memory_save` whenever you learn operator preferences, facts, or corrections. (b) Call `memory_search` before starting any task to recall relevant context. (c) Call `memory_status` when reporting progress to include memory health. These tools persist knowledge across sessions." +
465
705
  threadHint +
466
706
  `\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`);
467
707
  }
468
708
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
469
709
  const { name, arguments: args } = request.params;
710
+ // Dead session detection — reset on any tool call
711
+ lastToolCallAt = Date.now();
712
+ deadSessionAlerted = false;
470
713
  // ── start_session ─────────────────────────────────────────────────────────
471
714
  if (name === "start_session") {
472
715
  sessionStartedAt = Date.now();
@@ -513,7 +756,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
513
756
  // Resume mode: verify the thread is still alive by sending a message.
514
757
  // If the topic was deleted, drop the cached mapping and fall through to
515
758
  // create a new topic.
516
- lastKeepAliveSentAt = Date.now();
517
759
  try {
518
760
  const msg = convertMarkdown("🔄 **Session resumed.** Continuing in this thread.");
519
761
  await telegram.sendMessage(TELEGRAM_CHAT_ID, msg, "MarkdownV2", currentThreadId);
@@ -555,7 +797,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
555
797
  return errorResult(`Error: Could not create forum topic: ${errorMessage(err)}. ` +
556
798
  "Ensure the Telegram chat is a forum supergroup with the bot as admin with can_manage_topics right.");
557
799
  }
558
- lastKeepAliveSentAt = Date.now();
559
800
  try {
560
801
  const greeting = convertMarkdown("# 🤖 Remote Copilot Ready\n\n" +
561
802
  "Your AI assistant is online and listening.\n\n" +
@@ -570,12 +811,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
570
811
  const threadNote = currentThreadId !== undefined
571
812
  ? ` Thread ID: ${currentThreadId} (pass this to start_session as threadId to resume this topic later).`
572
813
  : "";
814
+ // Auto-bootstrap memory
815
+ let memoryBriefing = "";
816
+ try {
817
+ const db = getMemoryDb();
818
+ if (currentThreadId !== undefined) {
819
+ memoryBriefing = "\n\n" + assembleBootstrap(db, currentThreadId);
820
+ }
821
+ }
822
+ catch (e) {
823
+ memoryBriefing = "\n\n_Memory system unavailable._";
824
+ }
573
825
  return {
574
826
  content: [
575
827
  {
576
828
  type: "text",
577
829
  text: `Session ${resolvedPreexisting ? "resumed" : "started"}.${threadNote}` +
578
830
  ` Call the remote_copilot_wait_for_instructions tool next.` +
831
+ memoryBriefing +
579
832
  getReminders(currentThreadId),
580
833
  },
581
834
  ],
@@ -612,6 +865,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
612
865
  }
613
866
  const contentBlocks = [];
614
867
  let hasVoiceMessages = false;
868
+ let episodeAlreadySaved = false;
615
869
  for (const msg of stored) {
616
870
  // Photos: download the largest size, persist to disk, and embed as base64.
617
871
  if (msg.message.photo && msg.message.photo.length > 0) {
@@ -699,6 +953,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
699
953
  ? `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: ${transcript}`
700
954
  : `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: (empty — no speech detected)`,
701
955
  });
956
+ // Auto-save voice signature
957
+ if (analysis && effectiveThreadId !== undefined) {
958
+ try {
959
+ const db = getMemoryDb();
960
+ const sessionId = `session_${sessionStartedAt}`;
961
+ const epId = saveEpisode(db, {
962
+ sessionId,
963
+ threadId: effectiveThreadId,
964
+ type: "operator_message",
965
+ modality: "voice",
966
+ content: { raw: transcript ?? "", duration: msg.message.voice.duration },
967
+ importance: 0.6,
968
+ });
969
+ saveVoiceSignature(db, {
970
+ episodeId: epId,
971
+ emotion: analysis.emotion ?? undefined,
972
+ arousal: analysis.arousal ?? undefined,
973
+ dominance: analysis.dominance ?? undefined,
974
+ valence: analysis.valence ?? undefined,
975
+ speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
976
+ meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
977
+ pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
978
+ jitter: analysis.paralinguistics?.jitter ?? undefined,
979
+ shimmer: analysis.paralinguistics?.shimmer ?? undefined,
980
+ hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
981
+ audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
982
+ durationSec: msg.message.voice.duration,
983
+ });
984
+ episodeAlreadySaved = true;
985
+ }
986
+ catch (_) { /* non-fatal */ }
987
+ }
702
988
  }
703
989
  catch (err) {
704
990
  contentBlocks.push({
@@ -754,6 +1040,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
754
1040
  if (!sceneDescription && !transcript)
755
1041
  parts.push("(no visual or audio content could be extracted)");
756
1042
  contentBlocks.push({ type: "text", text: parts.join("\n") });
1043
+ // Auto-save voice signature for video notes
1044
+ if (analysis && effectiveThreadId !== undefined) {
1045
+ try {
1046
+ const db = getMemoryDb();
1047
+ const sessionId = `session_${sessionStartedAt}`;
1048
+ const epId = saveEpisode(db, {
1049
+ sessionId,
1050
+ threadId: effectiveThreadId,
1051
+ type: "operator_message",
1052
+ modality: "video_note",
1053
+ content: { raw: transcript ?? "", scene: sceneDescription ?? "", duration: vn.duration },
1054
+ importance: 0.6,
1055
+ });
1056
+ saveVoiceSignature(db, {
1057
+ episodeId: epId,
1058
+ emotion: analysis.emotion ?? undefined,
1059
+ arousal: analysis.arousal ?? undefined,
1060
+ dominance: analysis.dominance ?? undefined,
1061
+ valence: analysis.valence ?? undefined,
1062
+ speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
1063
+ meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
1064
+ pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
1065
+ jitter: analysis.paralinguistics?.jitter ?? undefined,
1066
+ shimmer: analysis.paralinguistics?.shimmer ?? undefined,
1067
+ hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
1068
+ audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
1069
+ durationSec: vn.duration,
1070
+ });
1071
+ episodeAlreadySaved = true;
1072
+ }
1073
+ catch (_) { /* non-fatal */ }
1074
+ }
757
1075
  }
758
1076
  catch (err) {
759
1077
  contentBlocks.push({
@@ -776,6 +1094,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
776
1094
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
777
1095
  continue;
778
1096
  }
1097
+ // Auto-ingest episodes (skip if a modality-specific handler already saved)
1098
+ if (!episodeAlreadySaved) {
1099
+ try {
1100
+ const db = getMemoryDb();
1101
+ const sessionId = `session_${sessionStartedAt}`;
1102
+ if (effectiveThreadId !== undefined) {
1103
+ const textContent = contentBlocks
1104
+ .filter((b) => b.type === "text")
1105
+ .map(b => b.text)
1106
+ .join("\n")
1107
+ .slice(0, 2000);
1108
+ saveEpisode(db, {
1109
+ sessionId,
1110
+ threadId: effectiveThreadId,
1111
+ type: "operator_message",
1112
+ modality: hasVoiceMessages ? "voice" : "text",
1113
+ content: { raw: textContent },
1114
+ importance: 0.5,
1115
+ });
1116
+ }
1117
+ }
1118
+ catch (_) { /* memory write failures should never break the main flow */ }
1119
+ }
779
1120
  return {
780
1121
  content: [
781
1122
  {
@@ -837,23 +1178,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
837
1178
  };
838
1179
  }
839
1180
  }
840
- // Keep-alive ping: send a periodic heartbeat to Telegram so the operator
841
- // knows the session is still alive even with no activity.
842
- let keepAliveSent = false;
843
- if (Date.now() - lastKeepAliveSentAt >= KEEP_ALIVE_INTERVAL_MS) {
844
- lastKeepAliveSentAt = Date.now();
845
- try {
846
- const ping = convertMarkdown(`🟢 **Session alive** — ${new Date().toLocaleString("en-GB", {
847
- day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false,
848
- })}` +
849
- ` (thread ${effectiveThreadId})`);
850
- await telegram.sendMessage(TELEGRAM_CHAT_ID, ping, "MarkdownV2", effectiveThreadId);
851
- keepAliveSent = true;
852
- }
853
- catch {
854
- // Non-fatal.
855
- }
856
- }
857
1181
  const idleMinutes = Math.round((Date.now() - lastOperatorMessageAt) / 60000);
858
1182
  // Show pending scheduled tasks if any exist.
859
1183
  let scheduleHint = "";
@@ -876,12 +1200,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
876
1200
  scheduleHint = `\n\n📋 **Pending scheduled tasks:**\n${taskList}`;
877
1201
  }
878
1202
  }
1203
+ // ── Auto-consolidation during idle ──────────────────────────────────────
1204
+ try {
1205
+ const idleMs = Date.now() - lastOperatorMessageAt;
1206
+ if (idleMs > 30 * 60 * 1000 && effectiveThreadId !== undefined) {
1207
+ const db = getMemoryDb();
1208
+ const report = await runIntelligentConsolidation(db, effectiveThreadId);
1209
+ if (report.episodesProcessed > 0) {
1210
+ process.stderr.write(`[memory] Consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
1211
+ }
1212
+ }
1213
+ }
1214
+ catch (_) { /* consolidation failure is non-fatal */ }
879
1215
  return {
880
1216
  content: [
881
1217
  {
882
1218
  type: "text",
883
1219
  text: `[Poll #${callNumber} — timeout at ${now} — elapsed ${WAIT_TIMEOUT_MINUTES}m — session uptime ${Math.round((Date.now() - sessionStartedAt) / 60000)}m — operator idle ${idleMinutes}m]` +
884
- (keepAliveSent ? ` Keep-alive ping sent.` : "") +
885
1220
  ` No new instructions received. ` +
886
1221
  `YOU MUST call remote_copilot_wait_for_instructions again RIGHT NOW to continue listening. ` +
887
1222
  `Do NOT summarize, stop, or say the session is idle. ` +
@@ -1174,6 +1509,267 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1174
1509
  }],
1175
1510
  };
1176
1511
  }
1512
+ // ── memory_bootstrap ────────────────────────────────────────────────────
1513
+ if (name === "memory_bootstrap") {
1514
+ const threadId = resolveThreadId(args);
1515
+ if (threadId === undefined) {
1516
+ return { content: [{ type: "text", text: "Error: No active thread. Call start_session first." + getReminders() }] };
1517
+ }
1518
+ try {
1519
+ const db = getMemoryDb();
1520
+ const briefing = assembleBootstrap(db, threadId);
1521
+ return {
1522
+ content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getReminders(threadId) }],
1523
+ };
1524
+ }
1525
+ catch (err) {
1526
+ return { content: [{ type: "text", text: `Memory bootstrap error: ${errorMessage(err)}` + getReminders(threadId) }] };
1527
+ }
1528
+ }
1529
+ // ── memory_search ───────────────────────────────────────────────────────
1530
+ if (name === "memory_search") {
1531
+ const typedArgs = (args ?? {});
1532
+ const threadId = resolveThreadId(typedArgs);
1533
+ const query = String(typedArgs.query ?? "");
1534
+ if (!query) {
1535
+ return { content: [{ type: "text", text: "Error: query is required." + getReminders(threadId) }] };
1536
+ }
1537
+ try {
1538
+ const db = getMemoryDb();
1539
+ const layers = typedArgs.layers ?? ["episodic", "semantic", "procedural"];
1540
+ const types = typedArgs.types;
1541
+ const results = [];
1542
+ if (layers.includes("semantic")) {
1543
+ const notes = searchSemanticNotes(db, query, { types, maxResults: 10 });
1544
+ if (notes.length > 0) {
1545
+ results.push("### Semantic Memory");
1546
+ for (const n of notes) {
1547
+ results.push(`- **[${n.type}]** ${n.content} _(conf: ${n.confidence}, id: ${n.noteId})_`);
1548
+ }
1549
+ }
1550
+ }
1551
+ if (layers.includes("procedural")) {
1552
+ const procs = searchProcedures(db, query, 5);
1553
+ if (procs.length > 0) {
1554
+ results.push("### Procedural Memory");
1555
+ for (const p of procs) {
1556
+ results.push(`- **${p.name}** (${p.type}): ${p.description} _(success: ${Math.round(p.successRate * 100)}%, id: ${p.procedureId})_`);
1557
+ }
1558
+ }
1559
+ }
1560
+ if (layers.includes("episodic") && threadId !== undefined) {
1561
+ const episodes = getRecentEpisodes(db, threadId, 10);
1562
+ const filtered = episodes.filter(ep => {
1563
+ const content = JSON.stringify(ep.content).toLowerCase();
1564
+ return query.toLowerCase().split(/\s+/).some(word => content.includes(word));
1565
+ });
1566
+ if (filtered.length > 0) {
1567
+ results.push("### Episodic Memory");
1568
+ for (const ep of filtered.slice(0, 5)) {
1569
+ const summary = typeof ep.content === "object" && ep.content !== null
1570
+ ? ep.content.text ?? JSON.stringify(ep.content).slice(0, 200)
1571
+ : String(ep.content).slice(0, 200);
1572
+ results.push(`- [${ep.modality}] ${summary} _(${ep.timestamp}, id: ${ep.episodeId})_`);
1573
+ }
1574
+ }
1575
+ }
1576
+ const text = results.length > 0
1577
+ ? results.join("\n")
1578
+ : `No memories found for "${query}".`;
1579
+ return { content: [{ type: "text", text: text + getReminders(threadId) }] };
1580
+ }
1581
+ catch (err) {
1582
+ return { content: [{ type: "text", text: `Memory search error: ${errorMessage(err)}` + getReminders(threadId) }] };
1583
+ }
1584
+ }
1585
+ // ── memory_save ─────────────────────────────────────────────────────────
1586
+ if (name === "memory_save") {
1587
+ const typedArgs = (args ?? {});
1588
+ const threadId = resolveThreadId(typedArgs);
1589
+ try {
1590
+ const db = getMemoryDb();
1591
+ const noteId = saveSemanticNote(db, {
1592
+ type: String(typedArgs.type ?? "fact"),
1593
+ content: String(typedArgs.content ?? ""),
1594
+ keywords: typedArgs.keywords ?? [],
1595
+ confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
1596
+ });
1597
+ return {
1598
+ content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) }],
1599
+ };
1600
+ }
1601
+ catch (err) {
1602
+ return { content: [{ type: "text", text: `Memory save error: ${errorMessage(err)}` + getReminders(threadId) }] };
1603
+ }
1604
+ }
1605
+ // ── memory_save_procedure ───────────────────────────────────────────────
1606
+ if (name === "memory_save_procedure") {
1607
+ const typedArgs = (args ?? {});
1608
+ const threadId = resolveThreadId(typedArgs);
1609
+ try {
1610
+ const db = getMemoryDb();
1611
+ const existingId = typedArgs.procedureId;
1612
+ if (existingId) {
1613
+ updateProcedure(db, existingId, {
1614
+ description: typedArgs.description,
1615
+ steps: typedArgs.steps,
1616
+ triggerConditions: typedArgs.triggerConditions,
1617
+ });
1618
+ return {
1619
+ content: [{ type: "text", text: `Updated procedure: ${existingId}` + getReminders(threadId) }],
1620
+ };
1621
+ }
1622
+ const procId = saveProcedure(db, {
1623
+ name: String(typedArgs.name ?? ""),
1624
+ type: String(typedArgs.type ?? "workflow"),
1625
+ description: String(typedArgs.description ?? ""),
1626
+ steps: typedArgs.steps,
1627
+ triggerConditions: typedArgs.triggerConditions,
1628
+ });
1629
+ return {
1630
+ content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
1631
+ };
1632
+ }
1633
+ catch (err) {
1634
+ return { content: [{ type: "text", text: `Procedure save error: ${errorMessage(err)}` + getReminders(threadId) }] };
1635
+ }
1636
+ }
1637
+ // ── memory_update ───────────────────────────────────────────────────────
1638
+ if (name === "memory_update") {
1639
+ const typedArgs = (args ?? {});
1640
+ const threadId = resolveThreadId(typedArgs);
1641
+ try {
1642
+ const db = getMemoryDb();
1643
+ const memId = String(typedArgs.memoryId ?? "");
1644
+ const action = String(typedArgs.action ?? "update");
1645
+ const reason = String(typedArgs.reason ?? "");
1646
+ if (action === "supersede" && memId.startsWith("sn_")) {
1647
+ const origRow = db.prepare("SELECT type, keywords FROM semantic_notes WHERE note_id = ?").get(memId);
1648
+ const newId = supersedeNote(db, memId, {
1649
+ type: (origRow?.type ?? "fact"),
1650
+ content: String(typedArgs.newContent ?? ""),
1651
+ keywords: origRow?.keywords ? JSON.parse(origRow.keywords) : [],
1652
+ confidence: typeof typedArgs.newConfidence === "number" ? typedArgs.newConfidence : 0.8,
1653
+ });
1654
+ return {
1655
+ content: [{ type: "text", text: `Superseded ${memId} → ${newId} (reason: ${reason})` + getReminders(threadId) }],
1656
+ };
1657
+ }
1658
+ if (memId.startsWith("sn_")) {
1659
+ const updates = {};
1660
+ if (typedArgs.newContent)
1661
+ updates.content = String(typedArgs.newContent);
1662
+ if (typeof typedArgs.newConfidence === "number")
1663
+ updates.confidence = typedArgs.newConfidence;
1664
+ updateSemanticNote(db, memId, updates);
1665
+ return {
1666
+ content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getReminders(threadId) }],
1667
+ };
1668
+ }
1669
+ if (memId.startsWith("pr_")) {
1670
+ const updates = {};
1671
+ if (typedArgs.newContent)
1672
+ updates.description = String(typedArgs.newContent);
1673
+ if (typeof typedArgs.newConfidence === "number")
1674
+ updates.confidence = typedArgs.newConfidence;
1675
+ updateProcedure(db, memId, updates);
1676
+ return {
1677
+ content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getReminders(threadId) }],
1678
+ };
1679
+ }
1680
+ return { content: [{ type: "text", text: `Unknown memory ID format: ${memId}` + getReminders(threadId) }] };
1681
+ }
1682
+ catch (err) {
1683
+ return { content: [{ type: "text", text: `Memory update error: ${errorMessage(err)}` + getReminders(threadId) }] };
1684
+ }
1685
+ }
1686
+ // ── memory_consolidate ──────────────────────────────────────────────────
1687
+ if (name === "memory_consolidate") {
1688
+ const typedArgs = (args ?? {});
1689
+ const threadId = resolveThreadId(typedArgs);
1690
+ if (threadId === undefined) {
1691
+ return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
1692
+ }
1693
+ try {
1694
+ const db = getMemoryDb();
1695
+ const report = await runIntelligentConsolidation(db, threadId);
1696
+ if (report.episodesProcessed === 0) {
1697
+ return {
1698
+ content: [{ type: "text", text: "No unconsolidated episodes. Memory is up to date." + getReminders(threadId) }],
1699
+ };
1700
+ }
1701
+ const reportLines = [
1702
+ "## Consolidation Report",
1703
+ `- Episodes processed: ${report.episodesProcessed}`,
1704
+ `- Notes created: ${report.notesCreated}`,
1705
+ `- Duration: ${report.durationMs}ms`,
1706
+ ];
1707
+ if (report.details.length > 0) {
1708
+ reportLines.push("", "### Extracted Knowledge");
1709
+ for (const d of report.details) {
1710
+ reportLines.push(`- ${d}`);
1711
+ }
1712
+ }
1713
+ return { content: [{ type: "text", text: reportLines.join("\n") + 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
  });