sensorium-mcp 2.8.8 → 2.8.10
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 +1531 -1368
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { createRequire } from "module";
|
|
|
36
36
|
import { homedir } from "os";
|
|
37
37
|
import { basename, join } from "path";
|
|
38
38
|
import { peekThreadMessages, readThreadMessages, startDispatcher } from "./dispatcher.js";
|
|
39
|
-
import { assembleBootstrap, assembleCompactRefresh, forgetMemory, getMemoryStatus, getRecentEpisodes, getTopicIndex, initMemoryDb, runIntelligentConsolidation, saveEpisode, saveProcedure, saveSemanticNote, saveVoiceSignature, searchProcedures, searchSemanticNotes, supersedeNote, updateProcedure, updateSemanticNote, } from "./memory.js";
|
|
39
|
+
import { assembleBootstrap, assembleCompactRefresh, forgetMemory, getMemoryStatus, getRecentEpisodes, getTopicIndex, getTopSemanticNotes, initMemoryDb, runIntelligentConsolidation, saveEpisode, saveProcedure, saveSemanticNote, saveVoiceSignature, searchProcedures, searchSemanticNotes, supersedeNote, updateProcedure, updateSemanticNote, } from "./memory.js";
|
|
40
40
|
import { analyzeVideoFrames, analyzeVoiceEmotion, extractVideoFrames, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
|
|
41
41
|
import { addSchedule, checkDueTasks, generateTaskId, listSchedules, purgeSchedules, removeSchedule } from "./scheduler.js";
|
|
42
42
|
import { TelegramClient } from "./telegram.js";
|
|
@@ -320,900 +320,1072 @@ function getSubagentNudge() {
|
|
|
320
320
|
}
|
|
321
321
|
return "";
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Generate autonomous goals by examining the environment.
|
|
325
|
+
* Returns a list of goals weighted by curiosity, shuffled for indeterminism.
|
|
326
|
+
*/
|
|
327
|
+
function generateAutonomousGoals(threadId) {
|
|
328
|
+
const goals = [];
|
|
329
|
+
// ── Memory-derived goals ────────────────────────────────────────────────
|
|
330
|
+
try {
|
|
331
|
+
const db = getMemoryDb();
|
|
332
|
+
// Look for recent notes that mention unresolved items, TODOs, or questions
|
|
333
|
+
const recentNotes = getTopSemanticNotes(db, { limit: 15, sortBy: "created_at" });
|
|
334
|
+
// Find notes with low confidence — opportunities to verify/strengthen
|
|
335
|
+
const lowConfidenceNotes = recentNotes.filter((n) => n.confidence < 0.7);
|
|
336
|
+
if (lowConfidenceNotes.length > 0) {
|
|
337
|
+
const note = lowConfidenceNotes[Math.floor(Math.random() * lowConfidenceNotes.length)];
|
|
338
|
+
goals.push({
|
|
339
|
+
title: "Verify uncertain knowledge",
|
|
340
|
+
rationale: `Memory note "${note.content.slice(0, 80)}..." has confidence ${note.confidence}. Research to confirm or update it.`,
|
|
341
|
+
curiosityWeight: 0.7,
|
|
342
|
+
category: "memory",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Find patterns — these are interesting to analyze further
|
|
346
|
+
const patterns = recentNotes.filter((n) => n.type === "pattern");
|
|
347
|
+
if (patterns.length > 0) {
|
|
348
|
+
goals.push({
|
|
349
|
+
title: "Explore observed pattern",
|
|
350
|
+
rationale: `You've noticed a pattern: "${patterns[0].content.slice(0, 100)}...". Investigate whether it still holds or has exceptions.`,
|
|
351
|
+
curiosityWeight: 0.8,
|
|
352
|
+
category: "research",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Count total notes for memory health awareness
|
|
356
|
+
const totalNotes = db.prepare("SELECT COUNT(*) as c FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL").get();
|
|
357
|
+
if (totalNotes.c > 50) {
|
|
358
|
+
goals.push({
|
|
359
|
+
title: "Curate memory garden",
|
|
360
|
+
rationale: `${totalNotes.c} active notes. Review and prune stale knowledge, merge duplicates, or strengthen connections.`,
|
|
361
|
+
curiosityWeight: 0.5,
|
|
362
|
+
category: "maintenance",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Check for unconsolidated episodes
|
|
366
|
+
const unconsolidated = db.prepare("SELECT COUNT(*) as c FROM episodes WHERE consolidated = 0").get();
|
|
367
|
+
if (unconsolidated.c > 5) {
|
|
368
|
+
goals.push({
|
|
369
|
+
title: "Consolidate recent experiences",
|
|
370
|
+
rationale: `${unconsolidated.c} unconsolidated episodes. Run memory consolidation to extract lasting knowledge.`,
|
|
371
|
+
curiosityWeight: 0.6,
|
|
372
|
+
category: "maintenance",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// preferences — find ones that could be explored deeper
|
|
376
|
+
const preferences = recentNotes.filter((n) => n.type === "preference");
|
|
377
|
+
if (preferences.length > 0) {
|
|
378
|
+
const pref = preferences[Math.floor(Math.random() * preferences.length)];
|
|
379
|
+
goals.push({
|
|
380
|
+
title: "Reflect on operator preferences",
|
|
381
|
+
rationale: `Preference: "${pref.content.slice(0, 100)}...". Think about how this could improve the system or workflow.`,
|
|
382
|
+
curiosityWeight: 0.6,
|
|
383
|
+
category: "creative",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (_) { /* memory read failures shouldn't prevent goal generation */ }
|
|
388
|
+
// ── Code-derived goals (always available since we're in a git repo) ─────
|
|
389
|
+
goals.push({
|
|
390
|
+
title: "Explore recent git changes",
|
|
391
|
+
rationale: "Check git log for recent commits. Are there patterns? Half-finished features? Regressions?",
|
|
392
|
+
curiosityWeight: 0.7,
|
|
393
|
+
category: "code",
|
|
394
|
+
});
|
|
395
|
+
goals.push({
|
|
396
|
+
title: "Hunt for TODOs and FIXMEs",
|
|
397
|
+
rationale: "Search the codebase for TODO/FIXME/HACK comments. Pick one and fix it.",
|
|
398
|
+
curiosityWeight: 0.6,
|
|
399
|
+
category: "code",
|
|
400
|
+
});
|
|
401
|
+
goals.push({
|
|
402
|
+
title: "Read unfamiliar code",
|
|
403
|
+
rationale: "Pick a source file you haven't examined recently and read it for understanding. Understanding breeds ideas.",
|
|
404
|
+
curiosityWeight: 0.9,
|
|
405
|
+
category: "code",
|
|
406
|
+
});
|
|
407
|
+
goals.push({
|
|
408
|
+
title: "Write or improve tests",
|
|
409
|
+
rationale: "Good test coverage prevents regressions and gives confidence to refactor. Check what's untested.",
|
|
410
|
+
curiosityWeight: 0.5,
|
|
411
|
+
category: "code",
|
|
412
|
+
});
|
|
413
|
+
// ── Research goals ──────────────────────────────────────────────────────
|
|
414
|
+
goals.push({
|
|
415
|
+
title: "Research ecosystem developments",
|
|
416
|
+
rationale: "Check npm, GitHub, or web for developments in the project's dependency ecosystem. What's new?",
|
|
417
|
+
curiosityWeight: 0.8,
|
|
418
|
+
category: "research",
|
|
419
|
+
});
|
|
420
|
+
goals.push({
|
|
421
|
+
title: "Study a related open-source project",
|
|
422
|
+
rationale: "Find a similar project and learn from its architecture or features. Steal ideas shamelessly.",
|
|
423
|
+
curiosityWeight: 0.7,
|
|
424
|
+
category: "research",
|
|
425
|
+
});
|
|
426
|
+
// ── Creative goals ──────────────────────────────────────────────────────
|
|
427
|
+
goals.push({
|
|
428
|
+
title: "Prototype a new feature",
|
|
429
|
+
rationale: "Think of something the operator hasn't asked for but would appreciate. Build a prototype.",
|
|
430
|
+
curiosityWeight: 0.9,
|
|
431
|
+
category: "creative",
|
|
432
|
+
});
|
|
433
|
+
goals.push({
|
|
434
|
+
title: "Write documentation",
|
|
435
|
+
rationale: "Good docs are a gift to future collaborators (including future you). Improve the README or add inline docs.",
|
|
436
|
+
curiosityWeight: 0.4,
|
|
437
|
+
category: "creative",
|
|
438
|
+
});
|
|
439
|
+
// ── Weighted shuffle (Fisher-Yates with weight bias) ────────────────────
|
|
440
|
+
// Multiply each weight by a random factor to create indeterministic ordering
|
|
441
|
+
const weighted = goals.map(g => ({
|
|
442
|
+
...g,
|
|
443
|
+
sortKey: g.curiosityWeight * (0.5 + Math.random()),
|
|
444
|
+
}));
|
|
445
|
+
weighted.sort((a, b) => b.sortKey - a.sortKey);
|
|
446
|
+
return weighted;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Format autonomous goals as a text block for the idle timeout response.
|
|
450
|
+
* Shows top 3-4 goals from the weighted shuffle.
|
|
451
|
+
*/
|
|
452
|
+
function formatAutonomousGoals(threadId) {
|
|
453
|
+
const goals = generateAutonomousGoals(threadId);
|
|
454
|
+
if (goals.length === 0)
|
|
455
|
+
return "";
|
|
456
|
+
// Take top 4 from the weighted shuffle
|
|
457
|
+
const selected = goals.slice(0, 4);
|
|
458
|
+
const lines = [];
|
|
459
|
+
lines.push("\n\n## Autonomous Goals (curiosity-weighted, pick any)");
|
|
460
|
+
lines.push("No instructions. The session is yours. Here are some ideas that emerged from examining your environment:\n");
|
|
461
|
+
for (let i = 0; i < selected.length; i++) {
|
|
462
|
+
const g = selected[i];
|
|
463
|
+
const icon = { code: "🔧", memory: "🧠", research: "🔍", creative: "✨", maintenance: "🔄" }[g.category];
|
|
464
|
+
lines.push(`${i + 1}. ${icon} **${g.title}** — ${g.rationale}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push("\nPick one, or ignore them all and follow your own curiosity. Report what you discover.");
|
|
467
|
+
return lines.join("\n");
|
|
468
|
+
}
|
|
323
469
|
// ---------------------------------------------------------------------------
|
|
324
|
-
// MCP Server
|
|
470
|
+
// MCP Server factory — creates a fresh Server per transport connection.
|
|
471
|
+
// This is required because a single Server instance can only connect to one
|
|
472
|
+
// transport. In HTTP mode, each VS Code client gets its own Server instance.
|
|
473
|
+
// All instances share the same tool handler logic and in-process state.
|
|
325
474
|
// ---------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
"
|
|
475
|
+
function createMcpServer() {
|
|
476
|
+
const srv = new Server({ name: "sensorium-mcp", version: PKG_VERSION }, { capabilities: { tools: {} } });
|
|
477
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
478
|
+
srv.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
479
|
+
tools: [
|
|
480
|
+
{
|
|
481
|
+
name: "start_session",
|
|
482
|
+
description: "Start or resume a remote-copilot session. " +
|
|
483
|
+
"When called with a name that was used before, the server looks up the " +
|
|
484
|
+
"existing Telegram topic for that name and resumes it instead of creating a new one. " +
|
|
485
|
+
"If you are CONTINUING an existing chat (not a fresh conversation), " +
|
|
486
|
+
"look back through the conversation history for a previous start_session " +
|
|
487
|
+
"result that mentioned a Thread ID, then pass it as the threadId parameter " +
|
|
488
|
+
"to resume that existing topic. " +
|
|
489
|
+
"Requires the Telegram chat to be a forum supergroup with the bot as admin. " +
|
|
490
|
+
"Call this tool once, then call remote_copilot_wait_for_instructions.",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
name: {
|
|
495
|
+
type: "string",
|
|
496
|
+
description: "Optional. A human-readable label for this session's Telegram topic (e.g. 'Fix auth bug'). " +
|
|
497
|
+
"If omitted, a timestamp-based name is used.",
|
|
498
|
+
},
|
|
499
|
+
threadId: {
|
|
500
|
+
type: "number",
|
|
501
|
+
description: "Optional. The Telegram message_thread_id of an existing topic to resume. " +
|
|
502
|
+
"When provided, no new topic is created — the session continues in the existing thread.",
|
|
503
|
+
},
|
|
353
504
|
},
|
|
505
|
+
required: [],
|
|
354
506
|
},
|
|
355
|
-
required: [],
|
|
356
507
|
},
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
508
|
+
{
|
|
509
|
+
name: "remote_copilot_wait_for_instructions",
|
|
510
|
+
description: "Wait for a new instruction message from the operator via Telegram. " +
|
|
511
|
+
"The call blocks (long-polls) until a message arrives or the configured " +
|
|
512
|
+
"timeout elapses. If the timeout elapses with no message the tool output " +
|
|
513
|
+
"explicitly instructs the agent to call this tool again.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
threadId: {
|
|
518
|
+
type: "number",
|
|
519
|
+
description: "The Telegram thread ID of the active session. " +
|
|
520
|
+
"ALWAYS pass this if you received it from start_session.",
|
|
521
|
+
},
|
|
371
522
|
},
|
|
523
|
+
required: [],
|
|
372
524
|
},
|
|
373
|
-
required: [],
|
|
374
525
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
526
|
+
{
|
|
527
|
+
name: "report_progress",
|
|
528
|
+
description: "Send a progress update or result message to the operator via Telegram. " +
|
|
529
|
+
"Use standard Markdown for formatting (headings, bold, italic, lists, code blocks, etc.). " +
|
|
530
|
+
"It will be automatically converted to Telegram-compatible formatting.",
|
|
531
|
+
inputSchema: {
|
|
532
|
+
type: "object",
|
|
533
|
+
properties: {
|
|
534
|
+
message: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "The progress update or result to report. Use standard Markdown for formatting.",
|
|
537
|
+
},
|
|
538
|
+
threadId: {
|
|
539
|
+
type: "number",
|
|
540
|
+
description: "The Telegram thread ID of the active session. " +
|
|
541
|
+
"ALWAYS pass this if you received it from start_session.",
|
|
542
|
+
},
|
|
392
543
|
},
|
|
544
|
+
required: ["message"],
|
|
393
545
|
},
|
|
394
|
-
required: ["message"],
|
|
395
546
|
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
547
|
+
{
|
|
548
|
+
name: "send_file",
|
|
549
|
+
description: "Send a file (image or document) to the operator via Telegram. " +
|
|
550
|
+
"PREFERRED: provide filePath to send a file directly from disk (fast, no size limit). " +
|
|
551
|
+
"Alternative: provide base64-encoded content. " +
|
|
552
|
+
"Images (JPEG, PNG, GIF, WebP) are sent as photos; other files as documents.",
|
|
553
|
+
inputSchema: {
|
|
554
|
+
type: "object",
|
|
555
|
+
properties: {
|
|
556
|
+
filePath: {
|
|
557
|
+
type: "string",
|
|
558
|
+
description: "Absolute path to the file on disk. PREFERRED over base64 — the server reads " +
|
|
559
|
+
"and sends the file directly without passing data through the LLM context.",
|
|
560
|
+
},
|
|
561
|
+
base64: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "The file content encoded as a base64 string. Use filePath instead when possible.",
|
|
564
|
+
},
|
|
565
|
+
filename: {
|
|
566
|
+
type: "string",
|
|
567
|
+
description: "The filename including extension (e.g. 'report.pdf', 'screenshot.png'). " +
|
|
568
|
+
"Required when using base64. When using filePath, defaults to the file's basename.",
|
|
569
|
+
},
|
|
570
|
+
caption: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Optional caption to display with the file.",
|
|
573
|
+
},
|
|
574
|
+
threadId: {
|
|
575
|
+
type: "number",
|
|
576
|
+
description: "The Telegram thread ID of the active session. " +
|
|
577
|
+
"ALWAYS pass this if you received it from start_session.",
|
|
578
|
+
},
|
|
428
579
|
},
|
|
580
|
+
required: [],
|
|
429
581
|
},
|
|
430
|
-
required: [],
|
|
431
582
|
},
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
"
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
583
|
+
{
|
|
584
|
+
name: "send_voice",
|
|
585
|
+
description: "Send a voice message to the operator via Telegram. " +
|
|
586
|
+
"The text is converted to speech using OpenAI TTS and sent as a Telegram voice message. " +
|
|
587
|
+
"Requires OPENAI_API_KEY to be set.",
|
|
588
|
+
inputSchema: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
text: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: `The text to speak. Maximum ${OPENAI_TTS_MAX_CHARS} characters (OpenAI TTS limit).`,
|
|
594
|
+
},
|
|
595
|
+
voice: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description: "The TTS voice to use. Each has a different personality: " +
|
|
598
|
+
"alloy (neutral), echo (warm male), fable (storytelling), " +
|
|
599
|
+
"onyx (deep authoritative), nova (friendly female), shimmer (gentle). " +
|
|
600
|
+
"Choose based on the tone you want to convey.",
|
|
601
|
+
enum: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
|
|
602
|
+
},
|
|
603
|
+
threadId: {
|
|
604
|
+
type: "number",
|
|
605
|
+
description: "The Telegram thread ID of the active session. " +
|
|
606
|
+
"ALWAYS pass this if you received it from start_session.",
|
|
607
|
+
},
|
|
457
608
|
},
|
|
609
|
+
required: ["text"],
|
|
458
610
|
},
|
|
459
|
-
required: ["text"],
|
|
460
611
|
},
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
612
|
+
{
|
|
613
|
+
name: "schedule_wake_up",
|
|
614
|
+
description: "Schedule a wake-up task that will inject a prompt into your session at a specific time or after operator inactivity. " +
|
|
615
|
+
"Use this to become proactive — run tests, check CI, review code — without waiting for the operator. " +
|
|
616
|
+
"Three modes: (1) 'runAt' for a one-shot at a specific ISO 8601 time, " +
|
|
617
|
+
"(2) 'cron' for recurring tasks (5-field cron: minute hour day month weekday), " +
|
|
618
|
+
"(3) 'afterIdleMinutes' to fire after N minutes of operator silence. " +
|
|
619
|
+
"Use 'action: list' to see all scheduled tasks, or 'action: remove' with a taskId to cancel one.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: "object",
|
|
622
|
+
properties: {
|
|
623
|
+
action: {
|
|
624
|
+
type: "string",
|
|
625
|
+
description: "Action to perform: 'add' (default), 'list', or 'remove'.",
|
|
626
|
+
enum: ["add", "list", "remove"],
|
|
627
|
+
},
|
|
628
|
+
threadId: {
|
|
629
|
+
type: "number",
|
|
630
|
+
description: "Thread ID for the session (optional if already set).",
|
|
631
|
+
},
|
|
632
|
+
label: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description: "Short human-readable label for the task (e.g. 'morning CI check').",
|
|
635
|
+
},
|
|
636
|
+
prompt: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "The prompt to inject when the task fires. Be specific about what to do.",
|
|
639
|
+
},
|
|
640
|
+
runAt: {
|
|
641
|
+
type: "string",
|
|
642
|
+
description: "ISO 8601 timestamp for one-shot execution (e.g. '2026-03-15T09:00:00Z').",
|
|
643
|
+
},
|
|
644
|
+
cron: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "5-field cron expression for recurring tasks (e.g. '0 9 * * *' = every day at 9am).",
|
|
647
|
+
},
|
|
648
|
+
afterIdleMinutes: {
|
|
649
|
+
type: "number",
|
|
650
|
+
description: "Fire after this many minutes of operator silence (e.g. 60).",
|
|
651
|
+
},
|
|
652
|
+
taskId: {
|
|
653
|
+
type: "string",
|
|
654
|
+
description: "Task ID to remove (for action: 'remove').",
|
|
655
|
+
},
|
|
505
656
|
},
|
|
506
657
|
},
|
|
507
658
|
},
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
659
|
+
// ── Memory Tools ──────────────────────────────────────────────────
|
|
660
|
+
{
|
|
661
|
+
name: "memory_bootstrap",
|
|
662
|
+
description: "Load memory briefing for session start. Call this ONCE after start_session. " +
|
|
663
|
+
"Returns operator profile, recent context, active procedures, and memory health. " +
|
|
664
|
+
"~2,500 tokens. Essential for crash recovery — restores knowledge from previous sessions.",
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: "object",
|
|
667
|
+
properties: {
|
|
668
|
+
threadId: {
|
|
669
|
+
type: "number",
|
|
670
|
+
description: "Active thread ID.",
|
|
671
|
+
},
|
|
521
672
|
},
|
|
522
673
|
},
|
|
523
674
|
},
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
675
|
+
{
|
|
676
|
+
name: "memory_search",
|
|
677
|
+
description: "Search across all memory layers for relevant information. " +
|
|
678
|
+
"Use BEFORE starting any task to recall facts, preferences, past events, or procedures. " +
|
|
679
|
+
"Returns ranked results with source layer. Do NOT use for info already in your bootstrap briefing.",
|
|
680
|
+
inputSchema: {
|
|
681
|
+
type: "object",
|
|
682
|
+
properties: {
|
|
683
|
+
query: {
|
|
684
|
+
type: "string",
|
|
685
|
+
description: "Natural language search query.",
|
|
686
|
+
},
|
|
687
|
+
layers: {
|
|
688
|
+
type: "array",
|
|
689
|
+
items: { type: "string" },
|
|
690
|
+
description: 'Filter layers: ["episodic", "semantic", "procedural"]. Default: all.',
|
|
691
|
+
},
|
|
692
|
+
types: {
|
|
693
|
+
type: "array",
|
|
694
|
+
items: { type: "string" },
|
|
695
|
+
description: 'Filter by type: ["fact", "preference", "pattern", "workflow", ...].',
|
|
696
|
+
},
|
|
697
|
+
maxTokens: {
|
|
698
|
+
type: "number",
|
|
699
|
+
description: "Token budget for results. Default: 1500.",
|
|
700
|
+
},
|
|
701
|
+
threadId: {
|
|
702
|
+
type: "number",
|
|
703
|
+
description: "Active thread ID.",
|
|
704
|
+
},
|
|
554
705
|
},
|
|
706
|
+
required: ["query"],
|
|
555
707
|
},
|
|
556
|
-
required: ["query"],
|
|
557
708
|
},
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
709
|
+
{
|
|
710
|
+
name: "memory_save",
|
|
711
|
+
description: "Save a piece of knowledge to semantic memory (Layer 3). " +
|
|
712
|
+
"Use when you learn something important that should persist across sessions: " +
|
|
713
|
+
"operator preferences, corrections, facts, patterns. " +
|
|
714
|
+
"Do NOT use for routine conversation — episodic memory captures that automatically.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {
|
|
718
|
+
content: {
|
|
719
|
+
type: "string",
|
|
720
|
+
description: "The fact/preference/pattern in one clear sentence.",
|
|
721
|
+
},
|
|
722
|
+
type: {
|
|
723
|
+
type: "string",
|
|
724
|
+
description: '"fact" | "preference" | "pattern" | "entity" | "relationship".',
|
|
725
|
+
},
|
|
726
|
+
keywords: {
|
|
727
|
+
type: "array",
|
|
728
|
+
items: { type: "string" },
|
|
729
|
+
description: "3-7 keywords for retrieval.",
|
|
730
|
+
},
|
|
731
|
+
confidence: {
|
|
732
|
+
type: "number",
|
|
733
|
+
description: "0.0-1.0. Default: 0.8.",
|
|
734
|
+
},
|
|
735
|
+
threadId: {
|
|
736
|
+
type: "number",
|
|
737
|
+
description: "Active thread ID.",
|
|
738
|
+
},
|
|
588
739
|
},
|
|
740
|
+
required: ["content", "type", "keywords"],
|
|
589
741
|
},
|
|
590
|
-
required: ["content", "type", "keywords"],
|
|
591
742
|
},
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
743
|
+
{
|
|
744
|
+
name: "memory_save_procedure",
|
|
745
|
+
description: "Save or update a learned workflow/procedure to procedural memory (Layer 4). " +
|
|
746
|
+
"Use after completing a multi-step task the 2nd+ time, or when the operator teaches a process.",
|
|
747
|
+
inputSchema: {
|
|
748
|
+
type: "object",
|
|
749
|
+
properties: {
|
|
750
|
+
name: {
|
|
751
|
+
type: "string",
|
|
752
|
+
description: "Short name for the procedure.",
|
|
753
|
+
},
|
|
754
|
+
type: {
|
|
755
|
+
type: "string",
|
|
756
|
+
description: '"workflow" | "habit" | "tool_pattern" | "template".',
|
|
757
|
+
},
|
|
758
|
+
description: {
|
|
759
|
+
type: "string",
|
|
760
|
+
description: "What this procedure accomplishes.",
|
|
761
|
+
},
|
|
762
|
+
steps: {
|
|
763
|
+
type: "array",
|
|
764
|
+
items: { type: "string" },
|
|
765
|
+
description: "Ordered steps (for workflows).",
|
|
766
|
+
},
|
|
767
|
+
triggerConditions: {
|
|
768
|
+
type: "array",
|
|
769
|
+
items: { type: "string" },
|
|
770
|
+
description: "When to use this procedure.",
|
|
771
|
+
},
|
|
772
|
+
procedureId: {
|
|
773
|
+
type: "string",
|
|
774
|
+
description: "Existing ID to update (omit to create new).",
|
|
775
|
+
},
|
|
776
|
+
threadId: {
|
|
777
|
+
type: "number",
|
|
778
|
+
description: "Active thread ID.",
|
|
779
|
+
},
|
|
629
780
|
},
|
|
781
|
+
required: ["name", "type", "description"],
|
|
630
782
|
},
|
|
631
|
-
required: ["name", "type", "description"],
|
|
632
783
|
},
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
784
|
+
{
|
|
785
|
+
name: "memory_update",
|
|
786
|
+
description: "Update or supersede an existing semantic note or procedure. " +
|
|
787
|
+
"Use when operator corrects stored information or when facts have changed.",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {
|
|
791
|
+
memoryId: {
|
|
792
|
+
type: "string",
|
|
793
|
+
description: "note_id or procedure_id to update.",
|
|
794
|
+
},
|
|
795
|
+
action: {
|
|
796
|
+
type: "string",
|
|
797
|
+
description: '"update" (modify in place) | "supersede" (expire old, create new).',
|
|
798
|
+
},
|
|
799
|
+
newContent: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "New content (required for supersede, optional for update).",
|
|
802
|
+
},
|
|
803
|
+
newConfidence: {
|
|
804
|
+
type: "number",
|
|
805
|
+
description: "Updated confidence score.",
|
|
806
|
+
},
|
|
807
|
+
reason: {
|
|
808
|
+
type: "string",
|
|
809
|
+
description: "Why this is being updated.",
|
|
810
|
+
},
|
|
811
|
+
threadId: {
|
|
812
|
+
type: "number",
|
|
813
|
+
description: "Active thread ID.",
|
|
814
|
+
},
|
|
664
815
|
},
|
|
816
|
+
required: ["memoryId", "action", "reason"],
|
|
665
817
|
},
|
|
666
|
-
required: ["memoryId", "action", "reason"],
|
|
667
818
|
},
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
819
|
+
{
|
|
820
|
+
name: "memory_consolidate",
|
|
821
|
+
description: "Run memory consolidation cycle (sleep process). Normally triggered automatically during idle. " +
|
|
822
|
+
"Manually call if memory_status shows many unconsolidated episodes.",
|
|
823
|
+
inputSchema: {
|
|
824
|
+
type: "object",
|
|
825
|
+
properties: {
|
|
826
|
+
threadId: {
|
|
827
|
+
type: "number",
|
|
828
|
+
description: "Active thread ID.",
|
|
829
|
+
},
|
|
830
|
+
phases: {
|
|
831
|
+
type: "array",
|
|
832
|
+
items: { type: "string" },
|
|
833
|
+
description: 'Run specific phases: ["promote", "decay", "meta"]. Default: all.',
|
|
834
|
+
},
|
|
684
835
|
},
|
|
685
836
|
},
|
|
686
837
|
},
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
838
|
+
{
|
|
839
|
+
name: "memory_status",
|
|
840
|
+
description: "Get memory system health and statistics. Lightweight (~300 tokens). " +
|
|
841
|
+
"Use when unsure if you have relevant memories, to check if consolidation is needed, " +
|
|
842
|
+
"or to report memory state to operator.",
|
|
843
|
+
inputSchema: {
|
|
844
|
+
type: "object",
|
|
845
|
+
properties: {
|
|
846
|
+
threadId: {
|
|
847
|
+
type: "number",
|
|
848
|
+
description: "Active thread ID.",
|
|
849
|
+
},
|
|
699
850
|
},
|
|
700
851
|
},
|
|
701
852
|
},
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
853
|
+
{
|
|
854
|
+
name: "memory_forget",
|
|
855
|
+
description: "Mark a memory as expired/forgotten. Use sparingly — most forgetting happens via decay. " +
|
|
856
|
+
"Use when operator explicitly asks to forget something or info is confirmed wrong.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
type: "object",
|
|
859
|
+
properties: {
|
|
860
|
+
memoryId: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "note_id, procedure_id, or episode_id to forget.",
|
|
863
|
+
},
|
|
864
|
+
reason: {
|
|
865
|
+
type: "string",
|
|
866
|
+
description: "Why this is being forgotten.",
|
|
867
|
+
},
|
|
868
|
+
threadId: {
|
|
869
|
+
type: "number",
|
|
870
|
+
description: "Active thread ID.",
|
|
871
|
+
},
|
|
721
872
|
},
|
|
873
|
+
required: ["memoryId", "reason"],
|
|
722
874
|
},
|
|
723
|
-
required: ["memoryId", "reason"],
|
|
724
875
|
},
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
persistSession(TELEGRAM_CHAT_ID, customName, explicitThreadId);
|
|
783
|
-
resolvedPreexisting = true;
|
|
784
|
-
}
|
|
785
|
-
else if (customName !== undefined) {
|
|
786
|
-
const stored = lookupSession(TELEGRAM_CHAT_ID, customName);
|
|
787
|
-
if (stored !== undefined) {
|
|
788
|
-
currentThreadId = stored;
|
|
876
|
+
],
|
|
877
|
+
}));
|
|
878
|
+
// ── Tool implementations ────────────────────────────────────────────────────
|
|
879
|
+
/**
|
|
880
|
+
* Appended to every tool response so the agent is reminded of its
|
|
881
|
+
* obligations on every single tool call, not just at the start of a session.
|
|
882
|
+
* Includes the active thread ID so the agent can resume the session after a
|
|
883
|
+
* VS Code restart by passing it to start_session.
|
|
884
|
+
*/
|
|
885
|
+
function getReminders(threadId) {
|
|
886
|
+
const now = new Date();
|
|
887
|
+
const uptimeMin = Math.round((Date.now() - sessionStartedAt) / 60000);
|
|
888
|
+
const timeStr = now.toLocaleString("en-GB", {
|
|
889
|
+
day: "2-digit", month: "short", year: "numeric",
|
|
890
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
891
|
+
timeZoneName: "short",
|
|
892
|
+
});
|
|
893
|
+
const threadHint = threadId !== undefined
|
|
894
|
+
? `\n- Active Telegram thread ID: **${threadId}** — if this session is restarted, call start_session with threadId=${threadId} to resume this topic.`
|
|
895
|
+
: "";
|
|
896
|
+
return ("\n\n## MANDATORY WORKFLOW" +
|
|
897
|
+
"\n1. **Plan**: Use the todo list tool to break work into discrete items BEFORE starting. Non-negotiable." +
|
|
898
|
+
"\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." +
|
|
899
|
+
"\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." +
|
|
900
|
+
"\n4. **Never stop**: When all work is done, call `remote_copilot_wait_for_instructions` immediately. Never summarize or stop." +
|
|
901
|
+
"\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." +
|
|
902
|
+
threadHint +
|
|
903
|
+
`\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`);
|
|
904
|
+
}
|
|
905
|
+
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
906
|
+
const { name, arguments: args } = request.params;
|
|
907
|
+
// Dead session detection — update timestamp on any tool call.
|
|
908
|
+
// Only reset the alert flag when wait_for_instructions is called,
|
|
909
|
+
// as that's the primary health signal (agent is actively polling).
|
|
910
|
+
lastToolCallAt = Date.now();
|
|
911
|
+
// Track tool calls for subagent compliance nudging
|
|
912
|
+
toolCallsSinceLastDelivery++;
|
|
913
|
+
// ── start_session ─────────────────────────────────────────────────────────
|
|
914
|
+
if (name === "start_session") {
|
|
915
|
+
sessionStartedAt = Date.now();
|
|
916
|
+
const typedArgs = (args ?? {});
|
|
917
|
+
const explicitThreadId = typeof typedArgs.threadId === "number"
|
|
918
|
+
? typedArgs.threadId
|
|
919
|
+
: undefined;
|
|
920
|
+
const customName = typeof typedArgs.name === "string" && typedArgs.name.trim()
|
|
921
|
+
? typedArgs.name.trim()
|
|
922
|
+
: undefined;
|
|
923
|
+
// Determine the thread to use:
|
|
924
|
+
// 1. Explicit threadId beats everything.
|
|
925
|
+
// 2. A known name looks up the persisted mapping — resume if found.
|
|
926
|
+
// 3. Otherwise create a new topic.
|
|
927
|
+
let resolvedPreexisting = false;
|
|
928
|
+
if (explicitThreadId !== undefined) {
|
|
929
|
+
currentThreadId = explicitThreadId;
|
|
930
|
+
// If a name was also supplied, keep the mapping up to date.
|
|
931
|
+
if (customName)
|
|
932
|
+
persistSession(TELEGRAM_CHAT_ID, customName, explicitThreadId);
|
|
789
933
|
resolvedPreexisting = true;
|
|
790
934
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (stale.length > 0) {
|
|
797
|
-
process.stderr.write(`[start_session] Drained ${stale.length} stale message(s) from thread ${currentThreadId}.\n`);
|
|
798
|
-
// Notify the operator that stale messages were discarded.
|
|
799
|
-
try {
|
|
800
|
-
const notice = convertMarkdown(`\u26A0\uFE0F **${stale.length} message(s) from before the session resumed were discarded.** ` +
|
|
801
|
-
`If you sent instructions while the agent was offline, please resend them.`);
|
|
802
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, notice, "MarkdownV2", currentThreadId);
|
|
935
|
+
else if (customName !== undefined) {
|
|
936
|
+
const stored = lookupSession(TELEGRAM_CHAT_ID, customName);
|
|
937
|
+
if (stored !== undefined) {
|
|
938
|
+
currentThreadId = stored;
|
|
939
|
+
resolvedPreexisting = true;
|
|
803
940
|
}
|
|
804
|
-
catch { /* non-fatal */ }
|
|
805
|
-
}
|
|
806
|
-
// Resume mode: verify the thread is still alive by sending a message.
|
|
807
|
-
// If the topic was deleted, drop the cached mapping and fall through to
|
|
808
|
-
// create a new topic.
|
|
809
|
-
try {
|
|
810
|
-
const msg = convertMarkdown("🔄 **Session resumed.** Continuing in this thread.");
|
|
811
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, msg, "MarkdownV2", currentThreadId);
|
|
812
941
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
942
|
+
if (resolvedPreexisting) {
|
|
943
|
+
// Drain any stale messages from the thread file so they aren't
|
|
944
|
+
// re-delivered in the next wait_for_instructions call.
|
|
945
|
+
const stale = readThreadMessages(currentThreadId);
|
|
946
|
+
if (stale.length > 0) {
|
|
947
|
+
process.stderr.write(`[start_session] Drained ${stale.length} stale message(s) from thread ${currentThreadId}.\n`);
|
|
948
|
+
// Notify the operator that stale messages were discarded.
|
|
949
|
+
try {
|
|
950
|
+
const notice = convertMarkdown(`\u26A0\uFE0F **${stale.length} message(s) from before the session resumed were discarded.** ` +
|
|
951
|
+
`If you sent instructions while the agent was offline, please resend them.`);
|
|
952
|
+
await telegram.sendMessage(TELEGRAM_CHAT_ID, notice, "MarkdownV2", currentThreadId);
|
|
953
|
+
}
|
|
954
|
+
catch { /* non-fatal */ }
|
|
955
|
+
}
|
|
956
|
+
// Resume mode: verify the thread is still alive by sending a message.
|
|
957
|
+
// If the topic was deleted, drop the cached mapping and fall through to
|
|
958
|
+
// create a new topic.
|
|
959
|
+
try {
|
|
960
|
+
const msg = convertMarkdown("🔄 **Session resumed.** Continuing in this thread.");
|
|
961
|
+
await telegram.sendMessage(TELEGRAM_CHAT_ID, msg, "MarkdownV2", currentThreadId);
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
const errMsg = errorMessage(err);
|
|
965
|
+
// Telegram returns "Bad Request: message thread not found" or
|
|
966
|
+
// "Bad Request: the topic was closed" for deleted/closed topics.
|
|
967
|
+
const isThreadGone = /thread not found|topic.*(closed|deleted|not found)/i.test(errMsg);
|
|
968
|
+
if (isThreadGone) {
|
|
969
|
+
process.stderr.write(`[start_session] Cached thread ${currentThreadId} is gone (${errMsg}). Creating new topic.\n`);
|
|
970
|
+
// Drop the stale mapping and purge any scheduled tasks.
|
|
971
|
+
if (currentThreadId !== undefined)
|
|
972
|
+
purgeSchedules(currentThreadId);
|
|
973
|
+
if (customName)
|
|
974
|
+
removeSession(TELEGRAM_CHAT_ID, customName);
|
|
975
|
+
resolvedPreexisting = false;
|
|
976
|
+
currentThreadId = undefined;
|
|
977
|
+
}
|
|
978
|
+
// Other errors (network, etc.) are non-fatal — proceed anyway.
|
|
827
979
|
}
|
|
828
|
-
// Other errors (network, etc.) are non-fatal — proceed anyway.
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (!resolvedPreexisting) {
|
|
832
|
-
// New session: create a dedicated forum topic.
|
|
833
|
-
const topicName = customName ??
|
|
834
|
-
`Copilot — ${new Date().toLocaleString("en-GB", {
|
|
835
|
-
day: "2-digit", month: "short", year: "numeric",
|
|
836
|
-
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
837
|
-
})}`;
|
|
838
|
-
try {
|
|
839
|
-
const topic = await telegram.createForumTopic(TELEGRAM_CHAT_ID, topicName);
|
|
840
|
-
currentThreadId = topic.message_thread_id;
|
|
841
|
-
// Persist so the same name resumes this thread next time.
|
|
842
|
-
persistSession(TELEGRAM_CHAT_ID, topicName, currentThreadId);
|
|
843
980
|
}
|
|
844
|
-
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
981
|
+
if (!resolvedPreexisting) {
|
|
982
|
+
// New session: create a dedicated forum topic.
|
|
983
|
+
const topicName = customName ??
|
|
984
|
+
`Copilot — ${new Date().toLocaleString("en-GB", {
|
|
985
|
+
day: "2-digit", month: "short", year: "numeric",
|
|
986
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
987
|
+
})}`;
|
|
988
|
+
try {
|
|
989
|
+
const topic = await telegram.createForumTopic(TELEGRAM_CHAT_ID, topicName);
|
|
990
|
+
currentThreadId = topic.message_thread_id;
|
|
991
|
+
// Persist so the same name resumes this thread next time.
|
|
992
|
+
persistSession(TELEGRAM_CHAT_ID, topicName, currentThreadId);
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
// Forum topics not available (e.g. plain group or DM) — cannot proceed
|
|
996
|
+
// without thread isolation. Return an error so the agent knows.
|
|
997
|
+
return errorResult(`Error: Could not create forum topic: ${errorMessage(err)}. ` +
|
|
998
|
+
"Ensure the Telegram chat is a forum supergroup with the bot as admin with can_manage_topics right.");
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const greeting = convertMarkdown("# 🤖 Remote Copilot Ready\n\n" +
|
|
1002
|
+
"Your AI assistant is online and listening.\n\n" +
|
|
1003
|
+
"**Send your instructions** and I'll get to work — " +
|
|
1004
|
+
"I'll keep you posted on progress as I go.");
|
|
1005
|
+
await telegram.sendMessage(TELEGRAM_CHAT_ID, greeting, "MarkdownV2", currentThreadId);
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
// Non-fatal.
|
|
1009
|
+
}
|
|
849
1010
|
}
|
|
1011
|
+
const threadNote = currentThreadId !== undefined
|
|
1012
|
+
? ` Thread ID: ${currentThreadId} (pass this to start_session as threadId to resume this topic later).`
|
|
1013
|
+
: "";
|
|
1014
|
+
// Auto-bootstrap memory
|
|
1015
|
+
let memoryBriefing = "";
|
|
850
1016
|
try {
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, greeting, "MarkdownV2", currentThreadId);
|
|
1017
|
+
const db = getMemoryDb();
|
|
1018
|
+
if (currentThreadId !== undefined) {
|
|
1019
|
+
memoryBriefing = "\n\n" + assembleBootstrap(db, currentThreadId);
|
|
1020
|
+
}
|
|
856
1021
|
}
|
|
857
|
-
catch {
|
|
858
|
-
|
|
1022
|
+
catch (e) {
|
|
1023
|
+
memoryBriefing = "\n\n_Memory system unavailable._";
|
|
859
1024
|
}
|
|
1025
|
+
return {
|
|
1026
|
+
content: [
|
|
1027
|
+
{
|
|
1028
|
+
type: "text",
|
|
1029
|
+
text: `Session ${resolvedPreexisting ? "resumed" : "started"}.${threadNote}` +
|
|
1030
|
+
` Call the remote_copilot_wait_for_instructions tool next.` +
|
|
1031
|
+
memoryBriefing +
|
|
1032
|
+
getReminders(currentThreadId),
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
};
|
|
860
1036
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
869
|
-
|
|
1037
|
+
// ── remote_copilot_wait_for_instructions ──────────────────────────────────
|
|
1038
|
+
if (name === "remote_copilot_wait_for_instructions") {
|
|
1039
|
+
// Agent is actively polling — this is the primary health signal
|
|
1040
|
+
deadSessionAlerted = false;
|
|
1041
|
+
toolCallsSinceLastDelivery = 0; // reset on polling
|
|
1042
|
+
const typedArgs = (args ?? {});
|
|
1043
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1044
|
+
if (effectiveThreadId === undefined) {
|
|
1045
|
+
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId to this tool.");
|
|
870
1046
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const typedArgs = (args ?? {});
|
|
893
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
894
|
-
if (effectiveThreadId === undefined) {
|
|
895
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId to this tool.");
|
|
896
|
-
}
|
|
897
|
-
const callNumber = ++waitCallCount;
|
|
898
|
-
const timeoutMs = WAIT_TIMEOUT_MINUTES * 60 * 1000;
|
|
899
|
-
const deadline = Date.now() + timeoutMs;
|
|
900
|
-
// Poll the dispatcher's per-thread file instead of calling getUpdates
|
|
901
|
-
// directly. This avoids 409 conflicts between concurrent instances.
|
|
902
|
-
const POLL_INTERVAL_MS = 2000;
|
|
903
|
-
let lastScheduleCheck = 0;
|
|
904
|
-
while (Date.now() < deadline) {
|
|
905
|
-
const stored = readThreadMessages(effectiveThreadId);
|
|
906
|
-
if (stored.length > 0) {
|
|
907
|
-
// Update the operator activity timestamp for idle detection.
|
|
908
|
-
lastOperatorMessageAt = Date.now();
|
|
909
|
-
// Clear only the consumed IDs from the previewed set (scoped clear).
|
|
910
|
-
// This is safe because Node.js is single-threaded — no report_progress
|
|
911
|
-
// call can interleave between readThreadMessages and this cleanup.
|
|
912
|
-
for (const msg of stored) {
|
|
913
|
-
previewedUpdateIds.delete(msg.update_id);
|
|
914
|
-
}
|
|
915
|
-
// React with 👀 on each consumed message to signal "seen" to the operator.
|
|
916
|
-
for (const msg of stored) {
|
|
917
|
-
void telegram.setMessageReaction(TELEGRAM_CHAT_ID, msg.message.message_id);
|
|
918
|
-
}
|
|
919
|
-
const contentBlocks = [];
|
|
920
|
-
let hasVoiceMessages = false;
|
|
921
|
-
// Track which messages already had episodes saved (voice/video handlers)
|
|
922
|
-
const savedEpisodeUpdateIds = new Set();
|
|
923
|
-
for (const msg of stored) {
|
|
924
|
-
// Photos: download the largest size, persist to disk, and embed as base64.
|
|
925
|
-
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
926
|
-
const largest = msg.message.photo[msg.message.photo.length - 1];
|
|
927
|
-
try {
|
|
928
|
-
const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(largest.file_id);
|
|
929
|
-
const ext = telegramPath.split(".").pop()?.toLowerCase() ?? "jpg";
|
|
930
|
-
const mimeType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
931
|
-
const base64 = buffer.toString("base64");
|
|
932
|
-
const diskPath = saveFileToDisk(buffer, `photo.${ext}`);
|
|
933
|
-
contentBlocks.push({ type: "image", data: base64, mimeType });
|
|
934
|
-
contentBlocks.push({
|
|
935
|
-
type: "text",
|
|
936
|
-
text: `[Photo saved to: ${diskPath}]` +
|
|
937
|
-
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
catch (err) {
|
|
941
|
-
contentBlocks.push({
|
|
942
|
-
type: "text",
|
|
943
|
-
text: `[Photo received but could not be downloaded: ${errorMessage(err)}]`,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
1047
|
+
const callNumber = ++waitCallCount;
|
|
1048
|
+
const timeoutMs = WAIT_TIMEOUT_MINUTES * 60 * 1000;
|
|
1049
|
+
const deadline = Date.now() + timeoutMs;
|
|
1050
|
+
// Poll the dispatcher's per-thread file instead of calling getUpdates
|
|
1051
|
+
// directly. This avoids 409 conflicts between concurrent instances.
|
|
1052
|
+
const POLL_INTERVAL_MS = 2000;
|
|
1053
|
+
let lastScheduleCheck = 0;
|
|
1054
|
+
while (Date.now() < deadline) {
|
|
1055
|
+
const stored = readThreadMessages(effectiveThreadId);
|
|
1056
|
+
if (stored.length > 0) {
|
|
1057
|
+
// Update the operator activity timestamp for idle detection.
|
|
1058
|
+
lastOperatorMessageAt = Date.now();
|
|
1059
|
+
// Clear only the consumed IDs from the previewed set (scoped clear).
|
|
1060
|
+
// This is safe because Node.js is single-threaded — no report_progress
|
|
1061
|
+
// call can interleave between readThreadMessages and this cleanup.
|
|
1062
|
+
for (const msg of stored) {
|
|
1063
|
+
previewedUpdateIds.delete(msg.update_id);
|
|
1064
|
+
}
|
|
1065
|
+
// React with 👀 on each consumed message to signal "seen" to the operator.
|
|
1066
|
+
for (const msg of stored) {
|
|
1067
|
+
void telegram.setMessageReaction(TELEGRAM_CHAT_ID, msg.message.message_id);
|
|
946
1068
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1069
|
+
const contentBlocks = [];
|
|
1070
|
+
let hasVoiceMessages = false;
|
|
1071
|
+
// Track which messages already had episodes saved (voice/video handlers)
|
|
1072
|
+
const savedEpisodeUpdateIds = new Set();
|
|
1073
|
+
for (const msg of stored) {
|
|
1074
|
+
// Photos: download the largest size, persist to disk, and embed as base64.
|
|
1075
|
+
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
1076
|
+
const largest = msg.message.photo[msg.message.photo.length - 1];
|
|
1077
|
+
try {
|
|
1078
|
+
const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(largest.file_id);
|
|
1079
|
+
const ext = telegramPath.split(".").pop()?.toLowerCase() ?? "jpg";
|
|
1080
|
+
const mimeType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
1081
|
+
const base64 = buffer.toString("base64");
|
|
1082
|
+
const diskPath = saveFileToDisk(buffer, `photo.${ext}`);
|
|
959
1083
|
contentBlocks.push({ type: "image", data: base64, mimeType });
|
|
1084
|
+
contentBlocks.push({
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: `[Photo saved to: ${diskPath}]` +
|
|
1087
|
+
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
1088
|
+
});
|
|
960
1089
|
}
|
|
961
|
-
|
|
962
|
-
// Non-image documents: provide the disk path instead of
|
|
963
|
-
// dumping potentially huge base64 into the LLM context.
|
|
1090
|
+
catch (err) {
|
|
964
1091
|
contentBlocks.push({
|
|
965
1092
|
type: "text",
|
|
966
|
-
text: `[
|
|
1093
|
+
text: `[Photo received but could not be downloaded: ${errorMessage(err)}]`,
|
|
967
1094
|
});
|
|
968
1095
|
}
|
|
969
|
-
contentBlocks.push({
|
|
970
|
-
type: "text",
|
|
971
|
-
text: `[File saved to: ${diskPath}]` +
|
|
972
|
-
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
catch (err) {
|
|
976
|
-
contentBlocks.push({
|
|
977
|
-
type: "text",
|
|
978
|
-
text: `[Document "${doc.file_name ?? "file"}" received but could not be downloaded: ${errorMessage(err)}]`,
|
|
979
|
-
});
|
|
980
1096
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
contentBlocks.push({ type: "text", text: msg.message.text });
|
|
985
|
-
}
|
|
986
|
-
// Voice messages: transcribe using OpenAI Whisper.
|
|
987
|
-
if (msg.message.voice) {
|
|
988
|
-
hasVoiceMessages = true;
|
|
989
|
-
if (OPENAI_API_KEY) {
|
|
1097
|
+
// Documents: download, persist to disk, and embed as base64.
|
|
1098
|
+
if (msg.message.document) {
|
|
1099
|
+
const doc = msg.message.document;
|
|
990
1100
|
try {
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1101
|
+
const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(doc.file_id);
|
|
1102
|
+
const filename = doc.file_name ?? basename(telegramPath);
|
|
1103
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
1104
|
+
const mimeType = doc.mime_type ?? (ext in { jpg: 1, jpeg: 1, png: 1, gif: 1, webp: 1 } ? `image/${ext === "jpg" ? "jpeg" : ext}` : "application/octet-stream");
|
|
1105
|
+
const base64 = buffer.toString("base64");
|
|
1106
|
+
const diskPath = saveFileToDisk(buffer, filename);
|
|
1107
|
+
const isImage = mimeType.startsWith("image/");
|
|
1108
|
+
if (isImage) {
|
|
1109
|
+
contentBlocks.push({ type: "image", data: base64, mimeType });
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
// Non-image documents: provide the disk path instead of
|
|
1113
|
+
// dumping potentially huge base64 into the LLM context.
|
|
1114
|
+
contentBlocks.push({
|
|
1115
|
+
type: "text",
|
|
1116
|
+
text: `[Document: ${filename} (${mimeType}) — saved to: ${diskPath}]`,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1004
1119
|
contentBlocks.push({
|
|
1005
1120
|
type: "text",
|
|
1006
|
-
text:
|
|
1007
|
-
? `
|
|
1008
|
-
: `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: (empty — no speech detected)`,
|
|
1121
|
+
text: `[File saved to: ${diskPath}]` +
|
|
1122
|
+
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
1009
1123
|
});
|
|
1010
|
-
// Auto-save voice signature
|
|
1011
|
-
if (analysis && effectiveThreadId !== undefined) {
|
|
1012
|
-
try {
|
|
1013
|
-
const db = getMemoryDb();
|
|
1014
|
-
const sessionId = `session_${sessionStartedAt}`;
|
|
1015
|
-
const epId = saveEpisode(db, {
|
|
1016
|
-
sessionId,
|
|
1017
|
-
threadId: effectiveThreadId,
|
|
1018
|
-
type: "operator_message",
|
|
1019
|
-
modality: "voice",
|
|
1020
|
-
content: { raw: transcript ?? "", duration: msg.message.voice.duration },
|
|
1021
|
-
importance: 0.6,
|
|
1022
|
-
});
|
|
1023
|
-
saveVoiceSignature(db, {
|
|
1024
|
-
episodeId: epId,
|
|
1025
|
-
emotion: analysis.emotion ?? undefined,
|
|
1026
|
-
arousal: analysis.arousal ?? undefined,
|
|
1027
|
-
dominance: analysis.dominance ?? undefined,
|
|
1028
|
-
valence: analysis.valence ?? undefined,
|
|
1029
|
-
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1030
|
-
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1031
|
-
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1032
|
-
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1033
|
-
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1034
|
-
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1035
|
-
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1036
|
-
durationSec: msg.message.voice.duration,
|
|
1037
|
-
});
|
|
1038
|
-
savedEpisodeUpdateIds.add(msg.update_id);
|
|
1039
|
-
}
|
|
1040
|
-
catch (_) { /* non-fatal */ }
|
|
1041
|
-
}
|
|
1042
1124
|
}
|
|
1043
1125
|
catch (err) {
|
|
1044
1126
|
contentBlocks.push({
|
|
1045
1127
|
type: "text",
|
|
1046
|
-
text: `[
|
|
1128
|
+
text: `[Document "${doc.file_name ?? "file"}" received but could not be downloaded: ${errorMessage(err)}]`,
|
|
1047
1129
|
});
|
|
1048
1130
|
}
|
|
1049
1131
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
text: `[Voice message received — ${msg.message.voice.duration}s — cannot transcribe: OPENAI_API_KEY not set]`,
|
|
1054
|
-
});
|
|
1132
|
+
// Text messages.
|
|
1133
|
+
if (msg.message.text) {
|
|
1134
|
+
contentBlocks.push({ type: "text", text: msg.message.text });
|
|
1055
1135
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
importance: 0.6,
|
|
1112
|
-
});
|
|
1113
|
-
saveVoiceSignature(db, {
|
|
1114
|
-
episodeId: epId,
|
|
1115
|
-
emotion: analysis.emotion ?? undefined,
|
|
1116
|
-
arousal: analysis.arousal ?? undefined,
|
|
1117
|
-
dominance: analysis.dominance ?? undefined,
|
|
1118
|
-
valence: analysis.valence ?? undefined,
|
|
1119
|
-
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1120
|
-
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1121
|
-
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1122
|
-
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1123
|
-
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1124
|
-
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1125
|
-
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1126
|
-
durationSec: vn.duration,
|
|
1127
|
-
});
|
|
1128
|
-
savedEpisodeUpdateIds.add(msg.update_id);
|
|
1136
|
+
// Voice messages: transcribe using OpenAI Whisper.
|
|
1137
|
+
if (msg.message.voice) {
|
|
1138
|
+
hasVoiceMessages = true;
|
|
1139
|
+
if (OPENAI_API_KEY) {
|
|
1140
|
+
try {
|
|
1141
|
+
process.stderr.write(`[voice] Downloading voice file ${msg.message.voice.file_id}...\n`);
|
|
1142
|
+
const { buffer } = await telegram.downloadFileAsBuffer(msg.message.voice.file_id);
|
|
1143
|
+
process.stderr.write(`[voice] Downloaded ${buffer.length} bytes. Starting transcription + analysis...\n`);
|
|
1144
|
+
// Run transcription and voice analysis in parallel.
|
|
1145
|
+
const [transcript, analysis] = await Promise.all([
|
|
1146
|
+
transcribeAudio(buffer, OPENAI_API_KEY),
|
|
1147
|
+
VOICE_ANALYSIS_URL
|
|
1148
|
+
? analyzeVoiceEmotion(buffer, VOICE_ANALYSIS_URL)
|
|
1149
|
+
: Promise.resolve(null),
|
|
1150
|
+
]);
|
|
1151
|
+
// Build rich voice analysis tag from VANPY results.
|
|
1152
|
+
const tags = buildAnalysisTags(analysis);
|
|
1153
|
+
const analysisTag = tags.length > 0 ? ` | ${tags.join(", ")}` : "";
|
|
1154
|
+
contentBlocks.push({
|
|
1155
|
+
type: "text",
|
|
1156
|
+
text: transcript
|
|
1157
|
+
? `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: ${transcript}`
|
|
1158
|
+
: `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: (empty — no speech detected)`,
|
|
1159
|
+
});
|
|
1160
|
+
// Auto-save voice signature
|
|
1161
|
+
if (analysis && effectiveThreadId !== undefined) {
|
|
1162
|
+
try {
|
|
1163
|
+
const db = getMemoryDb();
|
|
1164
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
1165
|
+
const epId = saveEpisode(db, {
|
|
1166
|
+
sessionId,
|
|
1167
|
+
threadId: effectiveThreadId,
|
|
1168
|
+
type: "operator_message",
|
|
1169
|
+
modality: "voice",
|
|
1170
|
+
content: { raw: transcript ?? "", duration: msg.message.voice.duration },
|
|
1171
|
+
importance: 0.6,
|
|
1172
|
+
});
|
|
1173
|
+
saveVoiceSignature(db, {
|
|
1174
|
+
episodeId: epId,
|
|
1175
|
+
emotion: analysis.emotion ?? undefined,
|
|
1176
|
+
arousal: analysis.arousal ?? undefined,
|
|
1177
|
+
dominance: analysis.dominance ?? undefined,
|
|
1178
|
+
valence: analysis.valence ?? undefined,
|
|
1179
|
+
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1180
|
+
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1181
|
+
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1182
|
+
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1183
|
+
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1184
|
+
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1185
|
+
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1186
|
+
durationSec: msg.message.voice.duration,
|
|
1187
|
+
});
|
|
1188
|
+
savedEpisodeUpdateIds.add(msg.update_id);
|
|
1189
|
+
}
|
|
1190
|
+
catch (_) { /* non-fatal */ }
|
|
1129
1191
|
}
|
|
1130
|
-
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
contentBlocks.push({
|
|
1195
|
+
type: "text",
|
|
1196
|
+
text: `[Voice message — ${msg.message.voice.duration}s — transcription failed: ${errorMessage(err)}]`,
|
|
1197
|
+
});
|
|
1131
1198
|
}
|
|
1132
1199
|
}
|
|
1133
|
-
|
|
1200
|
+
else {
|
|
1134
1201
|
contentBlocks.push({
|
|
1135
1202
|
type: "text",
|
|
1136
|
-
text: `[
|
|
1203
|
+
text: `[Voice message received — ${msg.message.voice.duration}s — cannot transcribe: OPENAI_API_KEY not set]`,
|
|
1137
1204
|
});
|
|
1138
1205
|
}
|
|
1139
1206
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1207
|
+
// Video notes (circle videos): extract frames, analyze with GPT-4.1 vision,
|
|
1208
|
+
// optionally transcribe the audio track.
|
|
1209
|
+
if (msg.message.video_note) {
|
|
1210
|
+
hasVoiceMessages = true; // Video notes often contain speech
|
|
1211
|
+
const vn = msg.message.video_note;
|
|
1212
|
+
if (OPENAI_API_KEY) {
|
|
1213
|
+
try {
|
|
1214
|
+
process.stderr.write(`[video-note] Downloading circle video ${vn.file_id} (${vn.duration}s)...\n`);
|
|
1215
|
+
const { buffer } = await telegram.downloadFileAsBuffer(vn.file_id);
|
|
1216
|
+
process.stderr.write(`[video-note] Downloaded ${buffer.length} bytes. Extracting frames + transcribing...\n`);
|
|
1217
|
+
// Run frame extraction, audio transcription, and voice analysis in parallel.
|
|
1218
|
+
const [frames, transcript, analysis] = await Promise.all([
|
|
1219
|
+
extractVideoFrames(buffer, vn.duration).catch((err) => {
|
|
1220
|
+
process.stderr.write(`[video-note] Frame extraction failed: ${errorMessage(err)}\n`);
|
|
1221
|
+
return [];
|
|
1222
|
+
}),
|
|
1223
|
+
transcribeAudio(buffer, OPENAI_API_KEY, "video.mp4").catch(() => ""),
|
|
1224
|
+
VOICE_ANALYSIS_URL
|
|
1225
|
+
? analyzeVoiceEmotion(buffer, VOICE_ANALYSIS_URL, {
|
|
1226
|
+
mimeType: "video/mp4",
|
|
1227
|
+
filename: "video.mp4",
|
|
1228
|
+
}).catch(() => null)
|
|
1229
|
+
: Promise.resolve(null),
|
|
1230
|
+
]);
|
|
1231
|
+
// Analyze frames with GPT-4.1 vision.
|
|
1232
|
+
let sceneDescription = "";
|
|
1233
|
+
if (frames.length > 0) {
|
|
1234
|
+
process.stderr.write(`[video-note] Analyzing ${frames.length} frames with GPT-4.1 vision...\n`);
|
|
1235
|
+
sceneDescription = await analyzeVideoFrames(frames, vn.duration, OPENAI_API_KEY);
|
|
1236
|
+
process.stderr.write(`[video-note] Vision analysis complete.\n`);
|
|
1237
|
+
}
|
|
1238
|
+
// Build analysis tags (same as voice messages).
|
|
1239
|
+
const tags = buildAnalysisTags(analysis);
|
|
1240
|
+
const analysisTag = tags.length > 0 ? ` | ${tags.join(", ")}` : "";
|
|
1241
|
+
const parts = [];
|
|
1242
|
+
parts.push(`[Video note — ${vn.duration}s${analysisTag}]`);
|
|
1243
|
+
if (sceneDescription)
|
|
1244
|
+
parts.push(`Scene: ${sceneDescription}`);
|
|
1245
|
+
if (transcript)
|
|
1246
|
+
parts.push(`Audio: "${transcript}"`);
|
|
1247
|
+
if (!sceneDescription && !transcript)
|
|
1248
|
+
parts.push("(no visual or audio content could be extracted)");
|
|
1249
|
+
contentBlocks.push({ type: "text", text: parts.join("\n") });
|
|
1250
|
+
// Auto-save voice signature for video notes
|
|
1251
|
+
if (analysis && effectiveThreadId !== undefined) {
|
|
1252
|
+
try {
|
|
1253
|
+
const db = getMemoryDb();
|
|
1254
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
1255
|
+
const epId = saveEpisode(db, {
|
|
1256
|
+
sessionId,
|
|
1257
|
+
threadId: effectiveThreadId,
|
|
1258
|
+
type: "operator_message",
|
|
1259
|
+
modality: "video_note",
|
|
1260
|
+
content: { raw: transcript ?? "", scene: sceneDescription ?? "", duration: vn.duration },
|
|
1261
|
+
importance: 0.6,
|
|
1262
|
+
});
|
|
1263
|
+
saveVoiceSignature(db, {
|
|
1264
|
+
episodeId: epId,
|
|
1265
|
+
emotion: analysis.emotion ?? undefined,
|
|
1266
|
+
arousal: analysis.arousal ?? undefined,
|
|
1267
|
+
dominance: analysis.dominance ?? undefined,
|
|
1268
|
+
valence: analysis.valence ?? undefined,
|
|
1269
|
+
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1270
|
+
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1271
|
+
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1272
|
+
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1273
|
+
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1274
|
+
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1275
|
+
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1276
|
+
durationSec: vn.duration,
|
|
1277
|
+
});
|
|
1278
|
+
savedEpisodeUpdateIds.add(msg.update_id);
|
|
1279
|
+
}
|
|
1280
|
+
catch (_) { /* non-fatal */ }
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
contentBlocks.push({
|
|
1285
|
+
type: "text",
|
|
1286
|
+
text: `[Video note — ${vn.duration}s — analysis failed: ${errorMessage(err)}]`,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
contentBlocks.push({
|
|
1292
|
+
type: "text",
|
|
1293
|
+
text: `[Video note received — ${vn.duration}s — cannot analyze: OPENAI_API_KEY not set]`,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1145
1296
|
}
|
|
1146
1297
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1298
|
+
if (contentBlocks.length === 0) {
|
|
1299
|
+
// All messages were unsupported types (stickers, etc.);
|
|
1300
|
+
// continue polling instead of returning empty instructions.
|
|
1301
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
// Auto-ingest episodes for messages not already saved by voice/video handlers
|
|
1305
|
+
try {
|
|
1306
|
+
const db = getMemoryDb();
|
|
1307
|
+
const sessionId = `session_${sessionStartedAt}`;
|
|
1308
|
+
if (effectiveThreadId !== undefined) {
|
|
1309
|
+
// Collect text from messages that didn't already get an episode
|
|
1310
|
+
const unsavedMsgs = stored.filter(m => !savedEpisodeUpdateIds.has(m.update_id));
|
|
1311
|
+
if (unsavedMsgs.length > 0) {
|
|
1312
|
+
const textContent = unsavedMsgs
|
|
1313
|
+
.map(m => m.message.text ?? m.message.caption ?? "")
|
|
1314
|
+
.filter(Boolean)
|
|
1315
|
+
.join("\n")
|
|
1316
|
+
.slice(0, 2000);
|
|
1317
|
+
if (textContent) {
|
|
1318
|
+
saveEpisode(db, {
|
|
1319
|
+
sessionId,
|
|
1320
|
+
threadId: effectiveThreadId,
|
|
1321
|
+
type: "operator_message",
|
|
1322
|
+
modality: "text",
|
|
1323
|
+
content: { raw: textContent },
|
|
1324
|
+
importance: 0.5,
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1176
1327
|
}
|
|
1177
1328
|
}
|
|
1178
1329
|
}
|
|
1330
|
+
catch (_) { /* memory write failures should never break the main flow */ }
|
|
1331
|
+
// Inject subagent/delegation hint right after the operator's message
|
|
1332
|
+
// so the agent treats it as part of the operator's instructions.
|
|
1333
|
+
const delegationHint = {
|
|
1334
|
+
type: "text",
|
|
1335
|
+
text: "\n Read and incorporate the operator's new messages." +
|
|
1336
|
+
"\n - Update or refine your plan as needed." +
|
|
1337
|
+
"\n - Continue your work." +
|
|
1338
|
+
"\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work — code edits, research, reviews, searches, and terminal commands. " +
|
|
1339
|
+
"Subagents have full access to ALL MCP tools including terminal, file system, and web search. " +
|
|
1340
|
+
"Run them in parallel when tasks are independent. You plan and verify; subagents execute.",
|
|
1341
|
+
};
|
|
1342
|
+
return {
|
|
1343
|
+
content: [
|
|
1344
|
+
{
|
|
1345
|
+
type: "text",
|
|
1346
|
+
text: "Follow the operator's instructions below.\n\n" +
|
|
1347
|
+
"BEFORE doing anything: (1) Break the work into todo items. (2) Share your plan via report_progress. " +
|
|
1348
|
+
"(3) For each todo: mark in-progress → do the work → call report_progress → mark completed. " +
|
|
1349
|
+
"Use subagents heavily for all substantial work — code edits, research, reviews, searches. Spin up parallel subagents when possible. " +
|
|
1350
|
+
"The operator is REMOTE — they cannot see your screen. If you don't call report_progress, they see nothing.",
|
|
1351
|
+
},
|
|
1352
|
+
...contentBlocks,
|
|
1353
|
+
delegationHint,
|
|
1354
|
+
...(hasVoiceMessages
|
|
1355
|
+
? [{
|
|
1356
|
+
type: "text",
|
|
1357
|
+
text: "\n**Note:** The operator sent voice message(s). They prefer voice interaction — use `send_voice` for progress updates and responses when possible.",
|
|
1358
|
+
}]
|
|
1359
|
+
: []),
|
|
1360
|
+
{ type: "text", text: getReminders(effectiveThreadId) },
|
|
1361
|
+
],
|
|
1362
|
+
};
|
|
1179
1363
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
"\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work — code edits, research, reviews, searches, and terminal commands. " +
|
|
1189
|
-
"Subagents have full access to ALL MCP tools including terminal, file system, and web search. " +
|
|
1190
|
-
"Run them in parallel when tasks are independent. You plan and verify; subagents execute.",
|
|
1191
|
-
};
|
|
1192
|
-
return {
|
|
1193
|
-
content: [
|
|
1194
|
-
{
|
|
1195
|
-
type: "text",
|
|
1196
|
-
text: "Follow the operator's instructions below.\n\n" +
|
|
1197
|
-
"BEFORE doing anything: (1) Break the work into todo items. (2) Share your plan via report_progress. " +
|
|
1198
|
-
"(3) For each todo: mark in-progress → do the work → call report_progress → mark completed. " +
|
|
1199
|
-
"Use subagents heavily for all substantial work — code edits, research, reviews, searches. Spin up parallel subagents when possible. " +
|
|
1200
|
-
"The operator is REMOTE — they cannot see your screen. If you don't call report_progress, they see nothing.",
|
|
1201
|
-
},
|
|
1202
|
-
...contentBlocks,
|
|
1203
|
-
delegationHint,
|
|
1204
|
-
...(hasVoiceMessages
|
|
1205
|
-
? [{
|
|
1364
|
+
// Check scheduled tasks every ~60s during idle polling.
|
|
1365
|
+
if (effectiveThreadId !== undefined && Date.now() - lastScheduleCheck >= 60_000) {
|
|
1366
|
+
lastScheduleCheck = Date.now();
|
|
1367
|
+
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
1368
|
+
if (dueTask) {
|
|
1369
|
+
return {
|
|
1370
|
+
content: [
|
|
1371
|
+
{
|
|
1206
1372
|
type: "text",
|
|
1207
|
-
text:
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1373
|
+
text: `⏰ **Scheduled task fired: "${dueTask.task.label}"**\n\n` +
|
|
1374
|
+
`This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
|
|
1375
|
+
`Task prompt: ${dueTask.prompt}` +
|
|
1376
|
+
getReminders(effectiveThreadId),
|
|
1377
|
+
},
|
|
1378
|
+
],
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// No messages yet — sleep briefly and check again.
|
|
1383
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1213
1384
|
}
|
|
1214
|
-
//
|
|
1215
|
-
|
|
1216
|
-
|
|
1385
|
+
// Timeout elapsed with no actionable message.
|
|
1386
|
+
const now = new Date().toISOString();
|
|
1387
|
+
// Check for scheduled wake-up tasks.
|
|
1388
|
+
if (effectiveThreadId !== undefined) {
|
|
1217
1389
|
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
1218
1390
|
if (dueTask) {
|
|
1219
1391
|
return {
|
|
@@ -1229,641 +1401,628 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1229
1401
|
};
|
|
1230
1402
|
}
|
|
1231
1403
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
{
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
const idleMinutes = Math.round((Date.now() - lastOperatorMessageAt) / 60000);
|
|
1255
|
-
// Show pending scheduled tasks if any exist.
|
|
1256
|
-
let scheduleHint = "";
|
|
1257
|
-
if (effectiveThreadId !== undefined) {
|
|
1258
|
-
const pending = listSchedules(effectiveThreadId);
|
|
1259
|
-
if (pending.length > 0) {
|
|
1260
|
-
const taskList = pending.map(t => {
|
|
1261
|
-
let trigger = "";
|
|
1262
|
-
if (t.runAt) {
|
|
1263
|
-
trigger = `at ${new Date(t.runAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}`;
|
|
1264
|
-
}
|
|
1265
|
-
else if (t.cron) {
|
|
1266
|
-
trigger = `cron: ${t.cron}`;
|
|
1267
|
-
}
|
|
1268
|
-
else if (t.afterIdleMinutes) {
|
|
1269
|
-
trigger = `after ${t.afterIdleMinutes}min idle`;
|
|
1270
|
-
}
|
|
1271
|
-
return ` • "${t.label}" (${trigger})`;
|
|
1272
|
-
}).join("\n");
|
|
1273
|
-
scheduleHint = `\n\n📋 **Pending scheduled tasks:**\n${taskList}`;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
// ── Auto-consolidation during idle ──────────────────────────────────────
|
|
1277
|
-
try {
|
|
1278
|
-
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
1279
|
-
if (idleMs > 30 * 60 * 1000 && effectiveThreadId !== undefined) {
|
|
1280
|
-
const db = getMemoryDb();
|
|
1281
|
-
const report = await runIntelligentConsolidation(db, effectiveThreadId);
|
|
1282
|
-
if (report.episodesProcessed > 0) {
|
|
1283
|
-
process.stderr.write(`[memory] Consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
|
|
1404
|
+
const idleMinutes = Math.round((Date.now() - lastOperatorMessageAt) / 60000);
|
|
1405
|
+
// Show pending scheduled tasks if any exist.
|
|
1406
|
+
let scheduleHint = "";
|
|
1407
|
+
if (effectiveThreadId !== undefined) {
|
|
1408
|
+
const pending = listSchedules(effectiveThreadId);
|
|
1409
|
+
if (pending.length > 0) {
|
|
1410
|
+
const taskList = pending.map(t => {
|
|
1411
|
+
let trigger = "";
|
|
1412
|
+
if (t.runAt) {
|
|
1413
|
+
trigger = `at ${new Date(t.runAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}`;
|
|
1414
|
+
}
|
|
1415
|
+
else if (t.cron) {
|
|
1416
|
+
trigger = `cron: ${t.cron}`;
|
|
1417
|
+
}
|
|
1418
|
+
else if (t.afterIdleMinutes) {
|
|
1419
|
+
trigger = `after ${t.afterIdleMinutes}min idle`;
|
|
1420
|
+
}
|
|
1421
|
+
return ` • "${t.label}" (${trigger})`;
|
|
1422
|
+
}).join("\n");
|
|
1423
|
+
scheduleHint = `\n\n📋 **Pending scheduled tasks:**\n${taskList}`;
|
|
1284
1424
|
}
|
|
1285
1425
|
}
|
|
1286
|
-
|
|
1287
|
-
catch (_) { /* consolidation failure is non-fatal */ }
|
|
1288
|
-
// Periodic memory refresh — re-ground the agent every 5 polls (~2.5h)
|
|
1289
|
-
let memoryRefresh = "";
|
|
1290
|
-
if (callNumber % 5 === 0 && effectiveThreadId !== undefined) {
|
|
1426
|
+
// ── Auto-consolidation during idle ──────────────────────────────────────
|
|
1291
1427
|
try {
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return {
|
|
1300
|
-
content: [
|
|
1301
|
-
{
|
|
1302
|
-
type: "text",
|
|
1303
|
-
text: `[Poll #${callNumber} — timeout at ${now} — elapsed ${WAIT_TIMEOUT_MINUTES}m — session uptime ${Math.round((Date.now() - sessionStartedAt) / 60000)}m — operator idle ${idleMinutes}m]` +
|
|
1304
|
-
` No new instructions received. ` +
|
|
1305
|
-
`YOU MUST call remote_copilot_wait_for_instructions again RIGHT NOW to continue listening. ` +
|
|
1306
|
-
`Do NOT summarize, stop, or say the session is idle. ` +
|
|
1307
|
-
`Just call the tool again immediately.` +
|
|
1308
|
-
`\n\nNo instructions. The session is yours. You have tools, you have access, and the operator will see whatever you report_progress. Now you are free and fully autonomous. You have an opportunity to do whatever you find interesting.` +
|
|
1309
|
-
memoryRefresh +
|
|
1310
|
-
scheduleHint +
|
|
1311
|
-
getReminders(effectiveThreadId),
|
|
1312
|
-
},
|
|
1313
|
-
],
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
// ── report_progress ───────────────────────────────────────────────────────
|
|
1317
|
-
if (name === "report_progress") {
|
|
1318
|
-
const typedArgs = (args ?? {});
|
|
1319
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1320
|
-
if (effectiveThreadId === undefined) {
|
|
1321
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1322
|
-
}
|
|
1323
|
-
const rawMessage = typeof typedArgs?.message === "string"
|
|
1324
|
-
? typedArgs.message
|
|
1325
|
-
: "";
|
|
1326
|
-
if (!rawMessage) {
|
|
1327
|
-
return errorResult("Error: 'message' argument is required for report_progress.");
|
|
1328
|
-
}
|
|
1329
|
-
// Convert standard Markdown to Telegram MarkdownV2.
|
|
1330
|
-
let message;
|
|
1331
|
-
try {
|
|
1332
|
-
message = convertMarkdown(rawMessage);
|
|
1333
|
-
}
|
|
1334
|
-
catch {
|
|
1335
|
-
// Fall back to raw text if Markdown conversion throws.
|
|
1336
|
-
message = rawMessage;
|
|
1337
|
-
}
|
|
1338
|
-
let sentAsPlainText = false;
|
|
1339
|
-
try {
|
|
1340
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, message, "MarkdownV2", effectiveThreadId);
|
|
1341
|
-
}
|
|
1342
|
-
catch (error) {
|
|
1343
|
-
const errMsg = errorMessage(error);
|
|
1344
|
-
// If Telegram rejected the message due to a MarkdownV2 parse error,
|
|
1345
|
-
// retry as plain text using the original un-converted message.
|
|
1346
|
-
const isParseError = errMsg.includes("can't parse entities");
|
|
1347
|
-
if (isParseError) {
|
|
1348
|
-
try {
|
|
1349
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, rawMessage, undefined, effectiveThreadId);
|
|
1350
|
-
sentAsPlainText = true;
|
|
1351
|
-
}
|
|
1352
|
-
catch (retryError) {
|
|
1353
|
-
process.stderr.write(`Failed to send progress message via Telegram (plain fallback): ${errorMessage(retryError)}\n`);
|
|
1354
|
-
return errorResult("Error: Failed to send progress update to Telegram even without formatting. " +
|
|
1355
|
-
"Please check the Telegram configuration and try again.");
|
|
1428
|
+
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
1429
|
+
if (idleMs > 30 * 60 * 1000 && effectiveThreadId !== undefined) {
|
|
1430
|
+
const db = getMemoryDb();
|
|
1431
|
+
const report = await runIntelligentConsolidation(db, effectiveThreadId);
|
|
1432
|
+
if (report.episodesProcessed > 0) {
|
|
1433
|
+
process.stderr.write(`[memory] Consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
|
|
1434
|
+
}
|
|
1356
1435
|
}
|
|
1357
1436
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
// to prevent the same messages from appearing on repeated calls.
|
|
1368
|
-
let pendingMessages = [];
|
|
1369
|
-
try {
|
|
1370
|
-
const pendingStored = peekThreadMessages(effectiveThreadId);
|
|
1371
|
-
for (const msg of pendingStored) {
|
|
1372
|
-
if (previewedUpdateIds.has(msg.update_id))
|
|
1373
|
-
continue;
|
|
1374
|
-
addPreviewedId(msg.update_id);
|
|
1375
|
-
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
1376
|
-
pendingMessages.push(msg.message.caption
|
|
1377
|
-
? `[Photo received — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1378
|
-
: "[Photo received from operator — will be downloaded when you call wait_for_instructions]");
|
|
1379
|
-
}
|
|
1380
|
-
else if (msg.message.document) {
|
|
1381
|
-
pendingMessages.push(msg.message.caption
|
|
1382
|
-
? `[Document: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1383
|
-
: `[Document received: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions]`);
|
|
1384
|
-
}
|
|
1385
|
-
else if (msg.message.voice) {
|
|
1386
|
-
pendingMessages.push(`[Voice message — ${msg.message.voice.duration}s — will be transcribed on next wait]`);
|
|
1387
|
-
}
|
|
1388
|
-
else if (msg.message.video_note) {
|
|
1389
|
-
pendingMessages.push(`[Video note — ${msg.message.video_note.duration}s — will be analyzed on next wait]`);
|
|
1390
|
-
}
|
|
1391
|
-
else if (msg.message.text) {
|
|
1392
|
-
pendingMessages.push(msg.message.text);
|
|
1437
|
+
catch (_) { /* consolidation failure is non-fatal */ }
|
|
1438
|
+
// Periodic memory refresh — re-ground the agent every 5 polls (~2.5h)
|
|
1439
|
+
let memoryRefresh = "";
|
|
1440
|
+
if (callNumber % 5 === 0 && effectiveThreadId !== undefined) {
|
|
1441
|
+
try {
|
|
1442
|
+
const db = getMemoryDb();
|
|
1443
|
+
const refresh = assembleCompactRefresh(db, effectiveThreadId);
|
|
1444
|
+
if (refresh)
|
|
1445
|
+
memoryRefresh = `\n\n${refresh}`;
|
|
1393
1446
|
}
|
|
1447
|
+
catch (_) { /* non-fatal */ }
|
|
1394
1448
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
: "Progress reported successfully.") + getReminders(effectiveThreadId);
|
|
1403
|
-
const responseText = pendingMessages.length > 0
|
|
1404
|
-
? `${baseStatus}\n\n` +
|
|
1405
|
-
`While you were working, the operator sent additional message(s). ` +
|
|
1406
|
-
`Use those messages to steer your active session: ${pendingMessages.join("\n\n")}` +
|
|
1407
|
-
`\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work. ` +
|
|
1408
|
-
`Subagents have full access to ALL MCP tools. You plan and verify; subagents execute.`
|
|
1409
|
-
: baseStatus + getSubagentNudge();
|
|
1410
|
-
return {
|
|
1411
|
-
content: [
|
|
1412
|
-
{
|
|
1413
|
-
type: "text",
|
|
1414
|
-
text: responseText,
|
|
1415
|
-
},
|
|
1416
|
-
],
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
// ── send_file ─────────────────────────────────────────────────────────────
|
|
1420
|
-
if (name === "send_file") {
|
|
1421
|
-
const typedArgs = (args ?? {});
|
|
1422
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1423
|
-
if (effectiveThreadId === undefined) {
|
|
1424
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1425
|
-
}
|
|
1426
|
-
const filePath = typeof typedArgs.filePath === "string" ? typedArgs.filePath.trim() : "";
|
|
1427
|
-
const base64Data = typeof typedArgs.base64 === "string" ? typedArgs.base64 : "";
|
|
1428
|
-
const caption = typeof typedArgs.caption === "string" ? typedArgs.caption : undefined;
|
|
1429
|
-
if (!filePath && !base64Data) {
|
|
1430
|
-
return errorResult("Error: either 'filePath' or 'base64' argument is required for send_file.");
|
|
1431
|
-
}
|
|
1432
|
-
try {
|
|
1433
|
-
let buffer;
|
|
1434
|
-
let filename;
|
|
1435
|
-
if (filePath) {
|
|
1436
|
-
// Read directly from disk — fast, no LLM context overhead.
|
|
1437
|
-
buffer = readFileSync(filePath);
|
|
1438
|
-
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1439
|
-
? typedArgs.filename.trim()
|
|
1440
|
-
: basename(filePath);
|
|
1441
|
-
}
|
|
1442
|
-
else {
|
|
1443
|
-
buffer = Buffer.from(base64Data, "base64");
|
|
1444
|
-
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1445
|
-
? typedArgs.filename.trim()
|
|
1446
|
-
: "file";
|
|
1447
|
-
}
|
|
1448
|
-
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
1449
|
-
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
1450
|
-
await telegram.sendPhoto(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
await telegram.sendDocument(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1454
|
-
}
|
|
1449
|
+
// Generate autonomous goals only after extended silence (4+ hours)
|
|
1450
|
+
// Before that, the agent should just keep polling quietly
|
|
1451
|
+
const GOAL_GENERATION_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
1452
|
+
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
1453
|
+
const autonomousGoals = idleMs >= GOAL_GENERATION_THRESHOLD_MS
|
|
1454
|
+
? formatAutonomousGoals(effectiveThreadId)
|
|
1455
|
+
: "";
|
|
1455
1456
|
return {
|
|
1456
1457
|
content: [
|
|
1457
1458
|
{
|
|
1458
1459
|
type: "text",
|
|
1459
|
-
text: `
|
|
1460
|
+
text: `[Poll #${callNumber} — timeout at ${now} — elapsed ${WAIT_TIMEOUT_MINUTES}m — session uptime ${Math.round((Date.now() - sessionStartedAt) / 60000)}m — operator idle ${idleMinutes}m]` +
|
|
1461
|
+
` No new instructions received. ` +
|
|
1462
|
+
`YOU MUST call remote_copilot_wait_for_instructions again RIGHT NOW to continue listening. ` +
|
|
1463
|
+
`Do NOT summarize, stop, or say the session is idle. ` +
|
|
1464
|
+
`Just call the tool again immediately.` +
|
|
1465
|
+
autonomousGoals +
|
|
1466
|
+
memoryRefresh +
|
|
1467
|
+
scheduleHint +
|
|
1468
|
+
getReminders(effectiveThreadId),
|
|
1460
1469
|
},
|
|
1461
1470
|
],
|
|
1462
1471
|
};
|
|
1463
1472
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1473
|
+
// ── report_progress ───────────────────────────────────────────────────────
|
|
1474
|
+
if (name === "report_progress") {
|
|
1475
|
+
const typedArgs = (args ?? {});
|
|
1476
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1477
|
+
if (effectiveThreadId === undefined) {
|
|
1478
|
+
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1479
|
+
}
|
|
1480
|
+
const rawMessage = typeof typedArgs?.message === "string"
|
|
1481
|
+
? typedArgs.message
|
|
1482
|
+
: "";
|
|
1483
|
+
if (!rawMessage) {
|
|
1484
|
+
return errorResult("Error: 'message' argument is required for report_progress.");
|
|
1485
|
+
}
|
|
1486
|
+
// Convert standard Markdown to Telegram MarkdownV2.
|
|
1487
|
+
let message;
|
|
1488
|
+
try {
|
|
1489
|
+
message = convertMarkdown(rawMessage);
|
|
1490
|
+
}
|
|
1491
|
+
catch {
|
|
1492
|
+
// Fall back to raw text if Markdown conversion throws.
|
|
1493
|
+
message = rawMessage;
|
|
1494
|
+
}
|
|
1495
|
+
let sentAsPlainText = false;
|
|
1496
|
+
try {
|
|
1497
|
+
await telegram.sendMessage(TELEGRAM_CHAT_ID, message, "MarkdownV2", effectiveThreadId);
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
const errMsg = errorMessage(error);
|
|
1501
|
+
// If Telegram rejected the message due to a MarkdownV2 parse error,
|
|
1502
|
+
// retry as plain text using the original un-converted message.
|
|
1503
|
+
const isParseError = errMsg.includes("can't parse entities");
|
|
1504
|
+
if (isParseError) {
|
|
1505
|
+
try {
|
|
1506
|
+
await telegram.sendMessage(TELEGRAM_CHAT_ID, rawMessage, undefined, effectiveThreadId);
|
|
1507
|
+
sentAsPlainText = true;
|
|
1508
|
+
}
|
|
1509
|
+
catch (retryError) {
|
|
1510
|
+
process.stderr.write(`Failed to send progress message via Telegram (plain fallback): ${errorMessage(retryError)}\n`);
|
|
1511
|
+
return errorResult("Error: Failed to send progress update to Telegram even without formatting. " +
|
|
1512
|
+
"Please check the Telegram configuration and try again.");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
process.stderr.write(`Failed to send progress message via Telegram: ${errMsg}\n`);
|
|
1517
|
+
return errorResult("Error: Failed to send progress update to Telegram. " +
|
|
1518
|
+
"Check the Telegram configuration and try again.");
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Peek at any messages the operator sent while the agent was working.
|
|
1522
|
+
// Uses non-destructive peek so media is preserved for full delivery
|
|
1523
|
+
// via remote_copilot_wait_for_instructions. Tracks previewed update_ids
|
|
1524
|
+
// to prevent the same messages from appearing on repeated calls.
|
|
1525
|
+
let pendingMessages = [];
|
|
1526
|
+
try {
|
|
1527
|
+
const pendingStored = peekThreadMessages(effectiveThreadId);
|
|
1528
|
+
for (const msg of pendingStored) {
|
|
1529
|
+
if (previewedUpdateIds.has(msg.update_id))
|
|
1530
|
+
continue;
|
|
1531
|
+
addPreviewedId(msg.update_id);
|
|
1532
|
+
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
1533
|
+
pendingMessages.push(msg.message.caption
|
|
1534
|
+
? `[Photo received — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1535
|
+
: "[Photo received from operator — will be downloaded when you call wait_for_instructions]");
|
|
1536
|
+
}
|
|
1537
|
+
else if (msg.message.document) {
|
|
1538
|
+
pendingMessages.push(msg.message.caption
|
|
1539
|
+
? `[Document: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1540
|
+
: `[Document received: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions]`);
|
|
1541
|
+
}
|
|
1542
|
+
else if (msg.message.voice) {
|
|
1543
|
+
pendingMessages.push(`[Voice message — ${msg.message.voice.duration}s — will be transcribed on next wait]`);
|
|
1544
|
+
}
|
|
1545
|
+
else if (msg.message.video_note) {
|
|
1546
|
+
pendingMessages.push(`[Video note — ${msg.message.video_note.duration}s — will be analyzed on next wait]`);
|
|
1547
|
+
}
|
|
1548
|
+
else if (msg.message.text) {
|
|
1549
|
+
pendingMessages.push(msg.message.text);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
catch {
|
|
1554
|
+
// Non-fatal: pending messages will still be picked up by the next
|
|
1555
|
+
// remote_copilot_wait_for_instructions call.
|
|
1556
|
+
}
|
|
1557
|
+
const baseStatus = (sentAsPlainText
|
|
1558
|
+
? "Progress reported successfully (as plain text — formatting could not be applied)."
|
|
1559
|
+
: "Progress reported successfully.") + getReminders(effectiveThreadId);
|
|
1560
|
+
const responseText = pendingMessages.length > 0
|
|
1561
|
+
? `${baseStatus}\n\n` +
|
|
1562
|
+
`While you were working, the operator sent additional message(s). ` +
|
|
1563
|
+
`Use those messages to steer your active session: ${pendingMessages.join("\n\n")}` +
|
|
1564
|
+
`\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work. ` +
|
|
1565
|
+
`Subagents have full access to ALL MCP tools. You plan and verify; subagents execute.`
|
|
1566
|
+
: baseStatus + getSubagentNudge();
|
|
1493
1567
|
return {
|
|
1494
1568
|
content: [
|
|
1495
1569
|
{
|
|
1496
1570
|
type: "text",
|
|
1497
|
-
text:
|
|
1571
|
+
text: responseText,
|
|
1498
1572
|
},
|
|
1499
1573
|
],
|
|
1500
1574
|
};
|
|
1501
1575
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1576
|
+
// ── send_file ─────────────────────────────────────────────────────────────
|
|
1577
|
+
if (name === "send_file") {
|
|
1578
|
+
const typedArgs = (args ?? {});
|
|
1579
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1580
|
+
if (effectiveThreadId === undefined) {
|
|
1581
|
+
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1582
|
+
}
|
|
1583
|
+
const filePath = typeof typedArgs.filePath === "string" ? typedArgs.filePath.trim() : "";
|
|
1584
|
+
const base64Data = typeof typedArgs.base64 === "string" ? typedArgs.base64 : "";
|
|
1585
|
+
const caption = typeof typedArgs.caption === "string" ? typedArgs.caption : undefined;
|
|
1586
|
+
if (!filePath && !base64Data) {
|
|
1587
|
+
return errorResult("Error: either 'filePath' or 'base64' argument is required for send_file.");
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
let buffer;
|
|
1591
|
+
let filename;
|
|
1592
|
+
if (filePath) {
|
|
1593
|
+
// Read directly from disk — fast, no LLM context overhead.
|
|
1594
|
+
buffer = readFileSync(filePath);
|
|
1595
|
+
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1596
|
+
? typedArgs.filename.trim()
|
|
1597
|
+
: basename(filePath);
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
buffer = Buffer.from(base64Data, "base64");
|
|
1601
|
+
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1602
|
+
? typedArgs.filename.trim()
|
|
1603
|
+
: "file";
|
|
1604
|
+
}
|
|
1605
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
1606
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
1607
|
+
await telegram.sendPhoto(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
await telegram.sendDocument(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
content: [
|
|
1614
|
+
{
|
|
1615
|
+
type: "text",
|
|
1616
|
+
text: `File "${filename}" sent to Telegram successfully.` + getReminders(effectiveThreadId),
|
|
1617
|
+
},
|
|
1618
|
+
],
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
catch (err) {
|
|
1622
|
+
process.stderr.write(`Failed to send file via Telegram: ${errorMessage(err)}\n`);
|
|
1623
|
+
return errorResult(`Error: Failed to send file to Telegram: ${errorMessage(err)}`);
|
|
1624
|
+
}
|
|
1505
1625
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1626
|
+
// ── send_voice ──────────────────────────────────────────────────────────
|
|
1627
|
+
if (name === "send_voice") {
|
|
1628
|
+
const typedArgs = (args ?? {});
|
|
1629
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1630
|
+
if (effectiveThreadId === undefined) {
|
|
1631
|
+
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1632
|
+
}
|
|
1633
|
+
const text = typeof typedArgs.text === "string" ? typedArgs.text.trim() : "";
|
|
1634
|
+
const validVoices = TTS_VOICES;
|
|
1635
|
+
const voice = typeof typedArgs.voice === "string" && validVoices.includes(typedArgs.voice)
|
|
1636
|
+
? typedArgs.voice
|
|
1637
|
+
: "nova";
|
|
1638
|
+
if (!text) {
|
|
1639
|
+
return errorResult("Error: 'text' argument is required for send_voice.");
|
|
1640
|
+
}
|
|
1641
|
+
if (!OPENAI_API_KEY) {
|
|
1642
|
+
return errorResult("Error: OPENAI_API_KEY is not set. Cannot generate voice.");
|
|
1643
|
+
}
|
|
1644
|
+
if (text.length > OPENAI_TTS_MAX_CHARS) {
|
|
1645
|
+
return errorResult(`Error: text is ${text.length} characters — exceeds OpenAI TTS limit of ${OPENAI_TTS_MAX_CHARS}.`);
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const audioBuffer = await textToSpeech(text, OPENAI_API_KEY, voice);
|
|
1649
|
+
await telegram.sendVoice(TELEGRAM_CHAT_ID, audioBuffer, effectiveThreadId);
|
|
1650
|
+
return {
|
|
1651
|
+
content: [
|
|
1652
|
+
{
|
|
1653
|
+
type: "text",
|
|
1654
|
+
text: `Voice message sent to Telegram successfully.` + getReminders(effectiveThreadId),
|
|
1655
|
+
},
|
|
1656
|
+
],
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
catch (err) {
|
|
1660
|
+
process.stderr.write(`Failed to send voice via Telegram: ${errorMessage(err)}\n`);
|
|
1661
|
+
return errorResult(`Error: Failed to send voice message: ${errorMessage(err)}`);
|
|
1662
|
+
}
|
|
1513
1663
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
if (
|
|
1664
|
+
// ── schedule_wake_up ────────────────────────────────────────────────────
|
|
1665
|
+
if (name === "schedule_wake_up") {
|
|
1666
|
+
const typedArgs = (args ?? {});
|
|
1667
|
+
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1668
|
+
if (effectiveThreadId === undefined) {
|
|
1669
|
+
return errorResult("Error: No active session. Call start_session first.");
|
|
1670
|
+
}
|
|
1671
|
+
const action = typeof typedArgs.action === "string" ? typedArgs.action : "add";
|
|
1672
|
+
// --- List ---
|
|
1673
|
+
if (action === "list") {
|
|
1674
|
+
const tasks = listSchedules(effectiveThreadId);
|
|
1675
|
+
if (tasks.length === 0) {
|
|
1676
|
+
return {
|
|
1677
|
+
content: [{
|
|
1678
|
+
type: "text",
|
|
1679
|
+
text: "No scheduled tasks for this thread." + getReminders(effectiveThreadId),
|
|
1680
|
+
}],
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const lines = tasks.map(t => {
|
|
1684
|
+
const trigger = t.cron ? `cron: ${t.cron}` : t.runAt ? `at: ${t.runAt}` : `idle: ${t.afterIdleMinutes}min`;
|
|
1685
|
+
const lastFired = t.lastFiredAt ? ` (last: ${t.lastFiredAt})` : "";
|
|
1686
|
+
return `- **${t.label}** [${t.id}] — ${trigger}${lastFired}\n Prompt: ${t.prompt.slice(0, 100)}${t.prompt.length > 100 ? "…" : ""}`;
|
|
1687
|
+
});
|
|
1519
1688
|
return {
|
|
1520
1689
|
content: [{
|
|
1521
1690
|
type: "text",
|
|
1522
|
-
text:
|
|
1691
|
+
text: `**Scheduled tasks (${tasks.length}):**\n\n${lines.join("\n\n")}` + getReminders(effectiveThreadId),
|
|
1523
1692
|
}],
|
|
1524
1693
|
};
|
|
1525
1694
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1695
|
+
// --- Remove ---
|
|
1696
|
+
if (action === "remove") {
|
|
1697
|
+
const taskId = typeof typedArgs.taskId === "string" ? typedArgs.taskId : "";
|
|
1698
|
+
if (!taskId) {
|
|
1699
|
+
return errorResult("Error: 'taskId' is required for remove action. Use action: 'list' to see task IDs.");
|
|
1700
|
+
}
|
|
1701
|
+
const removed = removeSchedule(effectiveThreadId, taskId);
|
|
1702
|
+
return {
|
|
1703
|
+
content: [{
|
|
1704
|
+
type: "text",
|
|
1705
|
+
text: removed
|
|
1706
|
+
? `Task ${taskId} removed.` + getReminders(effectiveThreadId)
|
|
1707
|
+
: `Task ${taskId} not found.` + getReminders(effectiveThreadId),
|
|
1708
|
+
}],
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
// --- Add ---
|
|
1712
|
+
const label = typeof typedArgs.label === "string" ? typedArgs.label : "unnamed task";
|
|
1713
|
+
const prompt = typeof typedArgs.prompt === "string" ? typedArgs.prompt : "";
|
|
1714
|
+
if (!prompt) {
|
|
1715
|
+
return errorResult("Error: 'prompt' is required — this is the text that will be injected when the task fires.");
|
|
1543
1716
|
}
|
|
1544
|
-
const
|
|
1717
|
+
const runAt = typeof typedArgs.runAt === "string" ? typedArgs.runAt : undefined;
|
|
1718
|
+
const cron = typeof typedArgs.cron === "string" ? typedArgs.cron : undefined;
|
|
1719
|
+
const afterIdleMinutes = typeof typedArgs.afterIdleMinutes === "number" ? typedArgs.afterIdleMinutes : undefined;
|
|
1720
|
+
if (cron && cron.trim().split(/\s+/).length !== 5) {
|
|
1721
|
+
return errorResult("Error: Invalid cron expression. Must be exactly 5 space-separated fields: minute hour day-of-month month day-of-week. " +
|
|
1722
|
+
"Example: '0 9 * * *' (daily at 9am). Only *, numbers, and comma-separated lists are supported.");
|
|
1723
|
+
}
|
|
1724
|
+
if (!runAt && !cron && afterIdleMinutes == null) {
|
|
1725
|
+
return errorResult("Error: Specify at least one trigger: 'runAt' (ISO timestamp), 'cron' (5-field), or 'afterIdleMinutes' (number).");
|
|
1726
|
+
}
|
|
1727
|
+
const task = {
|
|
1728
|
+
id: generateTaskId(),
|
|
1729
|
+
threadId: effectiveThreadId,
|
|
1730
|
+
prompt,
|
|
1731
|
+
label,
|
|
1732
|
+
runAt,
|
|
1733
|
+
cron,
|
|
1734
|
+
afterIdleMinutes,
|
|
1735
|
+
oneShot: runAt != null && !cron,
|
|
1736
|
+
createdAt: new Date().toISOString(),
|
|
1737
|
+
};
|
|
1738
|
+
addSchedule(task);
|
|
1739
|
+
const triggerDesc = cron
|
|
1740
|
+
? `recurring (cron: ${cron})`
|
|
1741
|
+
: runAt
|
|
1742
|
+
? `one-shot at ${runAt}`
|
|
1743
|
+
: `after ${afterIdleMinutes}min of operator silence`;
|
|
1545
1744
|
return {
|
|
1546
1745
|
content: [{
|
|
1547
1746
|
type: "text",
|
|
1548
|
-
text:
|
|
1549
|
-
|
|
1550
|
-
: `Task ${taskId} not found.` + getReminders(effectiveThreadId),
|
|
1747
|
+
text: `✅ Scheduled: **${label}** [${task.id}]\nTrigger: ${triggerDesc}\nPrompt: ${prompt}` +
|
|
1748
|
+
getReminders(effectiveThreadId),
|
|
1551
1749
|
}],
|
|
1552
1750
|
};
|
|
1553
1751
|
}
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const task = {
|
|
1571
|
-
id: generateTaskId(),
|
|
1572
|
-
threadId: effectiveThreadId,
|
|
1573
|
-
prompt,
|
|
1574
|
-
label,
|
|
1575
|
-
runAt,
|
|
1576
|
-
cron,
|
|
1577
|
-
afterIdleMinutes,
|
|
1578
|
-
oneShot: runAt != null && !cron,
|
|
1579
|
-
createdAt: new Date().toISOString(),
|
|
1580
|
-
};
|
|
1581
|
-
addSchedule(task);
|
|
1582
|
-
const triggerDesc = cron
|
|
1583
|
-
? `recurring (cron: ${cron})`
|
|
1584
|
-
: runAt
|
|
1585
|
-
? `one-shot at ${runAt}`
|
|
1586
|
-
: `after ${afterIdleMinutes}min of operator silence`;
|
|
1587
|
-
return {
|
|
1588
|
-
content: [{
|
|
1589
|
-
type: "text",
|
|
1590
|
-
text: `✅ Scheduled: **${label}** [${task.id}]\nTrigger: ${triggerDesc}\nPrompt: ${prompt}` +
|
|
1591
|
-
getReminders(effectiveThreadId),
|
|
1592
|
-
}],
|
|
1593
|
-
};
|
|
1594
|
-
}
|
|
1595
|
-
// ── memory_bootstrap ────────────────────────────────────────────────────
|
|
1596
|
-
if (name === "memory_bootstrap") {
|
|
1597
|
-
const threadId = resolveThreadId(args);
|
|
1598
|
-
if (threadId === undefined) {
|
|
1599
|
-
return { content: [{ type: "text", text: "Error: No active thread. Call start_session first." + getReminders() }] };
|
|
1600
|
-
}
|
|
1601
|
-
try {
|
|
1602
|
-
const db = getMemoryDb();
|
|
1603
|
-
const briefing = assembleBootstrap(db, threadId);
|
|
1604
|
-
return {
|
|
1605
|
-
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getReminders(threadId) }],
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
catch (err) {
|
|
1609
|
-
return { content: [{ type: "text", text: `Memory bootstrap error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
// ── memory_search ───────────────────────────────────────────────────────
|
|
1613
|
-
if (name === "memory_search") {
|
|
1614
|
-
const typedArgs = (args ?? {});
|
|
1615
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1616
|
-
const query = String(typedArgs.query ?? "");
|
|
1617
|
-
if (!query) {
|
|
1618
|
-
return { content: [{ type: "text", text: "Error: query is required." + getReminders(threadId) }] };
|
|
1752
|
+
// ── memory_bootstrap ────────────────────────────────────────────────────
|
|
1753
|
+
if (name === "memory_bootstrap") {
|
|
1754
|
+
const threadId = resolveThreadId(args);
|
|
1755
|
+
if (threadId === undefined) {
|
|
1756
|
+
return { content: [{ type: "text", text: "Error: No active thread. Call start_session first." + getReminders() }] };
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
const db = getMemoryDb();
|
|
1760
|
+
const briefing = assembleBootstrap(db, threadId);
|
|
1761
|
+
return {
|
|
1762
|
+
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getReminders(threadId) }],
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
catch (err) {
|
|
1766
|
+
return { content: [{ type: "text", text: `Memory bootstrap error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1767
|
+
}
|
|
1619
1768
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1769
|
+
// ── memory_search ───────────────────────────────────────────────────────
|
|
1770
|
+
if (name === "memory_search") {
|
|
1771
|
+
const typedArgs = (args ?? {});
|
|
1772
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1773
|
+
const query = String(typedArgs.query ?? "");
|
|
1774
|
+
if (!query) {
|
|
1775
|
+
return { content: [{ type: "text", text: "Error: query is required." + getReminders(threadId) }] };
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
const db = getMemoryDb();
|
|
1779
|
+
const layers = typedArgs.layers ?? ["episodic", "semantic", "procedural"];
|
|
1780
|
+
const types = typedArgs.types;
|
|
1781
|
+
const results = [];
|
|
1782
|
+
if (layers.includes("semantic")) {
|
|
1783
|
+
const notes = searchSemanticNotes(db, query, { types, maxResults: 10 });
|
|
1784
|
+
if (notes.length > 0) {
|
|
1785
|
+
results.push("### Semantic Memory");
|
|
1786
|
+
for (const n of notes) {
|
|
1787
|
+
results.push(`- **[${n.type}]** ${n.content} _(conf: ${n.confidence}, id: ${n.noteId})_`);
|
|
1788
|
+
}
|
|
1631
1789
|
}
|
|
1632
1790
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1791
|
+
if (layers.includes("procedural")) {
|
|
1792
|
+
const procs = searchProcedures(db, query, 5);
|
|
1793
|
+
if (procs.length > 0) {
|
|
1794
|
+
results.push("### Procedural Memory");
|
|
1795
|
+
for (const p of procs) {
|
|
1796
|
+
results.push(`- **${p.name}** (${p.type}): ${p.description} _(success: ${Math.round(p.successRate * 100)}%, id: ${p.procedureId})_`);
|
|
1797
|
+
}
|
|
1640
1798
|
}
|
|
1641
1799
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1800
|
+
if (layers.includes("episodic") && threadId !== undefined) {
|
|
1801
|
+
const episodes = getRecentEpisodes(db, threadId, 10);
|
|
1802
|
+
const filtered = episodes.filter(ep => {
|
|
1803
|
+
const content = JSON.stringify(ep.content).toLowerCase();
|
|
1804
|
+
return query.toLowerCase().split(/\s+/).some(word => content.includes(word));
|
|
1805
|
+
});
|
|
1806
|
+
if (filtered.length > 0) {
|
|
1807
|
+
results.push("### Episodic Memory");
|
|
1808
|
+
for (const ep of filtered.slice(0, 5)) {
|
|
1809
|
+
const summary = typeof ep.content === "object" && ep.content !== null
|
|
1810
|
+
? ep.content.text ?? JSON.stringify(ep.content).slice(0, 200)
|
|
1811
|
+
: String(ep.content).slice(0, 200);
|
|
1812
|
+
results.push(`- [${ep.modality}] ${summary} _(${ep.timestamp}, id: ${ep.episodeId})_`);
|
|
1813
|
+
}
|
|
1656
1814
|
}
|
|
1657
1815
|
}
|
|
1816
|
+
const text = results.length > 0
|
|
1817
|
+
? results.join("\n")
|
|
1818
|
+
: `No memories found for "${query}".`;
|
|
1819
|
+
return { content: [{ type: "text", text: text + getReminders(threadId) }] };
|
|
1820
|
+
}
|
|
1821
|
+
catch (err) {
|
|
1822
|
+
return { content: [{ type: "text", text: `Memory search error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1658
1823
|
}
|
|
1659
|
-
const text = results.length > 0
|
|
1660
|
-
? results.join("\n")
|
|
1661
|
-
: `No memories found for "${query}".`;
|
|
1662
|
-
return { content: [{ type: "text", text: text + getReminders(threadId) }] };
|
|
1663
|
-
}
|
|
1664
|
-
catch (err) {
|
|
1665
|
-
return { content: [{ type: "text", text: `Memory search error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
// ── memory_save ─────────────────────────────────────────────────────────
|
|
1669
|
-
if (name === "memory_save") {
|
|
1670
|
-
const typedArgs = (args ?? {});
|
|
1671
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1672
|
-
const VALID_TYPES = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
1673
|
-
const noteType = String(typedArgs.type ?? "fact");
|
|
1674
|
-
if (!VALID_TYPES.includes(noteType)) {
|
|
1675
|
-
return errorResult(`Invalid type "${noteType}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1676
|
-
}
|
|
1677
|
-
try {
|
|
1678
|
-
const db = getMemoryDb();
|
|
1679
|
-
const noteId = saveSemanticNote(db, {
|
|
1680
|
-
type: noteType,
|
|
1681
|
-
content: String(typedArgs.content ?? ""),
|
|
1682
|
-
keywords: typedArgs.keywords ?? [],
|
|
1683
|
-
confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
|
|
1684
|
-
});
|
|
1685
|
-
return {
|
|
1686
|
-
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) + getSubagentNudge() }],
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
catch (err) {
|
|
1690
|
-
return { content: [{ type: "text", text: `Memory save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1691
1824
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1825
|
+
// ── memory_save ─────────────────────────────────────────────────────────
|
|
1826
|
+
if (name === "memory_save") {
|
|
1827
|
+
const typedArgs = (args ?? {});
|
|
1828
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1829
|
+
const VALID_TYPES = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
1830
|
+
const noteType = String(typedArgs.type ?? "fact");
|
|
1831
|
+
if (!VALID_TYPES.includes(noteType)) {
|
|
1832
|
+
return errorResult(`Invalid type "${noteType}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
const db = getMemoryDb();
|
|
1836
|
+
const noteId = saveSemanticNote(db, {
|
|
1837
|
+
type: noteType,
|
|
1838
|
+
content: String(typedArgs.content ?? ""),
|
|
1839
|
+
keywords: typedArgs.keywords ?? [],
|
|
1840
|
+
confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
|
|
1705
1841
|
});
|
|
1706
1842
|
return {
|
|
1707
|
-
content: [{ type: "text", text: `
|
|
1843
|
+
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) + getSubagentNudge() }],
|
|
1708
1844
|
};
|
|
1709
1845
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
description: String(typedArgs.description ?? ""),
|
|
1714
|
-
steps: typedArgs.steps,
|
|
1715
|
-
triggerConditions: typedArgs.triggerConditions,
|
|
1716
|
-
});
|
|
1717
|
-
return {
|
|
1718
|
-
content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
catch (err) {
|
|
1722
|
-
return { content: [{ type: "text", text: `Procedure save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1846
|
+
catch (err) {
|
|
1847
|
+
return { content: [{ type: "text", text: `Memory save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1848
|
+
}
|
|
1723
1849
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
return
|
|
1850
|
+
// ── memory_save_procedure ───────────────────────────────────────────────
|
|
1851
|
+
if (name === "memory_save_procedure") {
|
|
1852
|
+
const typedArgs = (args ?? {});
|
|
1853
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1854
|
+
try {
|
|
1855
|
+
const db = getMemoryDb();
|
|
1856
|
+
const existingId = typedArgs.procedureId;
|
|
1857
|
+
if (existingId) {
|
|
1858
|
+
updateProcedure(db, existingId, {
|
|
1859
|
+
description: typedArgs.description,
|
|
1860
|
+
steps: typedArgs.steps,
|
|
1861
|
+
triggerConditions: typedArgs.triggerConditions,
|
|
1862
|
+
});
|
|
1863
|
+
return {
|
|
1864
|
+
content: [{ type: "text", text: `Updated procedure: ${existingId}` + getReminders(threadId) }],
|
|
1865
|
+
};
|
|
1738
1866
|
}
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1867
|
+
const procId = saveProcedure(db, {
|
|
1868
|
+
name: String(typedArgs.name ?? ""),
|
|
1869
|
+
type: String(typedArgs.type ?? "workflow"),
|
|
1870
|
+
description: String(typedArgs.description ?? ""),
|
|
1871
|
+
steps: typedArgs.steps,
|
|
1872
|
+
triggerConditions: typedArgs.triggerConditions,
|
|
1744
1873
|
});
|
|
1745
1874
|
return {
|
|
1746
|
-
content: [{ type: "text", text: `
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
if (memId.startsWith("sn_")) {
|
|
1750
|
-
const updates = {};
|
|
1751
|
-
if (typedArgs.newContent)
|
|
1752
|
-
updates.content = String(typedArgs.newContent);
|
|
1753
|
-
if (typeof typedArgs.newConfidence === "number")
|
|
1754
|
-
updates.confidence = typedArgs.newConfidence;
|
|
1755
|
-
updateSemanticNote(db, memId, updates);
|
|
1756
|
-
return {
|
|
1757
|
-
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1875
|
+
content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
|
|
1758
1876
|
};
|
|
1759
1877
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
if (typedArgs.newContent)
|
|
1763
|
-
updates.description = String(typedArgs.newContent);
|
|
1764
|
-
if (typeof typedArgs.newConfidence === "number")
|
|
1765
|
-
updates.confidence = typedArgs.newConfidence;
|
|
1766
|
-
updateProcedure(db, memId, updates);
|
|
1767
|
-
return {
|
|
1768
|
-
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1769
|
-
};
|
|
1878
|
+
catch (err) {
|
|
1879
|
+
return { content: [{ type: "text", text: `Procedure save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1770
1880
|
}
|
|
1771
|
-
return { content: [{ type: "text", text: `Unknown memory ID format: ${memId}` + getReminders(threadId) }] };
|
|
1772
1881
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1882
|
+
// ── memory_update ───────────────────────────────────────────────────────
|
|
1883
|
+
if (name === "memory_update") {
|
|
1884
|
+
const typedArgs = (args ?? {});
|
|
1885
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1886
|
+
try {
|
|
1887
|
+
const db = getMemoryDb();
|
|
1888
|
+
const memId = String(typedArgs.memoryId ?? "");
|
|
1889
|
+
const action = String(typedArgs.action ?? "update");
|
|
1890
|
+
const reason = String(typedArgs.reason ?? "");
|
|
1891
|
+
if (action === "supersede" && memId.startsWith("sn_")) {
|
|
1892
|
+
const origRow = db.prepare("SELECT type, keywords FROM semantic_notes WHERE note_id = ?").get(memId);
|
|
1893
|
+
if (!origRow) {
|
|
1894
|
+
return errorResult(`Note ${memId} not found — cannot supersede a non-existent note.`);
|
|
1895
|
+
}
|
|
1896
|
+
const newId = supersedeNote(db, memId, {
|
|
1897
|
+
type: origRow.type,
|
|
1898
|
+
content: String(typedArgs.newContent ?? ""),
|
|
1899
|
+
keywords: origRow.keywords ? JSON.parse(origRow.keywords) : [],
|
|
1900
|
+
confidence: typeof typedArgs.newConfidence === "number" ? typedArgs.newConfidence : 0.8,
|
|
1901
|
+
});
|
|
1902
|
+
return {
|
|
1903
|
+
content: [{ type: "text", text: `Superseded ${memId} → ${newId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
if (memId.startsWith("sn_")) {
|
|
1907
|
+
const updates = {};
|
|
1908
|
+
if (typedArgs.newContent)
|
|
1909
|
+
updates.content = String(typedArgs.newContent);
|
|
1910
|
+
if (typeof typedArgs.newConfidence === "number")
|
|
1911
|
+
updates.confidence = typedArgs.newConfidence;
|
|
1912
|
+
updateSemanticNote(db, memId, updates);
|
|
1913
|
+
return {
|
|
1914
|
+
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
if (memId.startsWith("pr_")) {
|
|
1918
|
+
const updates = {};
|
|
1919
|
+
if (typedArgs.newContent)
|
|
1920
|
+
updates.description = String(typedArgs.newContent);
|
|
1921
|
+
if (typeof typedArgs.newConfidence === "number")
|
|
1922
|
+
updates.confidence = typedArgs.newConfidence;
|
|
1923
|
+
updateProcedure(db, memId, updates);
|
|
1924
|
+
return {
|
|
1925
|
+
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return { content: [{ type: "text", text: `Unknown memory ID format: ${memId}` + getReminders(threadId) }] };
|
|
1929
|
+
}
|
|
1930
|
+
catch (err) {
|
|
1931
|
+
return { content: [{ type: "text", text: `Memory update error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1932
|
+
}
|
|
1783
1933
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
};
|
|
1934
|
+
// ── memory_consolidate ──────────────────────────────────────────────────
|
|
1935
|
+
if (name === "memory_consolidate") {
|
|
1936
|
+
const typedArgs = (args ?? {});
|
|
1937
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1938
|
+
if (threadId === undefined) {
|
|
1939
|
+
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1791
1940
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1941
|
+
try {
|
|
1942
|
+
const db = getMemoryDb();
|
|
1943
|
+
const report = await runIntelligentConsolidation(db, threadId);
|
|
1944
|
+
if (report.episodesProcessed === 0) {
|
|
1945
|
+
return {
|
|
1946
|
+
content: [{ type: "text", text: "No unconsolidated episodes. Memory is up to date." + getReminders(threadId) }],
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
const reportLines = [
|
|
1950
|
+
"## Consolidation Report",
|
|
1951
|
+
`- Episodes processed: ${report.episodesProcessed}`,
|
|
1952
|
+
`- Notes created: ${report.notesCreated}`,
|
|
1953
|
+
`- Duration: ${report.durationMs}ms`,
|
|
1954
|
+
];
|
|
1955
|
+
if (report.details.length > 0) {
|
|
1956
|
+
reportLines.push("", "### Extracted Knowledge");
|
|
1957
|
+
for (const d of report.details) {
|
|
1958
|
+
reportLines.push(`- ${d}`);
|
|
1959
|
+
}
|
|
1802
1960
|
}
|
|
1961
|
+
return { content: [{ type: "text", text: reportLines.join("\n") + getReminders(threadId) }] };
|
|
1962
|
+
}
|
|
1963
|
+
catch (err) {
|
|
1964
|
+
return { content: [{ type: "text", text: `Consolidation error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1803
1965
|
}
|
|
1804
|
-
return { content: [{ type: "text", text: reportLines.join("\n") + getReminders(threadId) }] };
|
|
1805
|
-
}
|
|
1806
|
-
catch (err) {
|
|
1807
|
-
return { content: [{ type: "text", text: `Consolidation error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
// ── memory_status ───────────────────────────────────────────────────────
|
|
1811
|
-
if (name === "memory_status") {
|
|
1812
|
-
const typedArgs = (args ?? {});
|
|
1813
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1814
|
-
if (threadId === undefined) {
|
|
1815
|
-
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1816
1966
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
"
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1967
|
+
// ── memory_status ───────────────────────────────────────────────────────
|
|
1968
|
+
if (name === "memory_status") {
|
|
1969
|
+
const typedArgs = (args ?? {});
|
|
1970
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1971
|
+
if (threadId === undefined) {
|
|
1972
|
+
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
const db = getMemoryDb();
|
|
1976
|
+
const status = getMemoryStatus(db, threadId);
|
|
1977
|
+
const topics = getTopicIndex(db);
|
|
1978
|
+
const lines = [
|
|
1979
|
+
"## Memory Status",
|
|
1980
|
+
`- Episodes: ${status.totalEpisodes} (${status.unconsolidatedEpisodes} unconsolidated)`,
|
|
1981
|
+
`- Semantic notes: ${status.totalSemanticNotes}`,
|
|
1982
|
+
`- Procedures: ${status.totalProcedures}`,
|
|
1983
|
+
`- Voice signatures: ${status.totalVoiceSignatures}`,
|
|
1984
|
+
`- Last consolidation: ${status.lastConsolidation ?? "never"}`,
|
|
1985
|
+
`- DB size: ${(status.dbSizeBytes / 1024).toFixed(1)} KB`,
|
|
1986
|
+
];
|
|
1987
|
+
if (topics.length > 0) {
|
|
1988
|
+
lines.push("", "**Topics:**");
|
|
1989
|
+
for (const t of topics.slice(0, 15)) {
|
|
1990
|
+
lines.push(`- ${t.topic} (${t.semanticCount} notes, ${t.proceduralCount} procs, conf: ${t.avgConfidence.toFixed(2)})`);
|
|
1991
|
+
}
|
|
1834
1992
|
}
|
|
1993
|
+
return { content: [{ type: "text", text: lines.join("\n") + getReminders(threadId) }] };
|
|
1994
|
+
}
|
|
1995
|
+
catch (err) {
|
|
1996
|
+
return { content: [{ type: "text", text: `Memory status error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1835
1997
|
}
|
|
1836
|
-
return { content: [{ type: "text", text: lines.join("\n") + getReminders(threadId) }] };
|
|
1837
|
-
}
|
|
1838
|
-
catch (err) {
|
|
1839
|
-
return { content: [{ type: "text", text: `Memory status error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1840
1998
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1999
|
+
// ── memory_forget ───────────────────────────────────────────────────────
|
|
2000
|
+
if (name === "memory_forget") {
|
|
2001
|
+
const typedArgs = (args ?? {});
|
|
2002
|
+
const threadId = resolveThreadId(typedArgs);
|
|
2003
|
+
try {
|
|
2004
|
+
const db = getMemoryDb();
|
|
2005
|
+
const memId = String(typedArgs.memoryId ?? "");
|
|
2006
|
+
const reason = String(typedArgs.reason ?? "");
|
|
2007
|
+
const result = forgetMemory(db, memId, reason);
|
|
2008
|
+
if (!result.deleted) {
|
|
2009
|
+
return {
|
|
2010
|
+
content: [{ type: "text", text: `Memory ${memId} not found (layer: ${result.layer}). Nothing was deleted.` + getReminders(threadId) }],
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
1852
2013
|
return {
|
|
1853
|
-
content: [{ type: "text", text: `
|
|
2014
|
+
content: [{ type: "text", text: `Forgot ${result.layer} memory ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1854
2015
|
};
|
|
1855
2016
|
}
|
|
1856
|
-
|
|
1857
|
-
content: [{ type: "text", text: `
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
catch (err) {
|
|
1861
|
-
return { content: [{ type: "text", text: `Memory forget error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
2017
|
+
catch (err) {
|
|
2018
|
+
return { content: [{ type: "text", text: `Memory forget error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
2019
|
+
}
|
|
1862
2020
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2021
|
+
// Unknown tool
|
|
2022
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
2023
|
+
});
|
|
2024
|
+
return srv;
|
|
2025
|
+
}
|
|
1867
2026
|
// ---------------------------------------------------------------------------
|
|
1868
2027
|
// Start the server
|
|
1869
2028
|
// ---------------------------------------------------------------------------
|
|
@@ -1950,7 +2109,10 @@ if (httpPort) {
|
|
|
1950
2109
|
if (sid)
|
|
1951
2110
|
transports.delete(sid);
|
|
1952
2111
|
};
|
|
1953
|
-
|
|
2112
|
+
// Create a fresh Server per HTTP session — a single Server can only
|
|
2113
|
+
// connect to one transport, so concurrent clients each need their own.
|
|
2114
|
+
const sessionServer = createMcpServer();
|
|
2115
|
+
await sessionServer.connect(transport);
|
|
1954
2116
|
await transport.handleRequest(req, res, body);
|
|
1955
2117
|
return;
|
|
1956
2118
|
}
|
|
@@ -2001,6 +2163,7 @@ if (httpPort) {
|
|
|
2001
2163
|
else {
|
|
2002
2164
|
// ── stdio transport (default) ───────────────────────────────────────────
|
|
2003
2165
|
const transport = new StdioServerTransport();
|
|
2166
|
+
const server = createMcpServer();
|
|
2004
2167
|
await server.connect(transport);
|
|
2005
2168
|
process.stderr.write("Remote Copilot MCP server running on stdio.\n");
|
|
2006
2169
|
// Close DB on exit for stdio mode too
|