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/README.md +125 -70
- package/dist/index.js +624 -28
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +193 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +825 -0
- package/dist/memory.js.map +1 -0
- package/package.json +3 -1
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 {
|
|
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
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
});
|