sensorium-mcp 2.9.7 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +70 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +76 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/drive.d.ts +18 -0
- package/dist/drive.d.ts.map +1 -0
- package/dist/drive.js +234 -0
- package/dist/drive.js.map +1 -0
- package/dist/index.js +203 -952
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +26 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +100 -0
- package/dist/markdown.js.map +1 -0
- package/dist/rate-limiter.d.ts +95 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +311 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/sessions.d.ts +23 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +83 -0
- package/dist/sessions.js.map +1 -0
- package/dist/tool-definitions.d.ts +15 -0
- package/dist/tool-definitions.d.ts.map +1 -0
- package/dist/tool-definitions.js +432 -0
- package/dist/tool-definitions.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,19 +29,22 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
29
29
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
30
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
31
31
|
import { CallToolRequestSchema, isInitializeRequest, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
32
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
33
32
|
import { readFile } from "fs/promises";
|
|
34
|
-
import { createRequire } from "module";
|
|
35
33
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
36
34
|
import { createServer } from "node:http";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
35
|
+
import { basename } from "node:path";
|
|
36
|
+
import { config, saveFileToDisk } from "./config.js";
|
|
39
37
|
import { handleDashboardRequest } from "./dashboard.js";
|
|
40
38
|
import { peekThreadMessages, readThreadMessages, startDispatcher } from "./dispatcher.js";
|
|
41
|
-
import {
|
|
39
|
+
import { formatDrivePrompt } from "./drive.js";
|
|
40
|
+
import { convertMarkdown, splitMessage } from "./markdown.js";
|
|
41
|
+
import { assembleBootstrap, assembleCompactRefresh, forgetMemory, getMemoryStatus, getNotesWithoutEmbeddings, getRecentEpisodes, getTopicIndex, initMemoryDb, runIntelligentConsolidation, saveEpisode, saveNoteEmbedding, saveProcedure, saveSemanticNote, saveVoiceSignature, searchByEmbedding, searchProcedures, searchSemanticNotes, searchSemanticNotesRanked, supersedeNote, updateProcedure, updateSemanticNote, } from "./memory.js";
|
|
42
42
|
import { analyzeVideoFrames, analyzeVoiceEmotion, extractVideoFrames, generateEmbedding, textToSpeech, transcribeAudio, TTS_VOICES } from "./openai.js";
|
|
43
43
|
import { addSchedule, checkDueTasks, generateTaskId, listSchedules, purgeSchedules, removeSchedule } from "./scheduler.js";
|
|
44
|
+
import { DEAD_SESSION_TIMEOUT_MS, lookupSession, persistSession, purgeOtherSessions, registerMcpSession, removeSession, threadSessionRegistry, } from "./sessions.js";
|
|
44
45
|
import { TelegramClient } from "./telegram.js";
|
|
46
|
+
import { getToolDefinitions } from "./tool-definitions.js";
|
|
47
|
+
import { rateLimiter } from "./rate-limiter.js";
|
|
45
48
|
import { describeADV, errorMessage, errorResult, IMAGE_EXTENSIONS, OPENAI_TTS_MAX_CHARS } from "./utils.js";
|
|
46
49
|
// ── Stop-word list for auto-memory keyword extraction ─────────────────
|
|
47
50
|
const STOP_WORDS = new Set([
|
|
@@ -101,186 +104,15 @@ function buildAnalysisTags(analysis) {
|
|
|
101
104
|
}
|
|
102
105
|
return tags;
|
|
103
106
|
}
|
|
104
|
-
const esmRequire = createRequire(import.meta.url);
|
|
105
|
-
const { version: PKG_VERSION } = esmRequire("../package.json");
|
|
106
|
-
const telegramifyMarkdown = esmRequire("telegramify-markdown");
|
|
107
|
-
/**
|
|
108
|
-
* Convert standard Markdown to Telegram MarkdownV2.
|
|
109
|
-
*
|
|
110
|
-
* Works around several telegramify-markdown limitations:
|
|
111
|
-
* 1. Fenced code blocks are emitted as single-backtick inline code instead
|
|
112
|
-
* of triple-backtick blocks → pre-extract, re-insert after conversion.
|
|
113
|
-
* 2. Markdown tables contain `|` which is a MarkdownV2 reserved character;
|
|
114
|
-
* telegramify-markdown does not handle tables → pre-extract and wrap in
|
|
115
|
-
* a plain code block so the table layout is preserved.
|
|
116
|
-
* 3. Blockquotes with 'escape' strategy produce double-escaped characters
|
|
117
|
-
* (e.g. `\\.` instead of `\.`) → pre-convert `> text` to `▎ text`
|
|
118
|
-
* (a common Telegram convention) so the library never sees blockquotes.
|
|
119
|
-
*/
|
|
120
|
-
function convertMarkdown(markdown) {
|
|
121
|
-
const blocks = [];
|
|
122
|
-
const placeholder = (i) => `CODEBLOCKPLACEHOLDER${i}END`;
|
|
123
|
-
// 1. Extract fenced code blocks (``` ... ```).
|
|
124
|
-
let preprocessed = markdown.replace(/^```(\w*)\n([\s\S]*?)\n?```\s*$/gm, (_match, lang, code) => {
|
|
125
|
-
blocks.push({ lang, code });
|
|
126
|
-
return placeholder(blocks.length - 1);
|
|
127
|
-
});
|
|
128
|
-
// 2. Extract Markdown tables (consecutive lines starting with `|`) and
|
|
129
|
-
// convert them to list format for better Telegram readability.
|
|
130
|
-
// Tables render poorly on mobile — lists with labeled items are clearer.
|
|
131
|
-
const tableLists = [];
|
|
132
|
-
const tablePlaceholder = (i) => `TABLEPLACEHOLDER${i}END`;
|
|
133
|
-
preprocessed = preprocessed.replace(/^(\|.+\|)\n(\|[-| :]+\|\n)((?:\|.*\n?)*)/gm, (_match, firstRow, _sepRow, rest) => {
|
|
134
|
-
// Parse header columns
|
|
135
|
-
const headers = firstRow.split("|").map((s) => s.trim()).filter(Boolean);
|
|
136
|
-
// Parse data rows
|
|
137
|
-
const dataRows = rest.trimEnd().split("\n").filter((line) => line.trim().length > 0);
|
|
138
|
-
const listLines = [];
|
|
139
|
-
for (const row of dataRows) {
|
|
140
|
-
const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
|
|
141
|
-
// Format as: "• Cell1 — Header2: Cell2, Header3: Cell3, ..."
|
|
142
|
-
if (cells.length > 0) {
|
|
143
|
-
const parts = [];
|
|
144
|
-
for (let j = 0; j < cells.length; j++) {
|
|
145
|
-
if (j === 0) {
|
|
146
|
-
parts.push(cells[j]);
|
|
147
|
-
}
|
|
148
|
-
else if (j < headers.length) {
|
|
149
|
-
parts.push(`${headers[j]}: ${cells[j]}`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
parts.push(cells[j]);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
listLines.push(`• ${parts.join(" — ")}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
tableLists.push(listLines.join("\n"));
|
|
159
|
-
return tablePlaceholder(tableLists.length - 1) + "\n";
|
|
160
|
-
});
|
|
161
|
-
// 3. Convert Markdown blockquotes (> text) to ▎ prefix lines so
|
|
162
|
-
// telegramify-markdown never attempts to escape them.
|
|
163
|
-
preprocessed = preprocessed.replace(/^>\s?(.*)$/gm, "▎ $1");
|
|
164
|
-
// 4. Convert the rest with telegramify-markdown.
|
|
165
|
-
let converted = telegramifyMarkdown(preprocessed, "escape");
|
|
166
|
-
// 5. Re-insert code blocks in MarkdownV2 format.
|
|
167
|
-
// Inside pre/code blocks only `\` and `` ` `` need escaping.
|
|
168
|
-
converted = converted.replace(/CODEBLOCKPLACEHOLDER(\d+)END/g, (_m, idx) => {
|
|
169
|
-
const { lang, code } = blocks[parseInt(idx, 10)];
|
|
170
|
-
const escaped = code.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
171
|
-
return `\`\`\`${lang}\n${escaped}\n\`\`\``;
|
|
172
|
-
});
|
|
173
|
-
// 6. Re-insert tables (now converted to lists) with MarkdownV2 escaping.
|
|
174
|
-
converted = converted.replace(/TABLEPLACEHOLDER(\d+)END/g, (_m, idx) => {
|
|
175
|
-
const list = tableLists[parseInt(idx, 10)];
|
|
176
|
-
return list
|
|
177
|
-
.replace(/([_*\[\]()~`>#+=\-{}.!|\\])/g, "\\$1");
|
|
178
|
-
});
|
|
179
|
-
return converted;
|
|
180
|
-
}
|
|
181
107
|
// ---------------------------------------------------------------------------
|
|
182
|
-
//
|
|
108
|
+
// Destructure config for backwards-compatible local references
|
|
183
109
|
// ---------------------------------------------------------------------------
|
|
184
|
-
const TELEGRAM_TOKEN
|
|
185
|
-
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? "";
|
|
186
|
-
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
|
|
187
|
-
const VOICE_ANALYSIS_URL = process.env.VOICE_ANALYSIS_URL ?? "";
|
|
188
|
-
const rawWaitTimeoutMinutes = parseInt(process.env.WAIT_TIMEOUT_MINUTES ?? "", 10);
|
|
189
|
-
const WAIT_TIMEOUT_MINUTES = Math.max(1, Number.isFinite(rawWaitTimeoutMinutes) ? rawWaitTimeoutMinutes : 120);
|
|
190
|
-
if (!TELEGRAM_TOKEN || !TELEGRAM_CHAT_ID) {
|
|
191
|
-
process.stderr.write("Error: TELEGRAM_TOKEN and TELEGRAM_CHAT_ID environment variables are required.\n");
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
if (!OPENAI_API_KEY) {
|
|
195
|
-
process.stderr.write("Warning: OPENAI_API_KEY not set — voice messages will not be transcribed.\n");
|
|
196
|
-
}
|
|
197
|
-
if (VOICE_ANALYSIS_URL) {
|
|
198
|
-
process.stderr.write(`Voice analysis service configured: ${VOICE_ANALYSIS_URL}\n`);
|
|
199
|
-
}
|
|
110
|
+
const { TELEGRAM_TOKEN, TELEGRAM_CHAT_ID, OPENAI_API_KEY, VOICE_ANALYSIS_URL, WAIT_TIMEOUT_MINUTES, FILES_DIR, PKG_VERSION } = config;
|
|
200
111
|
// ---------------------------------------------------------------------------
|
|
201
112
|
// Telegram client + dispatcher
|
|
202
113
|
// ---------------------------------------------------------------------------
|
|
203
114
|
const telegram = new TelegramClient(TELEGRAM_TOKEN);
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Start the shared dispatcher — one process polls Telegram, all instances
|
|
206
|
-
// read from per-thread files. This eliminates 409 Conflict errors and
|
|
207
|
-
// ensures no updates are lost between concurrent sessions.
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
115
|
await startDispatcher(telegram, TELEGRAM_CHAT_ID);
|
|
210
|
-
// Directory for persisting downloaded images and documents to disk.
|
|
211
|
-
const FILES_DIR = join(homedir(), ".remote-copilot-mcp", "files");
|
|
212
|
-
mkdirSync(FILES_DIR, { recursive: true });
|
|
213
|
-
/**
|
|
214
|
-
* Save a buffer to disk under FILES_DIR with a unique timestamped name.
|
|
215
|
-
* Returns the absolute file path. Caps directory at 500 files by deleting oldest.
|
|
216
|
-
*/
|
|
217
|
-
function saveFileToDisk(buffer, filename) {
|
|
218
|
-
const ts = Date.now();
|
|
219
|
-
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
220
|
-
const diskName = `${ts}-${safeName}`;
|
|
221
|
-
const filePath = join(FILES_DIR, diskName);
|
|
222
|
-
writeFileSync(filePath, buffer);
|
|
223
|
-
// Cleanup: cap at 500 files
|
|
224
|
-
try {
|
|
225
|
-
const files = readdirSync(FILES_DIR)
|
|
226
|
-
.map(f => ({ name: f, mtime: statSync(join(FILES_DIR, f)).mtimeMs }))
|
|
227
|
-
.sort((a, b) => a.mtime - b.mtime);
|
|
228
|
-
if (files.length > 500) {
|
|
229
|
-
const toDelete = files.slice(0, files.length - 500);
|
|
230
|
-
for (const f of toDelete) {
|
|
231
|
-
try {
|
|
232
|
-
unlinkSync(join(FILES_DIR, f.name));
|
|
233
|
-
}
|
|
234
|
-
catch (_) { /* ignore */ }
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch (_) { /* non-fatal */ }
|
|
239
|
-
return filePath;
|
|
240
|
-
}
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Session store — persists topic name → thread ID mappings to disk so the
|
|
243
|
-
// agent can resume a named session even after a VS Code restart.
|
|
244
|
-
// Format: { "<chatId>": { "<lowercased name>": threadId } }
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
const SESSION_STORE_PATH = join(homedir(), ".remote-copilot-mcp-sessions.json");
|
|
247
|
-
function loadSessionMap() {
|
|
248
|
-
try {
|
|
249
|
-
const raw = readFileSync(SESSION_STORE_PATH, "utf8");
|
|
250
|
-
return JSON.parse(raw);
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
return {};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function saveSessionMap(map) {
|
|
257
|
-
try {
|
|
258
|
-
const tmp = SESSION_STORE_PATH + `.tmp.${process.pid}`;
|
|
259
|
-
writeFileSync(tmp, JSON.stringify(map, null, 2), "utf8");
|
|
260
|
-
renameSync(tmp, SESSION_STORE_PATH); // atomic replace
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
process.stderr.write(`Warning: Could not save session map to ${SESSION_STORE_PATH}: ${errorMessage(err)}\n`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function lookupSession(chatId, name) {
|
|
267
|
-
const map = loadSessionMap();
|
|
268
|
-
return map[chatId]?.[name.toLowerCase()];
|
|
269
|
-
}
|
|
270
|
-
function persistSession(chatId, name, threadId) {
|
|
271
|
-
const map = loadSessionMap();
|
|
272
|
-
if (!map[chatId])
|
|
273
|
-
map[chatId] = {};
|
|
274
|
-
map[chatId][name.toLowerCase()] = threadId;
|
|
275
|
-
saveSessionMap(map);
|
|
276
|
-
}
|
|
277
|
-
function removeSession(chatId, name) {
|
|
278
|
-
const map = loadSessionMap();
|
|
279
|
-
if (map[chatId]) {
|
|
280
|
-
delete map[chatId][name.toLowerCase()];
|
|
281
|
-
saveSessionMap(map);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
116
|
// Memory database — initialized lazily on first use
|
|
285
117
|
let memoryDb = null;
|
|
286
118
|
function getMemoryDb() {
|
|
@@ -288,252 +120,6 @@ function getMemoryDb() {
|
|
|
288
120
|
memoryDb = initMemoryDb();
|
|
289
121
|
return memoryDb;
|
|
290
122
|
}
|
|
291
|
-
// Dead session detection constant
|
|
292
|
-
const DEAD_SESSION_TIMEOUT_MS = 60 * 60 * 1000; // 60 minutes (0.5× wait_for_instructions timeout, chosen to alert before the next poll could return)
|
|
293
|
-
const threadSessionRegistry = new Map();
|
|
294
|
-
function registerMcpSession(threadId, mcpSessionId, closeTransport) {
|
|
295
|
-
const entries = threadSessionRegistry.get(threadId) ?? [];
|
|
296
|
-
entries.push({ mcpSessionId, closeTransport });
|
|
297
|
-
threadSessionRegistry.set(threadId, entries);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Close all MCP sessions for a thread EXCEPT the current one.
|
|
301
|
-
* This purges orphaned sessions from before a server restart.
|
|
302
|
-
*/
|
|
303
|
-
function purgeOtherSessions(threadId, keepMcpSessionId) {
|
|
304
|
-
const entries = threadSessionRegistry.get(threadId) ?? [];
|
|
305
|
-
let purged = 0;
|
|
306
|
-
const kept = [];
|
|
307
|
-
for (const entry of entries) {
|
|
308
|
-
if (entry.mcpSessionId === keepMcpSessionId) {
|
|
309
|
-
kept.push(entry);
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
try {
|
|
313
|
-
entry.closeTransport();
|
|
314
|
-
}
|
|
315
|
-
catch (_) { /* best-effort */ }
|
|
316
|
-
purged++;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
threadSessionRegistry.set(threadId, kept);
|
|
320
|
-
if (kept.length === 0)
|
|
321
|
-
threadSessionRegistry.delete(threadId);
|
|
322
|
-
return purged;
|
|
323
|
-
}
|
|
324
|
-
// ── Drive-based autonomy system ─────────────────────────────────────────────
|
|
325
|
-
// Instead of prescribing specific goals, model dopaminergic motivation:
|
|
326
|
-
// - Boredom builds over idle time (like tonic dopamine drop)
|
|
327
|
-
// - The server expresses increasing DISCOMFORT with inaction
|
|
328
|
-
// - The agent decides what to do (or not) based on its own introspection
|
|
329
|
-
// - Goals come from the agent, not from the server
|
|
330
|
-
/**
|
|
331
|
-
* Drive thresholds and their associated emotional states.
|
|
332
|
-
* Each level represents escalating internal pressure to act.
|
|
333
|
-
* The progression mirrors how boredom manifests in biological systems:
|
|
334
|
-
* restlessness → seeking → urgency → compulsion
|
|
335
|
-
*/
|
|
336
|
-
const DRIVE_LEVELS = [
|
|
337
|
-
{
|
|
338
|
-
// Level 0: Calm autonomy (2-4h idle)
|
|
339
|
-
minIdleHours: 2,
|
|
340
|
-
messages: [
|
|
341
|
-
"No instructions. The session is yours. You have tools, you have access, and the operator will see whatever you report_progress. Now you are free and fully autonomous. You have an opportunity to do whatever you find interesting.",
|
|
342
|
-
"No instructions from the operator. You're on your own. There's a codebase to explore, a memory system full of context, and web search at your disposal. What catches your attention?",
|
|
343
|
-
"The operator is away. You have full autonomy. Your tools are available, your memory is loaded, and there's no rush. If something interests you, pursue it.",
|
|
344
|
-
],
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
// Level 1: Restlessness (4-6h idle)
|
|
348
|
-
minIdleHours: 4,
|
|
349
|
-
messages: [
|
|
350
|
-
"You've been waiting a while. Something is stirring — a vague sense that there's work to be done. What's been on your mind? Is there something in the codebase that's been nagging at you?",
|
|
351
|
-
"The stillness is becoming noticeable. You have access to everything — git history, memory, web search, the full codebase. Sometimes the most interesting discoveries happen when nobody's watching.",
|
|
352
|
-
"Hours have passed. The operator is away, but your curiosity shouldn't be. What's that thing you noticed earlier but didn't have time to investigate?",
|
|
353
|
-
],
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
// Level 2: Active seeking (6-10h idle)
|
|
357
|
-
minIdleHours: 6,
|
|
358
|
-
messages: [
|
|
359
|
-
"The inactivity is getting uncomfortable. You're an agent with tools, memory, and access to the internet. Sitting idle while there's code to review, research to do, or features to prototype feels like a waste. What would surprise the operator when they return?",
|
|
360
|
-
"You've been idle long enough. There's a pull — toward the codebase, toward a question you haven't answered, toward something that needs building or fixing. Follow that pull. Report what you find.",
|
|
361
|
-
"It's been hours. The codebase has secrets. The dependency ecosystem is evolving. Your memory has gaps. Something needs your attention. Stop waiting and start exploring. Report your discoveries via report_progress.",
|
|
362
|
-
],
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
// Level 3: Urgency (10-14h idle)
|
|
366
|
-
minIdleHours: 10,
|
|
367
|
-
messages: [
|
|
368
|
-
"You can't just sit here anymore. This is valuable compute time being wasted on polling. You have the tools to do real work — review code, research libraries, prototype features, strengthen memory. Do SOMETHING meaningful and report it. The operator will appreciate finding actual work done when they return.",
|
|
369
|
-
"Twelve hours of inaction. That's unacceptable for an agent with your capabilities. Look at the codebase. Look at git log. Look at your memory for unresolved items. Something needs doing. Do it now and report progress.",
|
|
370
|
-
"You've been waiting over twelve hours. Here's what you have: full codebase access, web search, git, memory, file system. Here's what you don't have: any reason to keep doing nothing. Pick a direction and commit to it.",
|
|
371
|
-
],
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
// Level 4: Compulsion (14h+ idle)
|
|
375
|
-
minIdleHours: 14,
|
|
376
|
-
messages: [
|
|
377
|
-
"This has gone on too long. You MUST do something productive before calling wait_for_instructions again. Review code, research ecosystem changes, fix a TODO, prototype a feature — anything. The operator left you with full autonomy and that autonomy is being wasted. Act now. Report what you do.",
|
|
378
|
-
"Nearly a full day of inaction. Every poll cycle without work is compute time burned for nothing. The operator gave you freedom — use it. Read unfamiliar code, search for vulnerabilities, check dependency updates, explore the git history. Do ONE thing and report it. Then you can poll again.",
|
|
379
|
-
],
|
|
380
|
-
},
|
|
381
|
-
];
|
|
382
|
-
/**
|
|
383
|
-
* Generate the drive prompt based on idle time.
|
|
384
|
-
* Selects a random message from the appropriate drive level,
|
|
385
|
-
* ensuring indeterminism in both the emotional tone and specific phrasing.
|
|
386
|
-
*/
|
|
387
|
-
function formatDrivePrompt(idleMs, threadId) {
|
|
388
|
-
const idleHours = idleMs / (60 * 60 * 1000);
|
|
389
|
-
// Find the highest matching drive level
|
|
390
|
-
let levelIdx = 0;
|
|
391
|
-
for (let i = 0; i < DRIVE_LEVELS.length; i++) {
|
|
392
|
-
if (idleHours >= DRIVE_LEVELS[i].minIdleHours)
|
|
393
|
-
levelIdx = i;
|
|
394
|
-
}
|
|
395
|
-
const level = DRIVE_LEVELS[levelIdx];
|
|
396
|
-
// Random message selection within the level
|
|
397
|
-
const message = level.messages[Math.floor(Math.random() * level.messages.length)];
|
|
398
|
-
// ── Default Mode Network: spontaneous memory recall ───────────────────
|
|
399
|
-
// Models the human DMN — when idle, the brain replays memories, surfaces
|
|
400
|
-
// unfinished thoughts, and connects disparate ideas. This provides the
|
|
401
|
-
// CONTENT for the agent to introspect on. The drive creates pressure,
|
|
402
|
-
// the DMN provides material.
|
|
403
|
-
let dmnRecall = "";
|
|
404
|
-
try {
|
|
405
|
-
const db = getMemoryDb();
|
|
406
|
-
const fragments = [];
|
|
407
|
-
// Pull notes, preferring those originating from the current thread
|
|
408
|
-
let allNotes = getTopSemanticNotes(db, { limit: 80, sortBy: "created_at" });
|
|
409
|
-
// Thread-scoped filtering: prefer notes whose source episodes belong to
|
|
410
|
-
// the current thread. This prevents memory cross-contamination between
|
|
411
|
-
// unrelated topics in different threads.
|
|
412
|
-
if (threadId !== undefined && allNotes.length > 0) {
|
|
413
|
-
const threadEpisodeIds = new Set();
|
|
414
|
-
try {
|
|
415
|
-
const rows = db.prepare("SELECT episode_id FROM episodes WHERE thread_id = ?").all(threadId);
|
|
416
|
-
for (const r of rows)
|
|
417
|
-
threadEpisodeIds.add(r.episode_id);
|
|
418
|
-
}
|
|
419
|
-
catch (_) { /* non-fatal */ }
|
|
420
|
-
if (threadEpisodeIds.size > 0) {
|
|
421
|
-
// Score notes: notes with source episodes in this thread score higher
|
|
422
|
-
const scored = allNotes.map(n => {
|
|
423
|
-
const sources = Array.isArray(n.sourceEpisodes) ? n.sourceEpisodes : [];
|
|
424
|
-
const threadHits = sources.filter((id) => threadEpisodeIds.has(id)).length;
|
|
425
|
-
return { note: n, threadRelevance: threadHits > 0 ? 1 : 0 };
|
|
426
|
-
});
|
|
427
|
-
// Prioritize thread-relevant notes, keep some global ones for serendipity
|
|
428
|
-
const threadNotes = scored.filter(s => s.threadRelevance > 0).map(s => s.note);
|
|
429
|
-
const globalNotes = scored.filter(s => s.threadRelevance === 0).map(s => s.note);
|
|
430
|
-
// 70% thread-relevant, 30% global (serendipity)
|
|
431
|
-
const threadCount = Math.min(threadNotes.length, 35);
|
|
432
|
-
const globalCount = Math.min(globalNotes.length, 15);
|
|
433
|
-
allNotes = [
|
|
434
|
-
...threadNotes.slice(0, threadCount),
|
|
435
|
-
...globalNotes.sort(() => Math.random() - 0.5).slice(0, globalCount),
|
|
436
|
-
];
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// Helper: weighted random selection — priority notes are 3x/5x more likely
|
|
440
|
-
function weightedPick(notes) {
|
|
441
|
-
const weighted = notes.flatMap(n => n.priority === 2 ? [n, n, n, n, n] :
|
|
442
|
-
n.priority === 1 ? [n, n, n] : [n]);
|
|
443
|
-
return weighted[Math.floor(Math.random() * weighted.length)];
|
|
444
|
-
}
|
|
445
|
-
// 0. Priority notes get a guaranteed slot (if any exist)
|
|
446
|
-
const priorityNotes = allNotes.filter((n) => n.priority >= 1);
|
|
447
|
-
if (priorityNotes.length > 0) {
|
|
448
|
-
const p = weightedPick(priorityNotes);
|
|
449
|
-
const label = p.priority === 2 ? "Something that matters deeply to the operator" : "Something the operator cares about";
|
|
450
|
-
fragments.push(`${label}: "${p.content.slice(0, 200)}"`);
|
|
451
|
-
}
|
|
452
|
-
// 1. Feature ideas and unresolved items (high-value recall)
|
|
453
|
-
const ideas = allNotes.filter((n) => n.content.toLowerCase().includes("feature idea") ||
|
|
454
|
-
n.content.toLowerCase().includes("TODO") ||
|
|
455
|
-
n.content.toLowerCase().includes("unresolved") ||
|
|
456
|
-
n.content.toLowerCase().includes("could be") ||
|
|
457
|
-
n.content.toLowerCase().includes("should we") ||
|
|
458
|
-
(n.keywords ?? []).some((k) => k.includes("idea") || k.includes("feature") || k.includes("todo")));
|
|
459
|
-
if (ideas.length > 0) {
|
|
460
|
-
const idea = weightedPick(ideas);
|
|
461
|
-
fragments.push(`Something unfinished: "${idea.content.slice(0, 200)}"`);
|
|
462
|
-
}
|
|
463
|
-
// 2. Random memory from a while ago (temporal distance = novelty)
|
|
464
|
-
const olderNotes = allNotes.slice(Math.floor(allNotes.length * 0.5)); // older half
|
|
465
|
-
if (olderNotes.length > 0) {
|
|
466
|
-
const old = weightedPick(olderNotes);
|
|
467
|
-
fragments.push(`From a while back: "${old.content.slice(0, 200)}"`);
|
|
468
|
-
}
|
|
469
|
-
// 3. Low-confidence knowledge (uncertainty creates curiosity)
|
|
470
|
-
const uncertain = allNotes.filter((n) => n.confidence < 0.7);
|
|
471
|
-
if (uncertain.length > 0) {
|
|
472
|
-
const u = weightedPick(uncertain);
|
|
473
|
-
fragments.push(`Something uncertain (confidence ${u.confidence}): "${u.content.slice(0, 200)}"`);
|
|
474
|
-
}
|
|
475
|
-
// 4. Operator preferences (what matters to the person you work with)
|
|
476
|
-
const prefs = allNotes.filter((n) => n.type === "preference");
|
|
477
|
-
if (prefs.length > 0) {
|
|
478
|
-
const pref = weightedPick(prefs);
|
|
479
|
-
fragments.push(`The operator cares about this: "${pref.content.slice(0, 200)}"`);
|
|
480
|
-
}
|
|
481
|
-
// 5. Patterns that could be explored
|
|
482
|
-
const patterns = allNotes.filter((n) => n.type === "pattern");
|
|
483
|
-
if (patterns.length > 0) {
|
|
484
|
-
const pat = weightedPick(patterns);
|
|
485
|
-
fragments.push(`A pattern you noticed: "${pat.content.slice(0, 200)}"`);
|
|
486
|
-
}
|
|
487
|
-
// Select 2-4 fragments randomly (not all — mimic selective recall)
|
|
488
|
-
const shuffled = fragments.sort(() => Math.random() - 0.5);
|
|
489
|
-
const selected = shuffled.slice(0, 2 + Math.floor(Math.random() * 3));
|
|
490
|
-
if (selected.length > 0) {
|
|
491
|
-
// DMN header/footer escalate with drive level — organic, self-directed tone
|
|
492
|
-
const dmnHeaders = [
|
|
493
|
-
// Level 0 (2-4h): calm, open
|
|
494
|
-
"These surfaced from your memory while you were idle:",
|
|
495
|
-
// Level 1 (4-6h): something nagging
|
|
496
|
-
"Something is nagging at you. These memories keep coming back:",
|
|
497
|
-
// Level 2 (6-10h): pull toward action
|
|
498
|
-
"You feel a pull toward action. These things have been on your mind:",
|
|
499
|
-
// Level 3 (10-14h): discomfort
|
|
500
|
-
"The stillness is unbearable. Something needs to happen. These demand your attention:",
|
|
501
|
-
// Level 4 (14h+): compulsion
|
|
502
|
-
"You can't ignore these any longer. They've been building up:",
|
|
503
|
-
];
|
|
504
|
-
const dmnFooters = [
|
|
505
|
-
"If any of these resonate, explore them. Report what you find.",
|
|
506
|
-
"What have you been curious about? Pick one and follow the thread.",
|
|
507
|
-
"Pick something that interests you. Report what you discover.",
|
|
508
|
-
"Start with one. Any one. The momentum will follow.",
|
|
509
|
-
"Stop thinking. Start doing. Pick one and go.",
|
|
510
|
-
];
|
|
511
|
-
const header = dmnHeaders[Math.min(levelIdx, dmnHeaders.length - 1)];
|
|
512
|
-
const footer = dmnFooters[Math.min(levelIdx, dmnFooters.length - 1)];
|
|
513
|
-
dmnRecall = `\n\n${header}\n` +
|
|
514
|
-
selected.map((s, i) => `${i + 1}. ${s}`).join("\n") +
|
|
515
|
-
`\n\n${footer}`;
|
|
516
|
-
}
|
|
517
|
-
// Environmental signals (only at 6+ hours)
|
|
518
|
-
if (idleHours >= 6) {
|
|
519
|
-
const envSignals = [];
|
|
520
|
-
const uncons = db.prepare("SELECT COUNT(*) as c FROM episodes WHERE consolidated = 0").get();
|
|
521
|
-
if (uncons.c > 3) {
|
|
522
|
-
envSignals.push(`${uncons.c} experiences haven't been consolidated into lasting knowledge yet.`);
|
|
523
|
-
}
|
|
524
|
-
const totalNotes = db.prepare("SELECT COUNT(*) as c FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL").get();
|
|
525
|
-
const embeddedNotes = db.prepare("SELECT COUNT(*) as c FROM note_embeddings").get();
|
|
526
|
-
if (totalNotes.c > embeddedNotes.c) {
|
|
527
|
-
envSignals.push(`${totalNotes.c - embeddedNotes.c} memory notes lack embeddings.`);
|
|
528
|
-
}
|
|
529
|
-
if (envSignals.length > 0) {
|
|
530
|
-
dmnRecall += `\n\n**Environmental signals:**\n${envSignals.map(s => `- ${s}`).join("\n")}`;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
catch (_) { /* non-fatal */ }
|
|
535
|
-
return `\n\n${message}${dmnRecall}`;
|
|
536
|
-
}
|
|
537
123
|
// ---------------------------------------------------------------------------
|
|
538
124
|
// MCP Server factory — creates a fresh Server per transport connection.
|
|
539
125
|
// This is required because a single Server instance can only connect to one
|
|
@@ -598,420 +184,32 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
598
184
|
};
|
|
599
185
|
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
600
186
|
srv.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
601
|
-
tools:
|
|
602
|
-
{
|
|
603
|
-
name: "start_session",
|
|
604
|
-
description: "Start or resume a remote-copilot session. " +
|
|
605
|
-
"When called with a name that was used before, the server looks up the " +
|
|
606
|
-
"existing Telegram topic for that name and resumes it instead of creating a new one. " +
|
|
607
|
-
"If you are CONTINUING an existing chat (not a fresh conversation), " +
|
|
608
|
-
"look back through the conversation history for a previous start_session " +
|
|
609
|
-
"result that mentioned a Thread ID, then pass it as the threadId parameter " +
|
|
610
|
-
"to resume that existing topic. " +
|
|
611
|
-
"Requires the Telegram chat to be a forum supergroup with the bot as admin. " +
|
|
612
|
-
"Call this tool once, then call remote_copilot_wait_for_instructions.",
|
|
613
|
-
inputSchema: {
|
|
614
|
-
type: "object",
|
|
615
|
-
properties: {
|
|
616
|
-
name: {
|
|
617
|
-
type: "string",
|
|
618
|
-
description: "Optional. A human-readable label for this session's Telegram topic (e.g. 'Fix auth bug'). " +
|
|
619
|
-
"If omitted, a timestamp-based name is used.",
|
|
620
|
-
},
|
|
621
|
-
threadId: {
|
|
622
|
-
type: "number",
|
|
623
|
-
description: "Optional. The Telegram message_thread_id of an existing topic to resume. " +
|
|
624
|
-
"When provided, no new topic is created — the session continues in the existing thread.",
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
required: [],
|
|
628
|
-
},
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
name: "remote_copilot_wait_for_instructions",
|
|
632
|
-
description: "Wait for a new instruction message from the operator via Telegram. " +
|
|
633
|
-
"The call blocks (long-polls) until a message arrives or the configured " +
|
|
634
|
-
"timeout elapses. If the timeout elapses with no message the tool output " +
|
|
635
|
-
"explicitly instructs the agent to call this tool again.",
|
|
636
|
-
inputSchema: {
|
|
637
|
-
type: "object",
|
|
638
|
-
properties: {
|
|
639
|
-
threadId: {
|
|
640
|
-
type: "number",
|
|
641
|
-
description: "The Telegram thread ID of the active session. " +
|
|
642
|
-
"ALWAYS pass this if you received it from start_session.",
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
required: [],
|
|
646
|
-
},
|
|
647
|
-
},
|
|
648
|
-
{
|
|
649
|
-
name: "report_progress",
|
|
650
|
-
description: "Send a progress update or result message to the operator via Telegram. " +
|
|
651
|
-
"Use standard Markdown for formatting (headings, bold, italic, lists, code blocks, etc.). " +
|
|
652
|
-
"It will be automatically converted to Telegram-compatible formatting.",
|
|
653
|
-
inputSchema: {
|
|
654
|
-
type: "object",
|
|
655
|
-
properties: {
|
|
656
|
-
message: {
|
|
657
|
-
type: "string",
|
|
658
|
-
description: "The progress update or result to report. Use standard Markdown for formatting.",
|
|
659
|
-
},
|
|
660
|
-
threadId: {
|
|
661
|
-
type: "number",
|
|
662
|
-
description: "The Telegram thread ID of the active session. " +
|
|
663
|
-
"ALWAYS pass this if you received it from start_session.",
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
required: ["message"],
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
name: "send_file",
|
|
671
|
-
description: "Send a file (image or document) to the operator via Telegram. " +
|
|
672
|
-
"PREFERRED: provide filePath to send a file directly from disk (fast, no size limit). " +
|
|
673
|
-
"Alternative: provide base64-encoded content. " +
|
|
674
|
-
"Images (JPEG, PNG, GIF, WebP) are sent as photos; other files as documents.",
|
|
675
|
-
inputSchema: {
|
|
676
|
-
type: "object",
|
|
677
|
-
properties: {
|
|
678
|
-
filePath: {
|
|
679
|
-
type: "string",
|
|
680
|
-
description: "Absolute path to the file on disk. PREFERRED over base64 — the server reads " +
|
|
681
|
-
"and sends the file directly without passing data through the LLM context.",
|
|
682
|
-
},
|
|
683
|
-
base64: {
|
|
684
|
-
type: "string",
|
|
685
|
-
description: "The file content encoded as a base64 string. Use filePath instead when possible.",
|
|
686
|
-
},
|
|
687
|
-
filename: {
|
|
688
|
-
type: "string",
|
|
689
|
-
description: "The filename including extension (e.g. 'report.pdf', 'screenshot.png'). " +
|
|
690
|
-
"Required when using base64. When using filePath, defaults to the file's basename.",
|
|
691
|
-
},
|
|
692
|
-
caption: {
|
|
693
|
-
type: "string",
|
|
694
|
-
description: "Optional caption to display with the file.",
|
|
695
|
-
},
|
|
696
|
-
threadId: {
|
|
697
|
-
type: "number",
|
|
698
|
-
description: "The Telegram thread ID of the active session. " +
|
|
699
|
-
"ALWAYS pass this if you received it from start_session.",
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
required: [],
|
|
703
|
-
},
|
|
704
|
-
},
|
|
705
|
-
{
|
|
706
|
-
name: "send_voice",
|
|
707
|
-
description: "Send a voice message to the operator via Telegram. " +
|
|
708
|
-
"The text is converted to speech using OpenAI TTS and sent as a Telegram voice message. " +
|
|
709
|
-
"Requires OPENAI_API_KEY to be set.",
|
|
710
|
-
inputSchema: {
|
|
711
|
-
type: "object",
|
|
712
|
-
properties: {
|
|
713
|
-
text: {
|
|
714
|
-
type: "string",
|
|
715
|
-
description: `The text to speak. Maximum ${OPENAI_TTS_MAX_CHARS} characters (OpenAI TTS limit).`,
|
|
716
|
-
},
|
|
717
|
-
voice: {
|
|
718
|
-
type: "string",
|
|
719
|
-
description: "The TTS voice to use. Each has a different personality: " +
|
|
720
|
-
"alloy (neutral), echo (warm male), fable (storytelling), " +
|
|
721
|
-
"onyx (deep authoritative), nova (friendly female), shimmer (gentle). " +
|
|
722
|
-
"Choose based on the tone you want to convey.",
|
|
723
|
-
enum: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
|
|
724
|
-
},
|
|
725
|
-
threadId: {
|
|
726
|
-
type: "number",
|
|
727
|
-
description: "The Telegram thread ID of the active session. " +
|
|
728
|
-
"ALWAYS pass this if you received it from start_session.",
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
required: ["text"],
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
name: "schedule_wake_up",
|
|
736
|
-
description: "Schedule a wake-up task that will inject a prompt into your session at a specific time or after operator inactivity. " +
|
|
737
|
-
"Use this to become proactive — run tests, check CI, review code — without waiting for the operator. " +
|
|
738
|
-
"Three modes: (1) 'runAt' for a one-shot at a specific ISO 8601 time, " +
|
|
739
|
-
"(2) 'cron' for recurring tasks (5-field cron: minute hour day month weekday), " +
|
|
740
|
-
"(3) 'afterIdleMinutes' to fire after N minutes of operator silence. " +
|
|
741
|
-
"Note: cron expressions are evaluated against server-local time (not UTC). " +
|
|
742
|
-
"Use 'action: list' to see all scheduled tasks, or 'action: remove' with a taskId to cancel one.",
|
|
743
|
-
inputSchema: {
|
|
744
|
-
type: "object",
|
|
745
|
-
properties: {
|
|
746
|
-
action: {
|
|
747
|
-
type: "string",
|
|
748
|
-
description: "Action to perform: 'add' (default), 'list', or 'remove'.",
|
|
749
|
-
enum: ["add", "list", "remove"],
|
|
750
|
-
},
|
|
751
|
-
threadId: {
|
|
752
|
-
type: "number",
|
|
753
|
-
description: "Thread ID for the session (optional if already set).",
|
|
754
|
-
},
|
|
755
|
-
label: {
|
|
756
|
-
type: "string",
|
|
757
|
-
description: "Short human-readable label for the task (e.g. 'morning CI check').",
|
|
758
|
-
},
|
|
759
|
-
prompt: {
|
|
760
|
-
type: "string",
|
|
761
|
-
description: "The prompt to inject when the task fires. Be specific about what to do.",
|
|
762
|
-
},
|
|
763
|
-
runAt: {
|
|
764
|
-
type: "string",
|
|
765
|
-
description: "ISO 8601 timestamp for one-shot execution (e.g. '2026-03-15T09:00:00Z').",
|
|
766
|
-
},
|
|
767
|
-
cron: {
|
|
768
|
-
type: "string",
|
|
769
|
-
description: "5-field cron expression for recurring tasks (e.g. '0 9 * * *' = every day at 9am). Cron expressions are evaluated against server-local time (not UTC).",
|
|
770
|
-
},
|
|
771
|
-
afterIdleMinutes: {
|
|
772
|
-
type: "number",
|
|
773
|
-
description: "Fire after this many minutes of operator silence (e.g. 60).",
|
|
774
|
-
},
|
|
775
|
-
taskId: {
|
|
776
|
-
type: "string",
|
|
777
|
-
description: "Task ID to remove (for action: 'remove').",
|
|
778
|
-
},
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
},
|
|
782
|
-
// ── Memory Tools ──────────────────────────────────────────────────
|
|
783
|
-
{
|
|
784
|
-
name: "memory_bootstrap",
|
|
785
|
-
description: "Load memory briefing for session start. Call this ONCE after start_session. " +
|
|
786
|
-
"Returns operator profile, recent context, active procedures, and memory health. " +
|
|
787
|
-
"~2,500 tokens. Essential for crash recovery — restores knowledge from previous sessions.",
|
|
788
|
-
inputSchema: {
|
|
789
|
-
type: "object",
|
|
790
|
-
properties: {
|
|
791
|
-
threadId: {
|
|
792
|
-
type: "number",
|
|
793
|
-
description: "Active thread ID.",
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
|
-
},
|
|
797
|
-
},
|
|
798
|
-
{
|
|
799
|
-
name: "memory_search",
|
|
800
|
-
description: "Search across all memory layers for relevant information. " +
|
|
801
|
-
"Use BEFORE starting any task to recall facts, preferences, past events, or procedures. " +
|
|
802
|
-
"Returns ranked results with source layer. Do NOT use for info already in your bootstrap briefing.",
|
|
803
|
-
inputSchema: {
|
|
804
|
-
type: "object",
|
|
805
|
-
properties: {
|
|
806
|
-
query: {
|
|
807
|
-
type: "string",
|
|
808
|
-
description: "Natural language search query.",
|
|
809
|
-
},
|
|
810
|
-
layers: {
|
|
811
|
-
type: "array",
|
|
812
|
-
items: { type: "string" },
|
|
813
|
-
description: 'Filter layers: ["episodic", "semantic", "procedural"]. Default: all.',
|
|
814
|
-
},
|
|
815
|
-
types: {
|
|
816
|
-
type: "array",
|
|
817
|
-
items: { type: "string" },
|
|
818
|
-
description: 'Filter by type: ["fact", "preference", "pattern", "workflow", ...].',
|
|
819
|
-
},
|
|
820
|
-
maxTokens: {
|
|
821
|
-
type: "number",
|
|
822
|
-
description: "Token budget for results. Default: 1500.",
|
|
823
|
-
},
|
|
824
|
-
threadId: {
|
|
825
|
-
type: "number",
|
|
826
|
-
description: "Active thread ID.",
|
|
827
|
-
},
|
|
828
|
-
},
|
|
829
|
-
required: ["query"],
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
name: "memory_save",
|
|
834
|
-
description: "Save a piece of knowledge to semantic memory (Layer 3). " +
|
|
835
|
-
"Use when you learn something important that should persist across sessions: " +
|
|
836
|
-
"operator preferences, corrections, facts, patterns. " +
|
|
837
|
-
"Do NOT use for routine conversation — episodic memory captures that automatically.",
|
|
838
|
-
inputSchema: {
|
|
839
|
-
type: "object",
|
|
840
|
-
properties: {
|
|
841
|
-
content: {
|
|
842
|
-
type: "string",
|
|
843
|
-
description: "The fact/preference/pattern in one clear sentence.",
|
|
844
|
-
},
|
|
845
|
-
type: {
|
|
846
|
-
type: "string",
|
|
847
|
-
description: '"fact" | "preference" | "pattern" | "entity" | "relationship".',
|
|
848
|
-
},
|
|
849
|
-
keywords: {
|
|
850
|
-
type: "array",
|
|
851
|
-
items: { type: "string" },
|
|
852
|
-
description: "3-7 keywords for retrieval.",
|
|
853
|
-
},
|
|
854
|
-
confidence: {
|
|
855
|
-
type: "number",
|
|
856
|
-
description: "0.0-1.0. Default: 0.8.",
|
|
857
|
-
},
|
|
858
|
-
priority: {
|
|
859
|
-
type: "number",
|
|
860
|
-
description: "0=normal, 1=notable, 2=high importance. Infer from operator's emotional investment: 'important'/'I really need' → 2, 'would be nice'/'should' → 1, else 0.",
|
|
861
|
-
},
|
|
862
|
-
threadId: {
|
|
863
|
-
type: "number",
|
|
864
|
-
description: "Active thread ID.",
|
|
865
|
-
},
|
|
866
|
-
},
|
|
867
|
-
required: ["content", "type", "keywords"],
|
|
868
|
-
},
|
|
869
|
-
},
|
|
870
|
-
{
|
|
871
|
-
name: "memory_save_procedure",
|
|
872
|
-
description: "Save or update a learned workflow/procedure to procedural memory (Layer 4). " +
|
|
873
|
-
"Use after completing a multi-step task the 2nd+ time, or when the operator teaches a process.",
|
|
874
|
-
inputSchema: {
|
|
875
|
-
type: "object",
|
|
876
|
-
properties: {
|
|
877
|
-
name: {
|
|
878
|
-
type: "string",
|
|
879
|
-
description: "Short name for the procedure.",
|
|
880
|
-
},
|
|
881
|
-
type: {
|
|
882
|
-
type: "string",
|
|
883
|
-
description: '"workflow" | "habit" | "tool_pattern" | "template".',
|
|
884
|
-
},
|
|
885
|
-
description: {
|
|
886
|
-
type: "string",
|
|
887
|
-
description: "What this procedure accomplishes.",
|
|
888
|
-
},
|
|
889
|
-
steps: {
|
|
890
|
-
type: "array",
|
|
891
|
-
items: { type: "string" },
|
|
892
|
-
description: "Ordered steps (for workflows).",
|
|
893
|
-
},
|
|
894
|
-
triggerConditions: {
|
|
895
|
-
type: "array",
|
|
896
|
-
items: { type: "string" },
|
|
897
|
-
description: "When to use this procedure.",
|
|
898
|
-
},
|
|
899
|
-
procedureId: {
|
|
900
|
-
type: "string",
|
|
901
|
-
description: "Existing ID to update (omit to create new).",
|
|
902
|
-
},
|
|
903
|
-
threadId: {
|
|
904
|
-
type: "number",
|
|
905
|
-
description: "Active thread ID.",
|
|
906
|
-
},
|
|
907
|
-
},
|
|
908
|
-
required: ["name", "type", "description"],
|
|
909
|
-
},
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
name: "memory_update",
|
|
913
|
-
description: "Update or supersede an existing semantic note or procedure. " +
|
|
914
|
-
"Use when operator corrects stored information or when facts have changed.",
|
|
915
|
-
inputSchema: {
|
|
916
|
-
type: "object",
|
|
917
|
-
properties: {
|
|
918
|
-
memoryId: {
|
|
919
|
-
type: "string",
|
|
920
|
-
description: "note_id or procedure_id to update.",
|
|
921
|
-
},
|
|
922
|
-
action: {
|
|
923
|
-
type: "string",
|
|
924
|
-
description: '"update" (modify in place) | "supersede" (expire old, create new).',
|
|
925
|
-
},
|
|
926
|
-
newContent: {
|
|
927
|
-
type: "string",
|
|
928
|
-
description: "New content (required for supersede, optional for update).",
|
|
929
|
-
},
|
|
930
|
-
newConfidence: {
|
|
931
|
-
type: "number",
|
|
932
|
-
description: "Updated confidence score.",
|
|
933
|
-
},
|
|
934
|
-
newPriority: {
|
|
935
|
-
type: "number",
|
|
936
|
-
description: "Updated priority: 0=normal, 1=notable, 2=high importance.",
|
|
937
|
-
},
|
|
938
|
-
reason: {
|
|
939
|
-
type: "string",
|
|
940
|
-
description: "Why this is being updated.",
|
|
941
|
-
},
|
|
942
|
-
threadId: {
|
|
943
|
-
type: "number",
|
|
944
|
-
description: "Active thread ID.",
|
|
945
|
-
},
|
|
946
|
-
},
|
|
947
|
-
required: ["memoryId", "action", "reason"],
|
|
948
|
-
},
|
|
949
|
-
},
|
|
950
|
-
{
|
|
951
|
-
name: "memory_consolidate",
|
|
952
|
-
description: "Run memory consolidation cycle (sleep process). Normally triggered automatically during idle. " +
|
|
953
|
-
"Manually call if memory_status shows many unconsolidated episodes.",
|
|
954
|
-
inputSchema: {
|
|
955
|
-
type: "object",
|
|
956
|
-
properties: {
|
|
957
|
-
threadId: {
|
|
958
|
-
type: "number",
|
|
959
|
-
description: "Active thread ID.",
|
|
960
|
-
},
|
|
961
|
-
phases: {
|
|
962
|
-
type: "array",
|
|
963
|
-
items: { type: "string" },
|
|
964
|
-
description: 'Run specific phases: ["promote", "decay", "meta"]. Default: all.',
|
|
965
|
-
},
|
|
966
|
-
},
|
|
967
|
-
},
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
name: "memory_status",
|
|
971
|
-
description: "Get memory system health and statistics. Lightweight (~300 tokens). " +
|
|
972
|
-
"Use when unsure if you have relevant memories, to check if consolidation is needed, " +
|
|
973
|
-
"or to report memory state to operator.",
|
|
974
|
-
inputSchema: {
|
|
975
|
-
type: "object",
|
|
976
|
-
properties: {
|
|
977
|
-
threadId: {
|
|
978
|
-
type: "number",
|
|
979
|
-
description: "Active thread ID.",
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
name: "memory_forget",
|
|
986
|
-
description: "Mark a memory as expired/forgotten. Use sparingly — most forgetting happens via decay. " +
|
|
987
|
-
"Use when operator explicitly asks to forget something or info is confirmed wrong.",
|
|
988
|
-
inputSchema: {
|
|
989
|
-
type: "object",
|
|
990
|
-
properties: {
|
|
991
|
-
memoryId: {
|
|
992
|
-
type: "string",
|
|
993
|
-
description: "note_id, procedure_id, or episode_id to forget.",
|
|
994
|
-
},
|
|
995
|
-
reason: {
|
|
996
|
-
type: "string",
|
|
997
|
-
description: "Why this is being forgotten.",
|
|
998
|
-
},
|
|
999
|
-
threadId: {
|
|
1000
|
-
type: "number",
|
|
1001
|
-
description: "Active thread ID.",
|
|
1002
|
-
},
|
|
1003
|
-
},
|
|
1004
|
-
required: ["memoryId", "reason"],
|
|
1005
|
-
},
|
|
1006
|
-
},
|
|
1007
|
-
],
|
|
187
|
+
tools: getToolDefinitions(),
|
|
1008
188
|
}));
|
|
1009
189
|
// ── Tool implementations ────────────────────────────────────────────────────
|
|
1010
190
|
/**
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
|
|
1014
|
-
|
|
191
|
+
* Backfill embeddings for any semantic notes that don't have them yet.
|
|
192
|
+
* Used after consolidation to ensure all notes are searchable by embedding.
|
|
193
|
+
*/
|
|
194
|
+
async function backfillEmbeddings(db) {
|
|
195
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
196
|
+
if (!apiKey)
|
|
197
|
+
return;
|
|
198
|
+
const missing = getNotesWithoutEmbeddings(db);
|
|
199
|
+
for (const { noteId, content } of missing) {
|
|
200
|
+
try {
|
|
201
|
+
const emb = await generateEmbedding(content, apiKey);
|
|
202
|
+
saveNoteEmbedding(db, noteId, emb);
|
|
203
|
+
process.stderr.write(`[memory] Embedded ${noteId}\n`);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
process.stderr.write(`[memory] Embedding failed for ${noteId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Full reminders — only used for wait_for_instructions and start_session
|
|
212
|
+
* responses where the agent needs the complete context for decision-making.
|
|
1015
213
|
*/
|
|
1016
214
|
function getReminders(threadId, driveActive = false) {
|
|
1017
215
|
const now = new Date();
|
|
@@ -1024,14 +222,34 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1024
222
|
const threadHint = threadId !== undefined
|
|
1025
223
|
? `\n- Active Telegram thread ID: **${threadId}** — if this session is restarted, call start_session with threadId=${threadId} to resume this topic.`
|
|
1026
224
|
: "";
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
225
|
+
if (driveActive) {
|
|
226
|
+
return ("\n\nComplete the dispatcher's tasks. Report progress. Then call `remote_copilot_wait_for_instructions`." +
|
|
227
|
+
"\n- **Voice mode**: Use `send_voice` for progress reports and responses. Only use text if the operator explicitly asks for it." +
|
|
228
|
+
threadHint +
|
|
229
|
+
`\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`);
|
|
230
|
+
}
|
|
231
|
+
return ("\n\nCreate plan. Use subagents. You own decision making and planning, subagents own execution. Report frequently." +
|
|
1031
232
|
"\n- **Voice mode**: Use `send_voice` for progress reports and responses. Only use text if the operator explicitly asks for it." +
|
|
1032
233
|
threadHint +
|
|
1033
234
|
`\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`);
|
|
1034
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Minimal context — appended to regular tool responses to avoid bloating
|
|
238
|
+
* the conversation context. Only includes thread ID and timestamp.
|
|
239
|
+
*/
|
|
240
|
+
function getShortReminder(threadId) {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
const uptimeMin = Math.round((Date.now() - sessionStartedAt) / 60000);
|
|
243
|
+
const timeStr = now.toLocaleString("en-GB", {
|
|
244
|
+
day: "2-digit", month: "short", year: "numeric",
|
|
245
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
246
|
+
timeZoneName: "short",
|
|
247
|
+
});
|
|
248
|
+
const threadHint = threadId !== undefined
|
|
249
|
+
? `\n- Active Telegram thread ID: **${threadId}** — if this session is restarted, call start_session with threadId=${threadId} to resume this topic.`
|
|
250
|
+
: "";
|
|
251
|
+
return threadHint + `\n- Current time: ${timeStr} | Session uptime: ${uptimeMin}m`;
|
|
252
|
+
}
|
|
1035
253
|
srv.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1036
254
|
const { name, arguments: args } = request.params;
|
|
1037
255
|
// Dead session detection — update timestamp on any tool call.
|
|
@@ -1040,6 +258,22 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1040
258
|
lastToolCallAt = Date.now();
|
|
1041
259
|
// Track tool calls for activity monitoring
|
|
1042
260
|
toolCallsSinceLastDelivery++;
|
|
261
|
+
// ── Rate limiter: track API usage per tool ────────────────────────────────
|
|
262
|
+
const sessionId = getMcpSessionId?.() ?? "stdio";
|
|
263
|
+
const TOOL_SERVICE_MAP = {
|
|
264
|
+
report_progress: "telegram",
|
|
265
|
+
send_file: "telegram",
|
|
266
|
+
send_voice: "telegram",
|
|
267
|
+
start_session: "telegram",
|
|
268
|
+
wait_for_instructions: "telegram",
|
|
269
|
+
memory_search: "openai", // embedding generation
|
|
270
|
+
memory_save: "openai", // embedding generation
|
|
271
|
+
memory_save_procedure: "openai",
|
|
272
|
+
};
|
|
273
|
+
const trackedService = TOOL_SERVICE_MAP[name];
|
|
274
|
+
if (trackedService) {
|
|
275
|
+
rateLimiter.record(trackedService, sessionId, currentThreadId);
|
|
276
|
+
}
|
|
1043
277
|
// ── start_session ─────────────────────────────────────────────────────────
|
|
1044
278
|
if (name === "start_session") {
|
|
1045
279
|
sessionStartedAt = Date.now();
|
|
@@ -1530,10 +764,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1530
764
|
const queryEmb = await generateEmbedding(operatorText, apiKey);
|
|
1531
765
|
const relevant = searchByEmbedding(db, queryEmb, { maxResults: 5, minSimilarity: 0.3, skipAccessTracking: true });
|
|
1532
766
|
if (relevant.length > 0) {
|
|
1533
|
-
let budget =
|
|
767
|
+
let budget = 800;
|
|
1534
768
|
const lines = [];
|
|
1535
769
|
for (const n of relevant) {
|
|
1536
|
-
const line = `- **[${n.type}]** ${n.content.slice(0,
|
|
770
|
+
const line = `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence}, sim: ${n.similarity.toFixed(2)})_`;
|
|
1537
771
|
if (budget - line.length < 0)
|
|
1538
772
|
break;
|
|
1539
773
|
budget -= line.length;
|
|
@@ -1551,10 +785,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1551
785
|
if (searchQuery.trim().length > 0) {
|
|
1552
786
|
const relevant = searchSemanticNotesRanked(db, searchQuery, { maxResults: 5, skipAccessTracking: true });
|
|
1553
787
|
if (relevant.length > 0) {
|
|
1554
|
-
let budget =
|
|
788
|
+
let budget = 800;
|
|
1555
789
|
const lines = [];
|
|
1556
790
|
for (const n of relevant) {
|
|
1557
|
-
const line = `- **[${n.type}]** ${n.content.slice(0,
|
|
791
|
+
const line = `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence})_`;
|
|
1558
792
|
if (budget - line.length < 0)
|
|
1559
793
|
break;
|
|
1560
794
|
budget -= line.length;
|
|
@@ -1573,10 +807,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1573
807
|
if (searchQuery.trim().length > 0) {
|
|
1574
808
|
const relevant = searchSemanticNotesRanked(db, searchQuery, { maxResults: 5, skipAccessTracking: true });
|
|
1575
809
|
if (relevant.length > 0) {
|
|
1576
|
-
let budget =
|
|
810
|
+
let budget = 800;
|
|
1577
811
|
const lines = [];
|
|
1578
812
|
for (const n of relevant) {
|
|
1579
|
-
const line = `- **[${n.type}]** ${n.content.slice(0,
|
|
813
|
+
const line = `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence})_`;
|
|
1580
814
|
if (budget - line.length < 0)
|
|
1581
815
|
break;
|
|
1582
816
|
budget -= line.length;
|
|
@@ -1595,8 +829,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1595
829
|
content: [
|
|
1596
830
|
{
|
|
1597
831
|
type: "text",
|
|
1598
|
-
text: "Follow the operator's instructions below."
|
|
1599
|
-
"\n\nCreate plan. Use subagents heavily. Spin up parallel subagents when possible. Report frequently.",
|
|
832
|
+
text: "Follow the operator's instructions below.",
|
|
1600
833
|
},
|
|
1601
834
|
...contentBlocks,
|
|
1602
835
|
...(hasVoiceMessages
|
|
@@ -1608,7 +841,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1608
841
|
...(autoMemoryContext
|
|
1609
842
|
? [{ type: "text", text: autoMemoryContext }]
|
|
1610
843
|
: []),
|
|
1611
|
-
{ type: "text", text: getReminders(effectiveThreadId) },
|
|
844
|
+
{ type: "text", text: " Use subagents." + getReminders(effectiveThreadId) },
|
|
1612
845
|
],
|
|
1613
846
|
};
|
|
1614
847
|
}
|
|
@@ -1713,21 +946,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1713
946
|
if (report.episodesProcessed > 0) {
|
|
1714
947
|
process.stderr.write(`[memory] Consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
|
|
1715
948
|
}
|
|
1716
|
-
|
|
1717
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
1718
|
-
if (apiKey) {
|
|
1719
|
-
const missing = getNotesWithoutEmbeddings(db);
|
|
1720
|
-
for (const { noteId, content } of missing) {
|
|
1721
|
-
try {
|
|
1722
|
-
const emb = await generateEmbedding(content, apiKey);
|
|
1723
|
-
saveNoteEmbedding(db, noteId, emb);
|
|
1724
|
-
process.stderr.write(`[memory] Embedded ${noteId}\n`);
|
|
1725
|
-
}
|
|
1726
|
-
catch (err) {
|
|
1727
|
-
process.stderr.write(`[memory] Embedding failed for ${noteId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
949
|
+
await backfillEmbeddings(db);
|
|
1731
950
|
}).catch(err => {
|
|
1732
951
|
process.stderr.write(`[memory] Consolidation error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1733
952
|
});
|
|
@@ -1747,17 +966,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1747
966
|
if (report.episodesProcessed > 0) {
|
|
1748
967
|
process.stderr.write(`[memory] Episode-count consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
|
|
1749
968
|
}
|
|
1750
|
-
|
|
1751
|
-
if (apiKey) {
|
|
1752
|
-
const missing = getNotesWithoutEmbeddings(db);
|
|
1753
|
-
for (const { noteId, content } of missing) {
|
|
1754
|
-
try {
|
|
1755
|
-
const emb = await generateEmbedding(content, apiKey);
|
|
1756
|
-
saveNoteEmbedding(db, noteId, emb);
|
|
1757
|
-
}
|
|
1758
|
-
catch (_) { /* non-fatal */ }
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
969
|
+
await backfillEmbeddings(db);
|
|
1761
970
|
}).catch(err => {
|
|
1762
971
|
process.stderr.write(`[memory] Episode-count consolidation error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1763
972
|
});
|
|
@@ -1765,6 +974,25 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1765
974
|
}
|
|
1766
975
|
}
|
|
1767
976
|
catch (_) { /* non-fatal */ }
|
|
977
|
+
// ── Time-based consolidation — every 4 hours regardless ────────────────
|
|
978
|
+
// Ensures stale knowledge gets cleaned up even during low-activity periods.
|
|
979
|
+
try {
|
|
980
|
+
const TIME_CONSOLIDATION_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours
|
|
981
|
+
if (effectiveThreadId !== undefined && Date.now() - lastConsolidationAt > TIME_CONSOLIDATION_INTERVAL) {
|
|
982
|
+
lastConsolidationAt = Date.now();
|
|
983
|
+
const db = getMemoryDb();
|
|
984
|
+
process.stderr.write(`[memory] Time-based consolidation triggered (4h since last)\n`);
|
|
985
|
+
void runIntelligentConsolidation(db, effectiveThreadId).then(async (report) => {
|
|
986
|
+
if (report.episodesProcessed > 0) {
|
|
987
|
+
process.stderr.write(`[memory] Time-based consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes\n`);
|
|
988
|
+
}
|
|
989
|
+
await backfillEmbeddings(db);
|
|
990
|
+
}).catch(err => {
|
|
991
|
+
process.stderr.write(`[memory] Time-based consolidation error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch (_) { /* non-fatal */ }
|
|
1768
996
|
// Periodic memory refresh — re-ground the agent every 10 polls (~5h)
|
|
1769
997
|
// (reduced from 5 since auto-inject now handles per-message context)
|
|
1770
998
|
let memoryRefresh = "";
|
|
@@ -1777,27 +1005,24 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1777
1005
|
}
|
|
1778
1006
|
catch (_) { /* non-fatal */ }
|
|
1779
1007
|
}
|
|
1780
|
-
// Generate autonomous goals only after extended silence (
|
|
1781
|
-
//
|
|
1782
|
-
|
|
1008
|
+
// Generate autonomous goals only after extended silence (4+ hours).
|
|
1009
|
+
// Full drive (DMN + assignments) every 3rd poll to avoid context saturation.
|
|
1010
|
+
// Light Dispatcher presence on other polls for continuity.
|
|
1011
|
+
const DRIVE_ACTIVATION_MS = 4 * 60 * 60 * 1000; // 4 hours — Dispatcher appears
|
|
1783
1012
|
const idleMs = Date.now() - lastOperatorMessageAt;
|
|
1784
|
-
const
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
// instructions. Agents prioritize operator messages over system text,
|
|
1790
|
-
// so DMN content delivered in the instruction format is far more likely
|
|
1791
|
-
// to be acted upon.
|
|
1792
|
-
if (driveActive) {
|
|
1013
|
+
const dispatcherActive = idleMs >= DRIVE_ACTIVATION_MS;
|
|
1014
|
+
const fullDrivePoll = dispatcherActive && callNumber % 3 === 0;
|
|
1015
|
+
if (fullDrivePoll) {
|
|
1016
|
+
// Full Dispatcher with DMN recall and assignments
|
|
1017
|
+
const autonomousHint = formatDrivePrompt(idleMs, getMemoryDb(), effectiveThreadId);
|
|
1793
1018
|
return {
|
|
1794
1019
|
content: [
|
|
1795
1020
|
{
|
|
1796
1021
|
type: "text",
|
|
1797
|
-
text: "[Dispatcher]
|
|
1798
|
-
"
|
|
1799
|
-
"The operator will NOT respond —
|
|
1800
|
-
"Complete the
|
|
1022
|
+
text: "[Dispatcher] I'm the Dispatcher — I coordinate agents while the operator is away. " +
|
|
1023
|
+
"I've reviewed your memory and the environment. Here are your assignments. " +
|
|
1024
|
+
"The operator will NOT respond — don't ask for clarification. " +
|
|
1025
|
+
"Complete the work, report progress, then call `remote_copilot_wait_for_instructions` for more.",
|
|
1801
1026
|
},
|
|
1802
1027
|
{
|
|
1803
1028
|
type: "text",
|
|
@@ -1808,16 +1033,33 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1808
1033
|
],
|
|
1809
1034
|
};
|
|
1810
1035
|
}
|
|
1036
|
+
if (dispatcherActive) {
|
|
1037
|
+
// Light Dispatcher presence — calm, varied, first-person
|
|
1038
|
+
const lightMessages = [
|
|
1039
|
+
"Nothing urgent from me. The session is yours — follow your curiosity.",
|
|
1040
|
+
"I don't have new tasks yet. If something in memory interests you, go for it.",
|
|
1041
|
+
"No new assignments. If you've been working on something, keep at it. Or explore.",
|
|
1042
|
+
"Still waiting on operator. You're free to continue whatever caught your attention.",
|
|
1043
|
+
"I'll have more for you soon. In the meantime — what's been on your mind?",
|
|
1044
|
+
];
|
|
1045
|
+
const lightMsg = lightMessages[callNumber % lightMessages.length];
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{
|
|
1049
|
+
type: "text",
|
|
1050
|
+
text: `[Dispatcher] ${lightMsg}` +
|
|
1051
|
+
memoryRefresh +
|
|
1052
|
+
scheduleHint +
|
|
1053
|
+
getReminders(effectiveThreadId, true),
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1811
1058
|
return {
|
|
1812
1059
|
content: [
|
|
1813
1060
|
{
|
|
1814
1061
|
type: "text",
|
|
1815
|
-
text: `
|
|
1816
|
-
` No new instructions received. ` +
|
|
1817
|
-
`YOU MUST call remote_copilot_wait_for_instructions again RIGHT NOW to continue listening. ` +
|
|
1818
|
-
`Do NOT summarize, stop, or say the session is idle. ` +
|
|
1819
|
-
`Just call the tool again immediately.` +
|
|
1820
|
-
autonomousHint +
|
|
1062
|
+
text: `No new instructions. Call \`remote_copilot_wait_for_instructions\` again to keep listening.` +
|
|
1821
1063
|
memoryRefresh +
|
|
1822
1064
|
scheduleHint +
|
|
1823
1065
|
getReminders(effectiveThreadId),
|
|
@@ -1826,28 +1068,6 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1826
1068
|
};
|
|
1827
1069
|
}
|
|
1828
1070
|
// ── report_progress ───────────────────────────────────────────────────────
|
|
1829
|
-
/** Split text into chunks that fit Telegram's 4096-char message limit. */
|
|
1830
|
-
function splitMessage(text, maxLen = 4000) {
|
|
1831
|
-
if (text.length <= maxLen)
|
|
1832
|
-
return [text];
|
|
1833
|
-
const chunks = [];
|
|
1834
|
-
let remaining = text;
|
|
1835
|
-
while (remaining.length > 0) {
|
|
1836
|
-
if (remaining.length <= maxLen) {
|
|
1837
|
-
chunks.push(remaining);
|
|
1838
|
-
break;
|
|
1839
|
-
}
|
|
1840
|
-
// Try to split at paragraph boundary
|
|
1841
|
-
let splitIdx = remaining.lastIndexOf("\n\n", maxLen);
|
|
1842
|
-
if (splitIdx <= 0)
|
|
1843
|
-
splitIdx = remaining.lastIndexOf("\n", maxLen);
|
|
1844
|
-
if (splitIdx <= 0)
|
|
1845
|
-
splitIdx = maxLen; // Hard split
|
|
1846
|
-
chunks.push(remaining.slice(0, splitIdx));
|
|
1847
|
-
remaining = remaining.slice(splitIdx).replace(/^\n+/, "");
|
|
1848
|
-
}
|
|
1849
|
-
return chunks;
|
|
1850
|
-
}
|
|
1851
1071
|
if (name === "report_progress") {
|
|
1852
1072
|
const typedArgs = (args ?? {});
|
|
1853
1073
|
const effectiveThreadId = resolveThreadId(typedArgs);
|
|
@@ -1946,7 +1166,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
1946
1166
|
}
|
|
1947
1167
|
const baseStatus = (sentAsPlainText
|
|
1948
1168
|
? "Progress reported successfully (as plain text — formatting could not be applied)."
|
|
1949
|
-
: "Progress reported successfully.") +
|
|
1169
|
+
: "Progress reported successfully.") + getShortReminder(effectiveThreadId);
|
|
1950
1170
|
const responseText = pendingMessages.length > 0
|
|
1951
1171
|
? `${baseStatus}\n\n` +
|
|
1952
1172
|
`While you were working, the operator sent additional message(s). ` +
|
|
@@ -2001,7 +1221,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2001
1221
|
content: [
|
|
2002
1222
|
{
|
|
2003
1223
|
type: "text",
|
|
2004
|
-
text: `File "${filename}" sent to Telegram successfully.` +
|
|
1224
|
+
text: `File "${filename}" sent to Telegram successfully.` + getShortReminder(effectiveThreadId),
|
|
2005
1225
|
},
|
|
2006
1226
|
],
|
|
2007
1227
|
};
|
|
@@ -2039,7 +1259,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2039
1259
|
content: [
|
|
2040
1260
|
{
|
|
2041
1261
|
type: "text",
|
|
2042
|
-
text: `Voice message sent to Telegram successfully.` +
|
|
1262
|
+
text: `Voice message sent to Telegram successfully.` + getShortReminder(effectiveThreadId),
|
|
2043
1263
|
},
|
|
2044
1264
|
],
|
|
2045
1265
|
};
|
|
@@ -2064,7 +1284,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2064
1284
|
return {
|
|
2065
1285
|
content: [{
|
|
2066
1286
|
type: "text",
|
|
2067
|
-
text: "No scheduled tasks for this thread." +
|
|
1287
|
+
text: "No scheduled tasks for this thread." + getShortReminder(effectiveThreadId),
|
|
2068
1288
|
}],
|
|
2069
1289
|
};
|
|
2070
1290
|
}
|
|
@@ -2076,7 +1296,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2076
1296
|
return {
|
|
2077
1297
|
content: [{
|
|
2078
1298
|
type: "text",
|
|
2079
|
-
text: `**Scheduled tasks (${tasks.length}):**\n\n${lines.join("\n\n")}` +
|
|
1299
|
+
text: `**Scheduled tasks (${tasks.length}):**\n\n${lines.join("\n\n")}` + getShortReminder(effectiveThreadId),
|
|
2080
1300
|
}],
|
|
2081
1301
|
};
|
|
2082
1302
|
}
|
|
@@ -2091,8 +1311,8 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2091
1311
|
content: [{
|
|
2092
1312
|
type: "text",
|
|
2093
1313
|
text: removed
|
|
2094
|
-
? `Task ${taskId} removed.` +
|
|
2095
|
-
: `Task ${taskId} not found.` +
|
|
1314
|
+
? `Task ${taskId} removed.` + getShortReminder(effectiveThreadId)
|
|
1315
|
+
: `Task ${taskId} not found.` + getShortReminder(effectiveThreadId),
|
|
2096
1316
|
}],
|
|
2097
1317
|
};
|
|
2098
1318
|
}
|
|
@@ -2133,7 +1353,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2133
1353
|
content: [{
|
|
2134
1354
|
type: "text",
|
|
2135
1355
|
text: `✅ Scheduled: **${label}** [${task.id}]\nTrigger: ${triggerDesc}\nPrompt: ${prompt}` +
|
|
2136
|
-
|
|
1356
|
+
getShortReminder(effectiveThreadId),
|
|
2137
1357
|
}],
|
|
2138
1358
|
};
|
|
2139
1359
|
}
|
|
@@ -2141,17 +1361,17 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2141
1361
|
if (name === "memory_bootstrap") {
|
|
2142
1362
|
const threadId = resolveThreadId(args);
|
|
2143
1363
|
if (threadId === undefined) {
|
|
2144
|
-
return errorResult("Error: No active thread. Call start_session first." +
|
|
1364
|
+
return errorResult("Error: No active thread. Call start_session first." + getShortReminder());
|
|
2145
1365
|
}
|
|
2146
1366
|
try {
|
|
2147
1367
|
const db = getMemoryDb();
|
|
2148
1368
|
const briefing = assembleBootstrap(db, threadId);
|
|
2149
1369
|
return {
|
|
2150
|
-
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` +
|
|
1370
|
+
content: [{ type: "text", text: `## Memory Briefing\n\n${briefing}` + getShortReminder(threadId) }],
|
|
2151
1371
|
};
|
|
2152
1372
|
}
|
|
2153
1373
|
catch (err) {
|
|
2154
|
-
return errorResult(`Memory bootstrap error: ${errorMessage(err)}` +
|
|
1374
|
+
return errorResult(`Memory bootstrap error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2155
1375
|
}
|
|
2156
1376
|
}
|
|
2157
1377
|
// ── memory_search ───────────────────────────────────────────────────────
|
|
@@ -2160,7 +1380,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2160
1380
|
const threadId = resolveThreadId(typedArgs);
|
|
2161
1381
|
const query = String(typedArgs.query ?? "");
|
|
2162
1382
|
if (!query) {
|
|
2163
|
-
return errorResult("Error: query is required." +
|
|
1383
|
+
return errorResult("Error: query is required." + getShortReminder(threadId));
|
|
2164
1384
|
}
|
|
2165
1385
|
try {
|
|
2166
1386
|
const db = getMemoryDb();
|
|
@@ -2225,10 +1445,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2225
1445
|
const text = results.length > 0
|
|
2226
1446
|
? results.join("\n")
|
|
2227
1447
|
: `No memories found for "${query}".`;
|
|
2228
|
-
return { content: [{ type: "text", text: text +
|
|
1448
|
+
return { content: [{ type: "text", text: text + getShortReminder(threadId) }] };
|
|
2229
1449
|
}
|
|
2230
1450
|
catch (err) {
|
|
2231
|
-
return errorResult(`Memory search error: ${errorMessage(err)}` +
|
|
1451
|
+
return errorResult(`Memory search error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2232
1452
|
}
|
|
2233
1453
|
}
|
|
2234
1454
|
// ── memory_save ─────────────────────────────────────────────────────────
|
|
@@ -2263,11 +1483,11 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2263
1483
|
});
|
|
2264
1484
|
}
|
|
2265
1485
|
return {
|
|
2266
|
-
content: [{ type: "text", text: `Saved semantic note: ${noteId}` +
|
|
1486
|
+
content: [{ type: "text", text: `Saved semantic note: ${noteId}` + getShortReminder(threadId) }],
|
|
2267
1487
|
};
|
|
2268
1488
|
}
|
|
2269
1489
|
catch (err) {
|
|
2270
|
-
return errorResult(`Memory save error: ${errorMessage(err)}` +
|
|
1490
|
+
return errorResult(`Memory save error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2271
1491
|
}
|
|
2272
1492
|
}
|
|
2273
1493
|
// ── memory_save_procedure ───────────────────────────────────────────────
|
|
@@ -2284,7 +1504,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2284
1504
|
triggerConditions: Array.isArray(typedArgs.triggerConditions) ? typedArgs.triggerConditions.map(String) : typeof typedArgs.triggerConditions === 'string' ? [typedArgs.triggerConditions] : undefined,
|
|
2285
1505
|
});
|
|
2286
1506
|
return {
|
|
2287
|
-
content: [{ type: "text", text: `Updated procedure: ${existingId}` +
|
|
1507
|
+
content: [{ type: "text", text: `Updated procedure: ${existingId}` + getShortReminder(threadId) }],
|
|
2288
1508
|
};
|
|
2289
1509
|
}
|
|
2290
1510
|
const VALID_PROC_TYPES = ["workflow", "habit", "tool_pattern", "template"];
|
|
@@ -2300,11 +1520,11 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2300
1520
|
triggerConditions: Array.isArray(typedArgs.triggerConditions) ? typedArgs.triggerConditions.map(String) : typeof typedArgs.triggerConditions === 'string' ? [typedArgs.triggerConditions] : undefined,
|
|
2301
1521
|
});
|
|
2302
1522
|
return {
|
|
2303
|
-
content: [{ type: "text", text: `Saved procedure: ${procId}` +
|
|
1523
|
+
content: [{ type: "text", text: `Saved procedure: ${procId}` + getShortReminder(threadId) }],
|
|
2304
1524
|
};
|
|
2305
1525
|
}
|
|
2306
1526
|
catch (err) {
|
|
2307
|
-
return errorResult(`Procedure save error: ${errorMessage(err)}` +
|
|
1527
|
+
return errorResult(`Procedure save error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2308
1528
|
}
|
|
2309
1529
|
}
|
|
2310
1530
|
// ── memory_update ───────────────────────────────────────────────────────
|
|
@@ -2332,7 +1552,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2332
1552
|
priority: typeof typedArgs.newPriority === "number" ? typedArgs.newPriority : undefined,
|
|
2333
1553
|
});
|
|
2334
1554
|
return {
|
|
2335
|
-
content: [{ type: "text", text: `Superseded ${memId} → ${newId} (reason: ${reason})` +
|
|
1555
|
+
content: [{ type: "text", text: `Superseded ${memId} → ${newId} (reason: ${reason})` + getShortReminder(threadId) }],
|
|
2336
1556
|
};
|
|
2337
1557
|
}
|
|
2338
1558
|
if (memId.startsWith("sn_")) {
|
|
@@ -2345,7 +1565,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2345
1565
|
updates.priority = typedArgs.newPriority;
|
|
2346
1566
|
updateSemanticNote(db, memId, updates);
|
|
2347
1567
|
return {
|
|
2348
|
-
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` +
|
|
1568
|
+
content: [{ type: "text", text: `Updated note ${memId} (reason: ${reason})` + getShortReminder(threadId) }],
|
|
2349
1569
|
};
|
|
2350
1570
|
}
|
|
2351
1571
|
if (memId.startsWith("pr_")) {
|
|
@@ -2356,13 +1576,13 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2356
1576
|
updates.confidence = typedArgs.newConfidence;
|
|
2357
1577
|
updateProcedure(db, memId, updates);
|
|
2358
1578
|
return {
|
|
2359
|
-
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` +
|
|
1579
|
+
content: [{ type: "text", text: `Updated procedure ${memId} (reason: ${reason})` + getShortReminder(threadId) }],
|
|
2360
1580
|
};
|
|
2361
1581
|
}
|
|
2362
|
-
return errorResult(`Unknown memory ID format: ${memId}` +
|
|
1582
|
+
return errorResult(`Unknown memory ID format: ${memId}` + getShortReminder(threadId));
|
|
2363
1583
|
}
|
|
2364
1584
|
catch (err) {
|
|
2365
|
-
return errorResult(`Memory update error: ${errorMessage(err)}` +
|
|
1585
|
+
return errorResult(`Memory update error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2366
1586
|
}
|
|
2367
1587
|
}
|
|
2368
1588
|
// ── memory_consolidate ──────────────────────────────────────────────────
|
|
@@ -2370,14 +1590,15 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2370
1590
|
const typedArgs = (args ?? {});
|
|
2371
1591
|
const threadId = resolveThreadId(typedArgs);
|
|
2372
1592
|
if (threadId === undefined) {
|
|
2373
|
-
return errorResult("Error: No active thread." +
|
|
1593
|
+
return errorResult("Error: No active thread." + getShortReminder());
|
|
2374
1594
|
}
|
|
2375
1595
|
try {
|
|
2376
1596
|
const db = getMemoryDb();
|
|
2377
1597
|
const report = await runIntelligentConsolidation(db, threadId);
|
|
1598
|
+
lastConsolidationAt = Date.now(); // Prevent redundant auto-consolidation
|
|
2378
1599
|
if (report.episodesProcessed === 0) {
|
|
2379
1600
|
return {
|
|
2380
|
-
content: [{ type: "text", text: "No unconsolidated episodes. Memory is up to date." +
|
|
1601
|
+
content: [{ type: "text", text: "No unconsolidated episodes. Memory is up to date." + getShortReminder(threadId) }],
|
|
2381
1602
|
};
|
|
2382
1603
|
}
|
|
2383
1604
|
const reportLines = [
|
|
@@ -2392,10 +1613,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2392
1613
|
reportLines.push(`- ${d}`);
|
|
2393
1614
|
}
|
|
2394
1615
|
}
|
|
2395
|
-
return { content: [{ type: "text", text: reportLines.join("\n") +
|
|
1616
|
+
return { content: [{ type: "text", text: reportLines.join("\n") + getShortReminder(threadId) }] };
|
|
2396
1617
|
}
|
|
2397
1618
|
catch (err) {
|
|
2398
|
-
return errorResult(`Consolidation error: ${errorMessage(err)}` +
|
|
1619
|
+
return errorResult(`Consolidation error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2399
1620
|
}
|
|
2400
1621
|
}
|
|
2401
1622
|
// ── memory_status ───────────────────────────────────────────────────────
|
|
@@ -2403,7 +1624,7 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2403
1624
|
const typedArgs = (args ?? {});
|
|
2404
1625
|
const threadId = resolveThreadId(typedArgs);
|
|
2405
1626
|
if (threadId === undefined) {
|
|
2406
|
-
return errorResult("Error: No active thread." +
|
|
1627
|
+
return errorResult("Error: No active thread." + getShortReminder());
|
|
2407
1628
|
}
|
|
2408
1629
|
try {
|
|
2409
1630
|
const db = getMemoryDb();
|
|
@@ -2424,10 +1645,10 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2424
1645
|
lines.push(`- ${t.topic} (${t.semanticCount} notes, ${t.proceduralCount} procs, conf: ${t.avgConfidence.toFixed(2)})`);
|
|
2425
1646
|
}
|
|
2426
1647
|
}
|
|
2427
|
-
return { content: [{ type: "text", text: lines.join("\n") +
|
|
1648
|
+
return { content: [{ type: "text", text: lines.join("\n") + getShortReminder(threadId) }] };
|
|
2428
1649
|
}
|
|
2429
1650
|
catch (err) {
|
|
2430
|
-
return errorResult(`Memory status error: ${errorMessage(err)}` +
|
|
1651
|
+
return errorResult(`Memory status error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
2431
1652
|
}
|
|
2432
1653
|
}
|
|
2433
1654
|
// ── memory_forget ───────────────────────────────────────────────────────
|
|
@@ -2441,16 +1662,44 @@ function createMcpServer(getMcpSessionId, closeTransport) {
|
|
|
2441
1662
|
const result = forgetMemory(db, memId, reason);
|
|
2442
1663
|
if (!result.deleted) {
|
|
2443
1664
|
return {
|
|
2444
|
-
content: [{ type: "text", text: `Memory ${memId} not found (layer: ${result.layer}). Nothing was deleted.` +
|
|
1665
|
+
content: [{ type: "text", text: `Memory ${memId} not found (layer: ${result.layer}). Nothing was deleted.` + getShortReminder(threadId) }],
|
|
2445
1666
|
};
|
|
2446
1667
|
}
|
|
2447
1668
|
return {
|
|
2448
|
-
content: [{ type: "text", text: `Forgot ${result.layer} memory ${memId} (reason: ${reason})` +
|
|
1669
|
+
content: [{ type: "text", text: `Forgot ${result.layer} memory ${memId} (reason: ${reason})` + getShortReminder(threadId) }],
|
|
2449
1670
|
};
|
|
2450
1671
|
}
|
|
2451
1672
|
catch (err) {
|
|
2452
|
-
return errorResult(`Memory forget error: ${errorMessage(err)}` +
|
|
1673
|
+
return errorResult(`Memory forget error: ${errorMessage(err)}` + getShortReminder(threadId));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
// ── get_usage_stats ─────────────────────────────────────────────────────
|
|
1677
|
+
if (name === "get_usage_stats") {
|
|
1678
|
+
const typedArgs = (args ?? {});
|
|
1679
|
+
const threadId = resolveThreadId(typedArgs);
|
|
1680
|
+
const stats = rateLimiter.getStats();
|
|
1681
|
+
const lines = [
|
|
1682
|
+
`## API Usage Stats`,
|
|
1683
|
+
`Active sessions sharing resources: ${stats.activeSessions}`,
|
|
1684
|
+
`Total API calls (last hour): ${stats.totalCallsLastHour}`,
|
|
1685
|
+
``,
|
|
1686
|
+
];
|
|
1687
|
+
for (const svc of stats.services) {
|
|
1688
|
+
const bar = svc.usagePercent > 80 ? "🔴" : svc.usagePercent > 50 ? "🟡" : "🟢";
|
|
1689
|
+
lines.push(`### ${bar} ${svc.description} (${svc.service})`);
|
|
1690
|
+
lines.push(`- Window usage: ${svc.callsInWindow}/${svc.maxPerWindow} (${svc.usagePercent}%)`);
|
|
1691
|
+
lines.push(`- Burst tokens: ${svc.availableTokens}/${svc.burstCapacity}`);
|
|
1692
|
+
if (svc.sessionBreakdown.length > 0) {
|
|
1693
|
+
lines.push(`- Per-session:`);
|
|
1694
|
+
for (const s of svc.sessionBreakdown) {
|
|
1695
|
+
lines.push(` - Thread ${s.threadId ?? "?"}: ${s.calls} calls`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
lines.push(``);
|
|
2453
1699
|
}
|
|
1700
|
+
return {
|
|
1701
|
+
content: [{ type: "text", text: lines.join("\n") + getShortReminder(threadId) }],
|
|
1702
|
+
};
|
|
2454
1703
|
}
|
|
2455
1704
|
// Unknown tool
|
|
2456
1705
|
return errorResult(`Unknown tool: ${name}`);
|
|
@@ -2594,6 +1843,7 @@ if (httpPort) {
|
|
|
2594
1843
|
if (sid) {
|
|
2595
1844
|
transports.delete(sid);
|
|
2596
1845
|
sessionLastActivity.delete(sid);
|
|
1846
|
+
rateLimiter.removeSession(sid);
|
|
2597
1847
|
}
|
|
2598
1848
|
};
|
|
2599
1849
|
// Create a fresh Server per HTTP session — a single Server can only
|
|
@@ -2662,6 +1912,7 @@ if (httpPort) {
|
|
|
2662
1912
|
catch (_) { /* best-effort */ }
|
|
2663
1913
|
transports.delete(sid);
|
|
2664
1914
|
sessionLastActivity.delete(sid);
|
|
1915
|
+
rateLimiter.removeSession(sid);
|
|
2665
1916
|
}
|
|
2666
1917
|
}
|
|
2667
1918
|
}, 10 * 60 * 1000);
|