sensorium-mcp 2.8.9 → 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 +1384 -1369
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -467,899 +467,925 @@ function formatAutonomousGoals(threadId) {
|
|
|
467
467
|
return lines.join("\n");
|
|
468
468
|
}
|
|
469
469
|
// ---------------------------------------------------------------------------
|
|
470
|
-
// 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.
|
|
471
474
|
// ---------------------------------------------------------------------------
|
|
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
|
-
"
|
|
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
|
+
},
|
|
499
504
|
},
|
|
505
|
+
required: [],
|
|
500
506
|
},
|
|
501
|
-
required: [],
|
|
502
507
|
},
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
},
|
|
517
522
|
},
|
|
523
|
+
required: [],
|
|
518
524
|
},
|
|
519
|
-
required: [],
|
|
520
525
|
},
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
},
|
|
538
543
|
},
|
|
544
|
+
required: ["message"],
|
|
539
545
|
},
|
|
540
|
-
required: ["message"],
|
|
541
546
|
},
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
},
|
|
574
579
|
},
|
|
580
|
+
required: [],
|
|
575
581
|
},
|
|
576
|
-
required: [],
|
|
577
582
|
},
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
"
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
},
|
|
603
608
|
},
|
|
609
|
+
required: ["text"],
|
|
604
610
|
},
|
|
605
|
-
required: ["text"],
|
|
606
611
|
},
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
},
|
|
651
656
|
},
|
|
652
657
|
},
|
|
653
658
|
},
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
},
|
|
667
672
|
},
|
|
668
673
|
},
|
|
669
674
|
},
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
+
},
|
|
700
705
|
},
|
|
706
|
+
required: ["query"],
|
|
701
707
|
},
|
|
702
|
-
required: ["query"],
|
|
703
708
|
},
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
},
|
|
734
739
|
},
|
|
740
|
+
required: ["content", "type", "keywords"],
|
|
735
741
|
},
|
|
736
|
-
required: ["content", "type", "keywords"],
|
|
737
742
|
},
|
|
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
|
-
|
|
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
|
+
},
|
|
775
780
|
},
|
|
781
|
+
required: ["name", "type", "description"],
|
|
776
782
|
},
|
|
777
|
-
required: ["name", "type", "description"],
|
|
778
783
|
},
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
},
|
|
810
815
|
},
|
|
816
|
+
required: ["memoryId", "action", "reason"],
|
|
811
817
|
},
|
|
812
|
-
required: ["memoryId", "action", "reason"],
|
|
813
818
|
},
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
},
|
|
830
835
|
},
|
|
831
836
|
},
|
|
832
837
|
},
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
+
},
|
|
845
850
|
},
|
|
846
851
|
},
|
|
847
852
|
},
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
+
},
|
|
867
872
|
},
|
|
873
|
+
required: ["memoryId", "reason"],
|
|
868
874
|
},
|
|
869
|
-
required: ["memoryId", "reason"],
|
|
870
875
|
},
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
persistSession(TELEGRAM_CHAT_ID, customName, explicitThreadId);
|
|
929
|
-
resolvedPreexisting = true;
|
|
930
|
-
}
|
|
931
|
-
else if (customName !== undefined) {
|
|
932
|
-
const stored = lookupSession(TELEGRAM_CHAT_ID, customName);
|
|
933
|
-
if (stored !== undefined) {
|
|
934
|
-
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);
|
|
935
933
|
resolvedPreexisting = true;
|
|
936
934
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (stale.length > 0) {
|
|
943
|
-
process.stderr.write(`[start_session] Drained ${stale.length} stale message(s) from thread ${currentThreadId}.\n`);
|
|
944
|
-
// Notify the operator that stale messages were discarded.
|
|
945
|
-
try {
|
|
946
|
-
const notice = convertMarkdown(`\u26A0\uFE0F **${stale.length} message(s) from before the session resumed were discarded.** ` +
|
|
947
|
-
`If you sent instructions while the agent was offline, please resend them.`);
|
|
948
|
-
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;
|
|
949
940
|
}
|
|
950
|
-
catch { /* non-fatal */ }
|
|
951
941
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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.
|
|
973
979
|
}
|
|
974
|
-
// Other errors (network, etc.) are non-fatal — proceed anyway.
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (!resolvedPreexisting) {
|
|
978
|
-
// New session: create a dedicated forum topic.
|
|
979
|
-
const topicName = customName ??
|
|
980
|
-
`Copilot — ${new Date().toLocaleString("en-GB", {
|
|
981
|
-
day: "2-digit", month: "short", year: "numeric",
|
|
982
|
-
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
983
|
-
})}`;
|
|
984
|
-
try {
|
|
985
|
-
const topic = await telegram.createForumTopic(TELEGRAM_CHAT_ID, topicName);
|
|
986
|
-
currentThreadId = topic.message_thread_id;
|
|
987
|
-
// Persist so the same name resumes this thread next time.
|
|
988
|
-
persistSession(TELEGRAM_CHAT_ID, topicName, currentThreadId);
|
|
989
980
|
}
|
|
990
|
-
|
|
991
|
-
//
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
+
}
|
|
995
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 = "";
|
|
996
1016
|
try {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
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
|
+
}
|
|
1002
1021
|
}
|
|
1003
|
-
catch {
|
|
1004
|
-
|
|
1022
|
+
catch (e) {
|
|
1023
|
+
memoryBriefing = "\n\n_Memory system unavailable._";
|
|
1005
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
|
+
};
|
|
1006
1036
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
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.");
|
|
1016
1046
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
if (name === "remote_copilot_wait_for_instructions") {
|
|
1035
|
-
// Agent is actively polling — this is the primary health signal
|
|
1036
|
-
deadSessionAlerted = false;
|
|
1037
|
-
toolCallsSinceLastDelivery = 0; // reset on polling
|
|
1038
|
-
const typedArgs = (args ?? {});
|
|
1039
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1040
|
-
if (effectiveThreadId === undefined) {
|
|
1041
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId to this tool.");
|
|
1042
|
-
}
|
|
1043
|
-
const callNumber = ++waitCallCount;
|
|
1044
|
-
const timeoutMs = WAIT_TIMEOUT_MINUTES * 60 * 1000;
|
|
1045
|
-
const deadline = Date.now() + timeoutMs;
|
|
1046
|
-
// Poll the dispatcher's per-thread file instead of calling getUpdates
|
|
1047
|
-
// directly. This avoids 409 conflicts between concurrent instances.
|
|
1048
|
-
const POLL_INTERVAL_MS = 2000;
|
|
1049
|
-
let lastScheduleCheck = 0;
|
|
1050
|
-
while (Date.now() < deadline) {
|
|
1051
|
-
const stored = readThreadMessages(effectiveThreadId);
|
|
1052
|
-
if (stored.length > 0) {
|
|
1053
|
-
// Update the operator activity timestamp for idle detection.
|
|
1054
|
-
lastOperatorMessageAt = Date.now();
|
|
1055
|
-
// Clear only the consumed IDs from the previewed set (scoped clear).
|
|
1056
|
-
// This is safe because Node.js is single-threaded — no report_progress
|
|
1057
|
-
// call can interleave between readThreadMessages and this cleanup.
|
|
1058
|
-
for (const msg of stored) {
|
|
1059
|
-
previewedUpdateIds.delete(msg.update_id);
|
|
1060
|
-
}
|
|
1061
|
-
// React with 👀 on each consumed message to signal "seen" to the operator.
|
|
1062
|
-
for (const msg of stored) {
|
|
1063
|
-
void telegram.setMessageReaction(TELEGRAM_CHAT_ID, msg.message.message_id);
|
|
1064
|
-
}
|
|
1065
|
-
const contentBlocks = [];
|
|
1066
|
-
let hasVoiceMessages = false;
|
|
1067
|
-
// Track which messages already had episodes saved (voice/video handlers)
|
|
1068
|
-
const savedEpisodeUpdateIds = new Set();
|
|
1069
|
-
for (const msg of stored) {
|
|
1070
|
-
// Photos: download the largest size, persist to disk, and embed as base64.
|
|
1071
|
-
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
1072
|
-
const largest = msg.message.photo[msg.message.photo.length - 1];
|
|
1073
|
-
try {
|
|
1074
|
-
const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(largest.file_id);
|
|
1075
|
-
const ext = telegramPath.split(".").pop()?.toLowerCase() ?? "jpg";
|
|
1076
|
-
const mimeType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
1077
|
-
const base64 = buffer.toString("base64");
|
|
1078
|
-
const diskPath = saveFileToDisk(buffer, `photo.${ext}`);
|
|
1079
|
-
contentBlocks.push({ type: "image", data: base64, mimeType });
|
|
1080
|
-
contentBlocks.push({
|
|
1081
|
-
type: "text",
|
|
1082
|
-
text: `[Photo saved to: ${diskPath}]` +
|
|
1083
|
-
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
catch (err) {
|
|
1087
|
-
contentBlocks.push({
|
|
1088
|
-
type: "text",
|
|
1089
|
-
text: `[Photo received but could not be downloaded: ${errorMessage(err)}]`,
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
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);
|
|
1092
1064
|
}
|
|
1093
|
-
//
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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);
|
|
1068
|
+
}
|
|
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}`);
|
|
1105
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
|
+
});
|
|
1106
1089
|
}
|
|
1107
|
-
|
|
1108
|
-
// Non-image documents: provide the disk path instead of
|
|
1109
|
-
// dumping potentially huge base64 into the LLM context.
|
|
1090
|
+
catch (err) {
|
|
1110
1091
|
contentBlocks.push({
|
|
1111
1092
|
type: "text",
|
|
1112
|
-
text: `[
|
|
1093
|
+
text: `[Photo received but could not be downloaded: ${errorMessage(err)}]`,
|
|
1113
1094
|
});
|
|
1114
1095
|
}
|
|
1115
|
-
contentBlocks.push({
|
|
1116
|
-
type: "text",
|
|
1117
|
-
text: `[File saved to: ${diskPath}]` +
|
|
1118
|
-
(msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
catch (err) {
|
|
1122
|
-
contentBlocks.push({
|
|
1123
|
-
type: "text",
|
|
1124
|
-
text: `[Document "${doc.file_name ?? "file"}" received but could not be downloaded: ${errorMessage(err)}]`,
|
|
1125
|
-
});
|
|
1126
1096
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
contentBlocks.push({ type: "text", text: msg.message.text });
|
|
1131
|
-
}
|
|
1132
|
-
// Voice messages: transcribe using OpenAI Whisper.
|
|
1133
|
-
if (msg.message.voice) {
|
|
1134
|
-
hasVoiceMessages = true;
|
|
1135
|
-
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;
|
|
1136
1100
|
try {
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
+
}
|
|
1150
1119
|
contentBlocks.push({
|
|
1151
1120
|
type: "text",
|
|
1152
|
-
text:
|
|
1153
|
-
? `
|
|
1154
|
-
: `[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}` : ""),
|
|
1155
1123
|
});
|
|
1156
|
-
// Auto-save voice signature
|
|
1157
|
-
if (analysis && effectiveThreadId !== undefined) {
|
|
1158
|
-
try {
|
|
1159
|
-
const db = getMemoryDb();
|
|
1160
|
-
const sessionId = `session_${sessionStartedAt}`;
|
|
1161
|
-
const epId = saveEpisode(db, {
|
|
1162
|
-
sessionId,
|
|
1163
|
-
threadId: effectiveThreadId,
|
|
1164
|
-
type: "operator_message",
|
|
1165
|
-
modality: "voice",
|
|
1166
|
-
content: { raw: transcript ?? "", duration: msg.message.voice.duration },
|
|
1167
|
-
importance: 0.6,
|
|
1168
|
-
});
|
|
1169
|
-
saveVoiceSignature(db, {
|
|
1170
|
-
episodeId: epId,
|
|
1171
|
-
emotion: analysis.emotion ?? undefined,
|
|
1172
|
-
arousal: analysis.arousal ?? undefined,
|
|
1173
|
-
dominance: analysis.dominance ?? undefined,
|
|
1174
|
-
valence: analysis.valence ?? undefined,
|
|
1175
|
-
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1176
|
-
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1177
|
-
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1178
|
-
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1179
|
-
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1180
|
-
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1181
|
-
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1182
|
-
durationSec: msg.message.voice.duration,
|
|
1183
|
-
});
|
|
1184
|
-
savedEpisodeUpdateIds.add(msg.update_id);
|
|
1185
|
-
}
|
|
1186
|
-
catch (_) { /* non-fatal */ }
|
|
1187
|
-
}
|
|
1188
1124
|
}
|
|
1189
1125
|
catch (err) {
|
|
1190
1126
|
contentBlocks.push({
|
|
1191
1127
|
type: "text",
|
|
1192
|
-
text: `[
|
|
1128
|
+
text: `[Document "${doc.file_name ?? "file"}" received but could not be downloaded: ${errorMessage(err)}]`,
|
|
1193
1129
|
});
|
|
1194
1130
|
}
|
|
1195
1131
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
text: `[Voice message received — ${msg.message.voice.duration}s — cannot transcribe: OPENAI_API_KEY not set]`,
|
|
1200
|
-
});
|
|
1132
|
+
// Text messages.
|
|
1133
|
+
if (msg.message.text) {
|
|
1134
|
+
contentBlocks.push({ type: "text", text: msg.message.text });
|
|
1201
1135
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
importance: 0.6,
|
|
1258
|
-
});
|
|
1259
|
-
saveVoiceSignature(db, {
|
|
1260
|
-
episodeId: epId,
|
|
1261
|
-
emotion: analysis.emotion ?? undefined,
|
|
1262
|
-
arousal: analysis.arousal ?? undefined,
|
|
1263
|
-
dominance: analysis.dominance ?? undefined,
|
|
1264
|
-
valence: analysis.valence ?? undefined,
|
|
1265
|
-
speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
|
|
1266
|
-
meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
|
|
1267
|
-
pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
|
|
1268
|
-
jitter: analysis.paralinguistics?.jitter ?? undefined,
|
|
1269
|
-
shimmer: analysis.paralinguistics?.shimmer ?? undefined,
|
|
1270
|
-
hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
|
|
1271
|
-
audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
|
|
1272
|
-
durationSec: vn.duration,
|
|
1273
|
-
});
|
|
1274
|
-
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 */ }
|
|
1275
1191
|
}
|
|
1276
|
-
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
contentBlocks.push({
|
|
1195
|
+
type: "text",
|
|
1196
|
+
text: `[Voice message — ${msg.message.voice.duration}s — transcription failed: ${errorMessage(err)}]`,
|
|
1197
|
+
});
|
|
1277
1198
|
}
|
|
1278
1199
|
}
|
|
1279
|
-
|
|
1200
|
+
else {
|
|
1280
1201
|
contentBlocks.push({
|
|
1281
1202
|
type: "text",
|
|
1282
|
-
text: `[
|
|
1203
|
+
text: `[Voice message received — ${msg.message.voice.duration}s — cannot transcribe: OPENAI_API_KEY not set]`,
|
|
1283
1204
|
});
|
|
1284
1205
|
}
|
|
1285
1206
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
+
}
|
|
1291
1296
|
}
|
|
1292
1297
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
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
|
+
}
|
|
1322
1327
|
}
|
|
1323
1328
|
}
|
|
1324
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
|
+
};
|
|
1325
1363
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
"\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work — code edits, research, reviews, searches, and terminal commands. " +
|
|
1335
|
-
"Subagents have full access to ALL MCP tools including terminal, file system, and web search. " +
|
|
1336
|
-
"Run them in parallel when tasks are independent. You plan and verify; subagents execute.",
|
|
1337
|
-
};
|
|
1338
|
-
return {
|
|
1339
|
-
content: [
|
|
1340
|
-
{
|
|
1341
|
-
type: "text",
|
|
1342
|
-
text: "Follow the operator's instructions below.\n\n" +
|
|
1343
|
-
"BEFORE doing anything: (1) Break the work into todo items. (2) Share your plan via report_progress. " +
|
|
1344
|
-
"(3) For each todo: mark in-progress → do the work → call report_progress → mark completed. " +
|
|
1345
|
-
"Use subagents heavily for all substantial work — code edits, research, reviews, searches. Spin up parallel subagents when possible. " +
|
|
1346
|
-
"The operator is REMOTE — they cannot see your screen. If you don't call report_progress, they see nothing.",
|
|
1347
|
-
},
|
|
1348
|
-
...contentBlocks,
|
|
1349
|
-
delegationHint,
|
|
1350
|
-
...(hasVoiceMessages
|
|
1351
|
-
? [{
|
|
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
|
+
{
|
|
1352
1372
|
type: "text",
|
|
1353
|
-
text:
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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));
|
|
1359
1384
|
}
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1362
|
-
|
|
1385
|
+
// Timeout elapsed with no actionable message.
|
|
1386
|
+
const now = new Date().toISOString();
|
|
1387
|
+
// Check for scheduled wake-up tasks.
|
|
1388
|
+
if (effectiveThreadId !== undefined) {
|
|
1363
1389
|
const dueTask = checkDueTasks(effectiveThreadId, lastOperatorMessageAt, false);
|
|
1364
1390
|
if (dueTask) {
|
|
1365
1391
|
return {
|
|
@@ -1375,643 +1401,628 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1375
1401
|
};
|
|
1376
1402
|
}
|
|
1377
1403
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
{
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
const idleMinutes = Math.round((Date.now() - lastOperatorMessageAt) / 60000);
|
|
1401
|
-
// Show pending scheduled tasks if any exist.
|
|
1402
|
-
let scheduleHint = "";
|
|
1403
|
-
if (effectiveThreadId !== undefined) {
|
|
1404
|
-
const pending = listSchedules(effectiveThreadId);
|
|
1405
|
-
if (pending.length > 0) {
|
|
1406
|
-
const taskList = pending.map(t => {
|
|
1407
|
-
let trigger = "";
|
|
1408
|
-
if (t.runAt) {
|
|
1409
|
-
trigger = `at ${new Date(t.runAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}`;
|
|
1410
|
-
}
|
|
1411
|
-
else if (t.cron) {
|
|
1412
|
-
trigger = `cron: ${t.cron}`;
|
|
1413
|
-
}
|
|
1414
|
-
else if (t.afterIdleMinutes) {
|
|
1415
|
-
trigger = `after ${t.afterIdleMinutes}min idle`;
|
|
1416
|
-
}
|
|
1417
|
-
return ` • "${t.label}" (${trigger})`;
|
|
1418
|
-
}).join("\n");
|
|
1419
|
-
scheduleHint = `\n\n📋 **Pending scheduled tasks:**\n${taskList}`;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
// ── Auto-consolidation during idle ──────────────────────────────────────
|
|
1423
|
-
try {
|
|
1424
|
-
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
1425
|
-
if (idleMs > 30 * 60 * 1000 && effectiveThreadId !== undefined) {
|
|
1426
|
-
const db = getMemoryDb();
|
|
1427
|
-
const report = await runIntelligentConsolidation(db, effectiveThreadId);
|
|
1428
|
-
if (report.episodesProcessed > 0) {
|
|
1429
|
-
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}`;
|
|
1430
1424
|
}
|
|
1431
1425
|
}
|
|
1432
|
-
|
|
1433
|
-
catch (_) { /* consolidation failure is non-fatal */ }
|
|
1434
|
-
// Periodic memory refresh — re-ground the agent every 5 polls (~2.5h)
|
|
1435
|
-
let memoryRefresh = "";
|
|
1436
|
-
if (callNumber % 5 === 0 && effectiveThreadId !== undefined) {
|
|
1426
|
+
// ── Auto-consolidation during idle ──────────────────────────────────────
|
|
1437
1427
|
try {
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
// Generate autonomous goals (curiosity-driven idle behavior)
|
|
1446
|
-
const autonomousGoals = formatAutonomousGoals(effectiveThreadId);
|
|
1447
|
-
return {
|
|
1448
|
-
content: [
|
|
1449
|
-
{
|
|
1450
|
-
type: "text",
|
|
1451
|
-
text: `[Poll #${callNumber} — timeout at ${now} — elapsed ${WAIT_TIMEOUT_MINUTES}m — session uptime ${Math.round((Date.now() - sessionStartedAt) / 60000)}m — operator idle ${idleMinutes}m]` +
|
|
1452
|
-
` No new instructions received. ` +
|
|
1453
|
-
`YOU MUST call remote_copilot_wait_for_instructions again RIGHT NOW to continue listening. ` +
|
|
1454
|
-
`Do NOT summarize, stop, or say the session is idle. ` +
|
|
1455
|
-
`Just call the tool again immediately.` +
|
|
1456
|
-
autonomousGoals +
|
|
1457
|
-
memoryRefresh +
|
|
1458
|
-
scheduleHint +
|
|
1459
|
-
getReminders(effectiveThreadId),
|
|
1460
|
-
},
|
|
1461
|
-
],
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
// ── report_progress ───────────────────────────────────────────────────────
|
|
1465
|
-
if (name === "report_progress") {
|
|
1466
|
-
const typedArgs = (args ?? {});
|
|
1467
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1468
|
-
if (effectiveThreadId === undefined) {
|
|
1469
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1470
|
-
}
|
|
1471
|
-
const rawMessage = typeof typedArgs?.message === "string"
|
|
1472
|
-
? typedArgs.message
|
|
1473
|
-
: "";
|
|
1474
|
-
if (!rawMessage) {
|
|
1475
|
-
return errorResult("Error: 'message' argument is required for report_progress.");
|
|
1476
|
-
}
|
|
1477
|
-
// Convert standard Markdown to Telegram MarkdownV2.
|
|
1478
|
-
let message;
|
|
1479
|
-
try {
|
|
1480
|
-
message = convertMarkdown(rawMessage);
|
|
1481
|
-
}
|
|
1482
|
-
catch {
|
|
1483
|
-
// Fall back to raw text if Markdown conversion throws.
|
|
1484
|
-
message = rawMessage;
|
|
1485
|
-
}
|
|
1486
|
-
let sentAsPlainText = false;
|
|
1487
|
-
try {
|
|
1488
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, message, "MarkdownV2", effectiveThreadId);
|
|
1489
|
-
}
|
|
1490
|
-
catch (error) {
|
|
1491
|
-
const errMsg = errorMessage(error);
|
|
1492
|
-
// If Telegram rejected the message due to a MarkdownV2 parse error,
|
|
1493
|
-
// retry as plain text using the original un-converted message.
|
|
1494
|
-
const isParseError = errMsg.includes("can't parse entities");
|
|
1495
|
-
if (isParseError) {
|
|
1496
|
-
try {
|
|
1497
|
-
await telegram.sendMessage(TELEGRAM_CHAT_ID, rawMessage, undefined, effectiveThreadId);
|
|
1498
|
-
sentAsPlainText = true;
|
|
1499
|
-
}
|
|
1500
|
-
catch (retryError) {
|
|
1501
|
-
process.stderr.write(`Failed to send progress message via Telegram (plain fallback): ${errorMessage(retryError)}\n`);
|
|
1502
|
-
return errorResult("Error: Failed to send progress update to Telegram even without formatting. " +
|
|
1503
|
-
"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
|
+
}
|
|
1504
1435
|
}
|
|
1505
1436
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
// to prevent the same messages from appearing on repeated calls.
|
|
1516
|
-
let pendingMessages = [];
|
|
1517
|
-
try {
|
|
1518
|
-
const pendingStored = peekThreadMessages(effectiveThreadId);
|
|
1519
|
-
for (const msg of pendingStored) {
|
|
1520
|
-
if (previewedUpdateIds.has(msg.update_id))
|
|
1521
|
-
continue;
|
|
1522
|
-
addPreviewedId(msg.update_id);
|
|
1523
|
-
if (msg.message.photo && msg.message.photo.length > 0) {
|
|
1524
|
-
pendingMessages.push(msg.message.caption
|
|
1525
|
-
? `[Photo received — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1526
|
-
: "[Photo received from operator — will be downloaded when you call wait_for_instructions]");
|
|
1527
|
-
}
|
|
1528
|
-
else if (msg.message.document) {
|
|
1529
|
-
pendingMessages.push(msg.message.caption
|
|
1530
|
-
? `[Document: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions] ${msg.message.caption}`
|
|
1531
|
-
: `[Document received: ${msg.message.document.file_name ?? "file"} — will be downloaded when you call wait_for_instructions]`);
|
|
1532
|
-
}
|
|
1533
|
-
else if (msg.message.voice) {
|
|
1534
|
-
pendingMessages.push(`[Voice message — ${msg.message.voice.duration}s — will be transcribed on next wait]`);
|
|
1535
|
-
}
|
|
1536
|
-
else if (msg.message.video_note) {
|
|
1537
|
-
pendingMessages.push(`[Video note — ${msg.message.video_note.duration}s — will be analyzed on next wait]`);
|
|
1538
|
-
}
|
|
1539
|
-
else if (msg.message.text) {
|
|
1540
|
-
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}`;
|
|
1541
1446
|
}
|
|
1447
|
+
catch (_) { /* non-fatal */ }
|
|
1542
1448
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
: "Progress reported successfully.") + getReminders(effectiveThreadId);
|
|
1551
|
-
const responseText = pendingMessages.length > 0
|
|
1552
|
-
? `${baseStatus}\n\n` +
|
|
1553
|
-
`While you were working, the operator sent additional message(s). ` +
|
|
1554
|
-
`Use those messages to steer your active session: ${pendingMessages.join("\n\n")}` +
|
|
1555
|
-
`\n\n⚠️ HINT from operator: Use subagents (runSubagent) for ALL substantial work. ` +
|
|
1556
|
-
`Subagents have full access to ALL MCP tools. You plan and verify; subagents execute.`
|
|
1557
|
-
: baseStatus + getSubagentNudge();
|
|
1558
|
-
return {
|
|
1559
|
-
content: [
|
|
1560
|
-
{
|
|
1561
|
-
type: "text",
|
|
1562
|
-
text: responseText,
|
|
1563
|
-
},
|
|
1564
|
-
],
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
// ── send_file ─────────────────────────────────────────────────────────────
|
|
1568
|
-
if (name === "send_file") {
|
|
1569
|
-
const typedArgs = (args ?? {});
|
|
1570
|
-
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
1571
|
-
if (effectiveThreadId === undefined) {
|
|
1572
|
-
return errorResult("Error: No active session. Call start_session first, then pass the returned threadId.");
|
|
1573
|
-
}
|
|
1574
|
-
const filePath = typeof typedArgs.filePath === "string" ? typedArgs.filePath.trim() : "";
|
|
1575
|
-
const base64Data = typeof typedArgs.base64 === "string" ? typedArgs.base64 : "";
|
|
1576
|
-
const caption = typeof typedArgs.caption === "string" ? typedArgs.caption : undefined;
|
|
1577
|
-
if (!filePath && !base64Data) {
|
|
1578
|
-
return errorResult("Error: either 'filePath' or 'base64' argument is required for send_file.");
|
|
1579
|
-
}
|
|
1580
|
-
try {
|
|
1581
|
-
let buffer;
|
|
1582
|
-
let filename;
|
|
1583
|
-
if (filePath) {
|
|
1584
|
-
// Read directly from disk — fast, no LLM context overhead.
|
|
1585
|
-
buffer = readFileSync(filePath);
|
|
1586
|
-
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1587
|
-
? typedArgs.filename.trim()
|
|
1588
|
-
: basename(filePath);
|
|
1589
|
-
}
|
|
1590
|
-
else {
|
|
1591
|
-
buffer = Buffer.from(base64Data, "base64");
|
|
1592
|
-
filename = typeof typedArgs.filename === "string" && typedArgs.filename.trim()
|
|
1593
|
-
? typedArgs.filename.trim()
|
|
1594
|
-
: "file";
|
|
1595
|
-
}
|
|
1596
|
-
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
1597
|
-
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
1598
|
-
await telegram.sendPhoto(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1599
|
-
}
|
|
1600
|
-
else {
|
|
1601
|
-
await telegram.sendDocument(TELEGRAM_CHAT_ID, buffer, filename, caption, effectiveThreadId);
|
|
1602
|
-
}
|
|
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
|
+
: "";
|
|
1603
1456
|
return {
|
|
1604
1457
|
content: [
|
|
1605
1458
|
{
|
|
1606
1459
|
type: "text",
|
|
1607
|
-
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),
|
|
1608
1469
|
},
|
|
1609
1470
|
],
|
|
1610
1471
|
};
|
|
1611
1472
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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();
|
|
1641
1567
|
return {
|
|
1642
1568
|
content: [
|
|
1643
1569
|
{
|
|
1644
1570
|
type: "text",
|
|
1645
|
-
text:
|
|
1571
|
+
text: responseText,
|
|
1646
1572
|
},
|
|
1647
1573
|
],
|
|
1648
1574
|
};
|
|
1649
1575
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
+
}
|
|
1653
1625
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
+
}
|
|
1661
1663
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const
|
|
1666
|
-
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
|
+
});
|
|
1667
1688
|
return {
|
|
1668
1689
|
content: [{
|
|
1669
1690
|
type: "text",
|
|
1670
|
-
text:
|
|
1691
|
+
text: `**Scheduled tasks (${tasks.length}):**\n\n${lines.join("\n\n")}` + getReminders(effectiveThreadId),
|
|
1671
1692
|
}],
|
|
1672
1693
|
};
|
|
1673
1694
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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.");
|
|
1691
1716
|
}
|
|
1692
|
-
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`;
|
|
1693
1744
|
return {
|
|
1694
1745
|
content: [{
|
|
1695
1746
|
type: "text",
|
|
1696
|
-
text:
|
|
1697
|
-
|
|
1698
|
-
: `Task ${taskId} not found.` + getReminders(effectiveThreadId),
|
|
1747
|
+
text: `✅ Scheduled: **${label}** [${task.id}]\nTrigger: ${triggerDesc}\nPrompt: ${prompt}` +
|
|
1748
|
+
getReminders(effectiveThreadId),
|
|
1699
1749
|
}],
|
|
1700
1750
|
};
|
|
1701
1751
|
}
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
const task = {
|
|
1719
|
-
id: generateTaskId(),
|
|
1720
|
-
threadId: effectiveThreadId,
|
|
1721
|
-
prompt,
|
|
1722
|
-
label,
|
|
1723
|
-
runAt,
|
|
1724
|
-
cron,
|
|
1725
|
-
afterIdleMinutes,
|
|
1726
|
-
oneShot: runAt != null && !cron,
|
|
1727
|
-
createdAt: new Date().toISOString(),
|
|
1728
|
-
};
|
|
1729
|
-
addSchedule(task);
|
|
1730
|
-
const triggerDesc = cron
|
|
1731
|
-
? `recurring (cron: ${cron})`
|
|
1732
|
-
: runAt
|
|
1733
|
-
? `one-shot at ${runAt}`
|
|
1734
|
-
: `after ${afterIdleMinutes}min of operator silence`;
|
|
1735
|
-
return {
|
|
1736
|
-
content: [{
|
|
1737
|
-
type: "text",
|
|
1738
|
-
text: `✅ Scheduled: **${label}** [${task.id}]\nTrigger: ${triggerDesc}\nPrompt: ${prompt}` +
|
|
1739
|
-
getReminders(effectiveThreadId),
|
|
1740
|
-
}],
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
// ── memory_bootstrap ────────────────────────────────────────────────────
|
|
1744
|
-
if (name === "memory_bootstrap") {
|
|
1745
|
-
const threadId = resolveThreadId(args);
|
|
1746
|
-
if (threadId === undefined) {
|
|
1747
|
-
return { content: [{ type: "text", text: "Error: No active thread. Call start_session first." + getReminders() }] };
|
|
1748
|
-
}
|
|
1749
|
-
try {
|
|
1750
|
-
const db = getMemoryDb();
|
|
1751
|
-
const briefing = assembleBootstrap(db, threadId);
|
|
1752
|
-
return {
|
|
1753
|
-
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getReminders(threadId) }],
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
catch (err) {
|
|
1757
|
-
return { content: [{ type: "text", text: `Memory bootstrap error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
// ── memory_search ───────────────────────────────────────────────────────
|
|
1761
|
-
if (name === "memory_search") {
|
|
1762
|
-
const typedArgs = (args ?? {});
|
|
1763
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1764
|
-
const query = String(typedArgs.query ?? "");
|
|
1765
|
-
if (!query) {
|
|
1766
|
-
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
|
+
}
|
|
1767
1768
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
1772
|
-
const
|
|
1773
|
-
if (
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
+
}
|
|
1779
1789
|
}
|
|
1780
1790
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
+
}
|
|
1788
1798
|
}
|
|
1789
1799
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
+
}
|
|
1804
1814
|
}
|
|
1805
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) }] };
|
|
1806
1823
|
}
|
|
1807
|
-
const text = results.length > 0
|
|
1808
|
-
? results.join("\n")
|
|
1809
|
-
: `No memories found for "${query}".`;
|
|
1810
|
-
return { content: [{ type: "text", text: text + getReminders(threadId) }] };
|
|
1811
|
-
}
|
|
1812
|
-
catch (err) {
|
|
1813
|
-
return { content: [{ type: "text", text: `Memory search error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
// ── memory_save ─────────────────────────────────────────────────────────
|
|
1817
|
-
if (name === "memory_save") {
|
|
1818
|
-
const typedArgs = (args ?? {});
|
|
1819
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1820
|
-
const VALID_TYPES = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
1821
|
-
const noteType = String(typedArgs.type ?? "fact");
|
|
1822
|
-
if (!VALID_TYPES.includes(noteType)) {
|
|
1823
|
-
return errorResult(`Invalid type "${noteType}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1824
|
-
}
|
|
1825
|
-
try {
|
|
1826
|
-
const db = getMemoryDb();
|
|
1827
|
-
const noteId = saveSemanticNote(db, {
|
|
1828
|
-
type: noteType,
|
|
1829
|
-
content: String(typedArgs.content ?? ""),
|
|
1830
|
-
keywords: typedArgs.keywords ?? [],
|
|
1831
|
-
confidence: typeof typedArgs.confidence === "number" ? typedArgs.confidence : 0.8,
|
|
1832
|
-
});
|
|
1833
|
-
return {
|
|
1834
|
-
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) + getSubagentNudge() }],
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
catch (err) {
|
|
1838
|
-
return { content: [{ type: "text", text: `Memory save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1839
1824
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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,
|
|
1853
1841
|
});
|
|
1854
1842
|
return {
|
|
1855
|
-
content: [{ type: "text", text: `
|
|
1843
|
+
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getReminders(threadId) + getSubagentNudge() }],
|
|
1856
1844
|
};
|
|
1857
1845
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
description: String(typedArgs.description ?? ""),
|
|
1862
|
-
steps: typedArgs.steps,
|
|
1863
|
-
triggerConditions: typedArgs.triggerConditions,
|
|
1864
|
-
});
|
|
1865
|
-
return {
|
|
1866
|
-
content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
catch (err) {
|
|
1870
|
-
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
|
+
}
|
|
1871
1849
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
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
|
+
};
|
|
1886
1866
|
}
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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,
|
|
1892
1873
|
});
|
|
1893
1874
|
return {
|
|
1894
|
-
content: [{ type: "text", text: `
|
|
1875
|
+
content: [{ type: "text", text: `Saved procedure: ${procId}` + getReminders(threadId) }],
|
|
1895
1876
|
};
|
|
1896
1877
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
if (typedArgs.newContent)
|
|
1900
|
-
updates.content = String(typedArgs.newContent);
|
|
1901
|
-
if (typeof typedArgs.newConfidence === "number")
|
|
1902
|
-
updates.confidence = typedArgs.newConfidence;
|
|
1903
|
-
updateSemanticNote(db, memId, updates);
|
|
1904
|
-
return {
|
|
1905
|
-
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
if (memId.startsWith("pr_")) {
|
|
1909
|
-
const updates = {};
|
|
1910
|
-
if (typedArgs.newContent)
|
|
1911
|
-
updates.description = String(typedArgs.newContent);
|
|
1912
|
-
if (typeof typedArgs.newConfidence === "number")
|
|
1913
|
-
updates.confidence = typedArgs.newConfidence;
|
|
1914
|
-
updateProcedure(db, memId, updates);
|
|
1915
|
-
return {
|
|
1916
|
-
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
1917
|
-
};
|
|
1878
|
+
catch (err) {
|
|
1879
|
+
return { content: [{ type: "text", text: `Procedure save error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1918
1880
|
}
|
|
1919
|
-
return { content: [{ type: "text", text: `Unknown memory ID format: ${memId}` + getReminders(threadId) }] };
|
|
1920
1881
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
+
}
|
|
1931
1933
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
};
|
|
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() }] };
|
|
1939
1940
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
+
}
|
|
1950
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) }] };
|
|
1951
1965
|
}
|
|
1952
|
-
return { content: [{ type: "text", text: reportLines.join("\n") + getReminders(threadId) }] };
|
|
1953
|
-
}
|
|
1954
|
-
catch (err) {
|
|
1955
|
-
return { content: [{ type: "text", text: `Consolidation error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
// ── memory_status ───────────────────────────────────────────────────────
|
|
1959
|
-
if (name === "memory_status") {
|
|
1960
|
-
const typedArgs = (args ?? {});
|
|
1961
|
-
const threadId = resolveThreadId(typedArgs);
|
|
1962
|
-
if (threadId === undefined) {
|
|
1963
|
-
return { content: [{ type: "text", text: "Error: No active thread." + getReminders() }] };
|
|
1964
1966
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
"
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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
|
+
}
|
|
1982
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) }] };
|
|
1983
1997
|
}
|
|
1984
|
-
return { content: [{ type: "text", text: lines.join("\n") + getReminders(threadId) }] };
|
|
1985
|
-
}
|
|
1986
|
-
catch (err) {
|
|
1987
|
-
return { content: [{ type: "text", text: `Memory status error: ${errorMessage(err)}` + getReminders(threadId) }] };
|
|
1988
1998
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
+
}
|
|
2000
2013
|
return {
|
|
2001
|
-
content: [{ type: "text", text: `
|
|
2014
|
+
content: [{ type: "text", text: `Forgot ${result.layer} memory ${memId} (reason: ${reason})` + getReminders(threadId) }],
|
|
2002
2015
|
};
|
|
2003
2016
|
}
|
|
2004
|
-
|
|
2005
|
-
content: [{ type: "text", text: `
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
catch (err) {
|
|
2009
|
-
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
|
+
}
|
|
2010
2020
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2021
|
+
// Unknown tool
|
|
2022
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
2023
|
+
});
|
|
2024
|
+
return srv;
|
|
2025
|
+
}
|
|
2015
2026
|
// ---------------------------------------------------------------------------
|
|
2016
2027
|
// Start the server
|
|
2017
2028
|
// ---------------------------------------------------------------------------
|
|
@@ -2098,7 +2109,10 @@ if (httpPort) {
|
|
|
2098
2109
|
if (sid)
|
|
2099
2110
|
transports.delete(sid);
|
|
2100
2111
|
};
|
|
2101
|
-
|
|
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);
|
|
2102
2116
|
await transport.handleRequest(req, res, body);
|
|
2103
2117
|
return;
|
|
2104
2118
|
}
|
|
@@ -2149,6 +2163,7 @@ if (httpPort) {
|
|
|
2149
2163
|
else {
|
|
2150
2164
|
// ── stdio transport (default) ───────────────────────────────────────────
|
|
2151
2165
|
const transport = new StdioServerTransport();
|
|
2166
|
+
const server = createMcpServer();
|
|
2152
2167
|
await server.connect(transport);
|
|
2153
2168
|
process.stderr.write("Remote Copilot MCP server running on stdio.\n");
|
|
2154
2169
|
// Close DB on exit for stdio mode too
|