sensorium-mcp 2.16.28 → 2.16.30

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.
Files changed (177) hide show
  1. package/dist/config.d.ts +1 -11
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +3 -49
  4. package/dist/config.js.map +1 -1
  5. package/dist/dashboard/presets.d.ts +18 -0
  6. package/dist/dashboard/presets.d.ts.map +1 -0
  7. package/dist/dashboard/presets.js +78 -0
  8. package/dist/dashboard/presets.js.map +1 -0
  9. package/dist/dashboard/routes.d.ts +33 -0
  10. package/dist/dashboard/routes.d.ts.map +1 -0
  11. package/dist/dashboard/routes.js +283 -0
  12. package/dist/dashboard/routes.js.map +1 -0
  13. package/dist/dashboard.d.ts +6 -29
  14. package/dist/dashboard.d.ts.map +1 -1
  15. package/dist/dashboard.js +6 -1158
  16. package/dist/dashboard.js.map +1 -1
  17. package/dist/data/file-storage.d.ts +19 -0
  18. package/dist/data/file-storage.d.ts.map +1 -0
  19. package/dist/data/file-storage.js +58 -0
  20. package/dist/data/file-storage.js.map +1 -0
  21. package/dist/data/memory/bootstrap.d.ts +40 -0
  22. package/dist/data/memory/bootstrap.d.ts.map +1 -0
  23. package/dist/data/memory/bootstrap.js +240 -0
  24. package/dist/data/memory/bootstrap.js.map +1 -0
  25. package/dist/data/memory/consolidation.d.ts +12 -0
  26. package/dist/data/memory/consolidation.d.ts.map +1 -0
  27. package/dist/data/memory/consolidation.js +248 -0
  28. package/dist/data/memory/consolidation.js.map +1 -0
  29. package/dist/data/memory/episodes.d.ts +34 -0
  30. package/dist/data/memory/episodes.d.ts.map +1 -0
  31. package/dist/data/memory/episodes.js +89 -0
  32. package/dist/data/memory/episodes.js.map +1 -0
  33. package/dist/data/memory/index.d.ts +14 -0
  34. package/dist/data/memory/index.d.ts.map +1 -0
  35. package/dist/data/memory/index.js +14 -0
  36. package/dist/data/memory/index.js.map +1 -0
  37. package/dist/data/memory/procedures.d.ts +42 -0
  38. package/dist/data/memory/procedures.d.ts.map +1 -0
  39. package/dist/data/memory/procedures.js +122 -0
  40. package/dist/data/memory/procedures.js.map +1 -0
  41. package/dist/data/memory/schema.d.ts +11 -0
  42. package/dist/data/memory/schema.d.ts.map +1 -0
  43. package/dist/data/memory/schema.js +327 -0
  44. package/dist/data/memory/schema.js.map +1 -0
  45. package/dist/data/memory/semantic.d.ts +94 -0
  46. package/dist/data/memory/semantic.d.ts.map +1 -0
  47. package/dist/data/memory/semantic.js +385 -0
  48. package/dist/data/memory/semantic.js.map +1 -0
  49. package/dist/data/memory/voice-sig.d.ts +33 -0
  50. package/dist/data/memory/voice-sig.d.ts.map +1 -0
  51. package/dist/data/memory/voice-sig.js +48 -0
  52. package/dist/data/memory/voice-sig.js.map +1 -0
  53. package/dist/data/templates.d.ts +19 -0
  54. package/dist/data/templates.d.ts.map +1 -0
  55. package/dist/data/templates.js +46 -0
  56. package/dist/data/templates.js.map +1 -0
  57. package/dist/dispatcher.d.ts +5 -97
  58. package/dist/dispatcher.d.ts.map +1 -1
  59. package/dist/dispatcher.js +5 -525
  60. package/dist/dispatcher.js.map +1 -1
  61. package/dist/drive.d.ts.map +1 -1
  62. package/dist/drive.js +3 -1
  63. package/dist/drive.js.map +1 -1
  64. package/dist/index.d.ts +4 -23
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +11 -289
  67. package/dist/index.js.map +1 -1
  68. package/dist/integrations/openai/chat.d.ts +29 -0
  69. package/dist/integrations/openai/chat.d.ts.map +1 -0
  70. package/dist/integrations/openai/chat.js +84 -0
  71. package/dist/integrations/openai/chat.js.map +1 -0
  72. package/dist/integrations/openai/index.d.ts +6 -0
  73. package/dist/integrations/openai/index.d.ts.map +1 -0
  74. package/dist/integrations/openai/index.js +6 -0
  75. package/dist/integrations/openai/index.js.map +1 -0
  76. package/dist/integrations/openai/speech.d.ts +21 -0
  77. package/dist/integrations/openai/speech.d.ts.map +1 -0
  78. package/dist/integrations/openai/speech.js +75 -0
  79. package/dist/integrations/openai/speech.js.map +1 -0
  80. package/dist/integrations/openai/video.d.ts +15 -0
  81. package/dist/integrations/openai/video.d.ts.map +1 -0
  82. package/dist/integrations/openai/video.js +131 -0
  83. package/dist/integrations/openai/video.js.map +1 -0
  84. package/dist/integrations/openai/vision.d.ts +23 -0
  85. package/dist/integrations/openai/vision.d.ts.map +1 -0
  86. package/dist/integrations/openai/vision.js +116 -0
  87. package/dist/integrations/openai/vision.js.map +1 -0
  88. package/dist/integrations/openai/voice-emotion.d.ts +41 -0
  89. package/dist/integrations/openai/voice-emotion.d.ts.map +1 -0
  90. package/dist/integrations/openai/voice-emotion.js +50 -0
  91. package/dist/integrations/openai/voice-emotion.js.map +1 -0
  92. package/dist/integrations/telegram/types.d.ts +112 -0
  93. package/dist/integrations/telegram/types.d.ts.map +1 -0
  94. package/dist/integrations/telegram/types.js +6 -0
  95. package/dist/integrations/telegram/types.js.map +1 -0
  96. package/dist/memory.d.ts +6 -205
  97. package/dist/memory.d.ts.map +1 -1
  98. package/dist/memory.js +6 -1357
  99. package/dist/memory.js.map +1 -1
  100. package/dist/openai.d.ts +11 -102
  101. package/dist/openai.d.ts.map +1 -1
  102. package/dist/openai.js +14 -421
  103. package/dist/openai.js.map +1 -1
  104. package/dist/response-builders.d.ts +1 -11
  105. package/dist/response-builders.d.ts.map +1 -1
  106. package/dist/response-builders.js +2 -38
  107. package/dist/response-builders.js.map +1 -1
  108. package/dist/server/factory.d.ts +17 -0
  109. package/dist/server/factory.d.ts.map +1 -0
  110. package/dist/server/factory.js +279 -0
  111. package/dist/server/factory.js.map +1 -0
  112. package/dist/services/dispatcher/broker.d.ts +83 -0
  113. package/dist/services/dispatcher/broker.d.ts.map +1 -0
  114. package/dist/services/dispatcher/broker.js +175 -0
  115. package/dist/services/dispatcher/broker.js.map +1 -0
  116. package/dist/services/dispatcher/index.d.ts +7 -0
  117. package/dist/services/dispatcher/index.d.ts.map +1 -0
  118. package/dist/services/dispatcher/index.js +7 -0
  119. package/dist/services/dispatcher/index.js.map +1 -0
  120. package/dist/services/dispatcher/lock.d.ts +25 -0
  121. package/dist/services/dispatcher/lock.d.ts.map +1 -0
  122. package/dist/services/dispatcher/lock.js +111 -0
  123. package/dist/services/dispatcher/lock.js.map +1 -0
  124. package/dist/services/dispatcher/poller.d.ts +19 -0
  125. package/dist/services/dispatcher/poller.d.ts.map +1 -0
  126. package/dist/services/dispatcher/poller.js +269 -0
  127. package/dist/services/dispatcher/poller.js.map +1 -0
  128. package/dist/telegram.d.ts +2 -88
  129. package/dist/telegram.d.ts.map +1 -1
  130. package/dist/telegram.js +2 -0
  131. package/dist/telegram.js.map +1 -1
  132. package/dist/tool-definitions.d.ts +1 -14
  133. package/dist/tool-definitions.d.ts.map +1 -1
  134. package/dist/tool-definitions.js +1 -403
  135. package/dist/tool-definitions.js.map +1 -1
  136. package/dist/tools/definitions.d.ts +15 -0
  137. package/dist/tools/definitions.d.ts.map +1 -0
  138. package/dist/tools/definitions.js +404 -0
  139. package/dist/tools/definitions.js.map +1 -0
  140. package/dist/tools/start-session-tool.d.ts.map +1 -1
  141. package/dist/tools/start-session-tool.js +2 -0
  142. package/dist/tools/start-session-tool.js.map +1 -1
  143. package/dist/tools/wait/drive-handler.d.ts +61 -0
  144. package/dist/tools/wait/drive-handler.d.ts.map +1 -0
  145. package/dist/tools/wait/drive-handler.js +138 -0
  146. package/dist/tools/wait/drive-handler.js.map +1 -0
  147. package/dist/tools/wait/index.d.ts +8 -0
  148. package/dist/tools/wait/index.d.ts.map +1 -0
  149. package/dist/tools/wait/index.js +8 -0
  150. package/dist/tools/wait/index.js.map +1 -0
  151. package/dist/tools/wait/media-processor.d.ts +52 -0
  152. package/dist/tools/wait/media-processor.d.ts.map +1 -0
  153. package/dist/tools/wait/media-processor.js +261 -0
  154. package/dist/tools/wait/media-processor.js.map +1 -0
  155. package/dist/tools/wait/message-delivery.d.ts +63 -0
  156. package/dist/tools/wait/message-delivery.d.ts.map +1 -0
  157. package/dist/tools/wait/message-delivery.js +281 -0
  158. package/dist/tools/wait/message-delivery.js.map +1 -0
  159. package/dist/tools/wait/poll-loop.d.ts +72 -0
  160. package/dist/tools/wait/poll-loop.d.ts.map +1 -0
  161. package/dist/tools/wait/poll-loop.js +280 -0
  162. package/dist/tools/wait/poll-loop.js.map +1 -0
  163. package/dist/tools/wait/reaction-handler.d.ts +49 -0
  164. package/dist/tools/wait/reaction-handler.d.ts.map +1 -0
  165. package/dist/tools/wait/reaction-handler.js +126 -0
  166. package/dist/tools/wait/reaction-handler.js.map +1 -0
  167. package/dist/tools/wait/task-handler.d.ts +40 -0
  168. package/dist/tools/wait/task-handler.d.ts.map +1 -0
  169. package/dist/tools/wait/task-handler.js +41 -0
  170. package/dist/tools/wait/task-handler.js.map +1 -0
  171. package/dist/tools/wait-tool.d.ts +3 -69
  172. package/dist/tools/wait-tool.d.ts.map +1 -1
  173. package/dist/tools/wait-tool.js +3 -876
  174. package/dist/tools/wait-tool.js.map +1 -1
  175. package/package.json +1 -1
  176. package/templates/daily-review.default.md +26 -0
  177. package/templates/drive-dispatcher.default.md +2 -0
@@ -1,879 +1,6 @@
1
1
  /**
2
- * remote_copilot_wait_for_instructions tool handler extracted from index.ts.
3
- *
4
- * This is the core long-polling loop that:
5
- * - Polls the dispatcher for new operator messages every 2s
6
- * - Processes all media types: text, photo, document, voice, video_note
7
- * - Runs voice analysis (transcription + emotion via VANPY)
8
- * - Auto-saves episodes to memory
9
- * - Injects relevant memory context via GPT-4o-mini smart filter
10
- * - Checks scheduled tasks during idle polling
11
- * - Triggers auto-consolidation (idle, episode-count, time-based)
12
- * - Sends SSE keepalive pings every 30s
13
- * - Detects maintenance flags and instructs agent to wait externally
14
- * - Activates the Dispatcher drive after extended operator silence
2
+ * Re-export shim — the poll-loop implementation has moved to tools/wait/poll-loop.ts.
3
+ * This file is kept so existing imports from "./tools/wait-tool.js" continue to work.
15
4
  */
16
- import { basename } from "node:path";
17
- import { checkMaintenanceFlag, saveFileToDisk } from "../config.js";
18
- import { peekThreadMessages, readThreadMessages, readPendingReaction } from "../dispatcher.js";
19
- import { formatDrivePrompt, PHASE3_APPROVAL_PROMPT } from "../drive.js";
20
- import { assembleCompactRefresh, runIntelligentConsolidation, saveEpisode, saveVoiceSignature, searchByEmbedding, searchSemanticNotesRanked, } from "../memory.js";
21
- import { analyzeVideoFrames, analyzeVoiceEmotion, chatCompletion, extractVideoFrames, generateEmbedding, transcribeAudio, } from "../openai.js";
22
- import { checkDueTasks, listSchedules } from "../scheduler.js";
23
- import { errorMessage, IMAGE_EXTENSIONS } from "../utils.js";
24
- import { log } from "../logger.js";
25
- import { extractSearchKeywords, buildAnalysisTags, getReminders, getMediumReminder, getShortReminder } from "../response-builders.js";
26
- import { classifyIntent } from "../intent.js";
27
- import { backfillEmbeddings } from "./memory-tools.js";
28
- // ---------------------------------------------------------------------------
29
- // Handler
30
- // ---------------------------------------------------------------------------
31
- export async function handleWaitForInstructions(args, ctx, extra) {
32
- const { state, telegram, telegramChatId, config, getMemoryDb } = ctx;
33
- const { OPENAI_API_KEY, VOICE_ANALYSIS_URL, WAIT_TIMEOUT_MINUTES, AUTONOMOUS_MODE } = config;
34
- // Agent is actively polling — this is the primary health signal
35
- state.deadSessionAlerted = false;
36
- state.toolCallsSinceLastDelivery = 0;
37
- const effectiveThreadId = ctx.resolveThreadId(args);
38
- if (effectiveThreadId === undefined) {
39
- return ctx.errorResult("Error: No active session. Call start_session first, then pass the returned threadId to this tool.");
40
- }
41
- const callNumber = ++state.waitCallCount;
42
- const timeoutMs = WAIT_TIMEOUT_MINUTES * 60 * 1000;
43
- const deadline = Date.now() + timeoutMs;
44
- // Poll the dispatcher's per-thread file instead of calling getUpdates
45
- // directly. This avoids 409 conflicts between concurrent instances.
46
- const POLL_INTERVAL_MS = 2000;
47
- const SSE_KEEPALIVE_INTERVAL_MS = 30_000;
48
- let lastScheduleCheck = 0;
49
- let lastKeepalive = Date.now();
50
- let maintenanceNotified = false;
51
- while (Date.now() < deadline) {
52
- // Check for pending update — tell agent to wait externally via Desktop Commander
53
- // CRITICAL: Do NOT tell agents to call hibernate or any MCP tool here — the server
54
- // is about to die. Agents must use an external sleep (PowerShell Start-Sleep) instead.
55
- const maintenanceInfo = checkMaintenanceFlag();
56
- if (maintenanceInfo) {
57
- log.info(`[wait] Maintenance flag detected: ${maintenanceInfo}`);
58
- // Notify operator via Telegram once
59
- if (!maintenanceNotified) {
60
- maintenanceNotified = true;
61
- let version = "unknown";
62
- try {
63
- version = JSON.parse(maintenanceInfo).version ?? version;
64
- }
65
- catch { /* not JSON or missing field */ }
66
- telegram.sendMessage(telegramChatId, `\u26A0\uFE0F Server update: v${version} deploying. Agent sessions will reconnect after update.`, undefined, effectiveThreadId).catch(() => { });
67
- }
68
- return {
69
- content: [{
70
- type: "text",
71
- text: `āš ļø **Server update pending** (${maintenanceInfo}). ` +
72
- `The MCP server will restart shortly. Use Desktop Commander to run: ` +
73
- `Start-Sleep -Seconds 600 — then call start_session with threadId=${effectiveThreadId} to reconnect.` +
74
- getShortReminder(effectiveThreadId, state.sessionStartedAt),
75
- }],
76
- };
77
- }
78
- // Peek first (non-destructive) to avoid consuming messages when the
79
- // SSE connection may be dead.
80
- const peeked = peekThreadMessages(effectiveThreadId);
81
- if (peeked.length > 0) {
82
- // Verify SSE connection is alive BEFORE consuming messages.
83
- // This prevents the destructive readThreadMessages from eating
84
- // messages that can never be delivered to a dead connection.
85
- if (extra.signal.aborted) {
86
- log.warn(`[wait] SSE connection aborted before consuming ${peeked.length} messages — leaving in queue.`);
87
- return {
88
- content: [{
89
- type: "text",
90
- text: "The connection was interrupted. Messages are preserved for the next call.",
91
- }],
92
- };
93
- }
94
- // Connection alive — now consume messages for real.
95
- const stored = readThreadMessages(effectiveThreadId);
96
- log.info(`[wait] Read ${stored.length} messages from thread ${effectiveThreadId}. Processing...`);
97
- // Update the operator activity timestamp and last message text.
98
- state.lastOperatorMessageAt = Date.now();
99
- state.lastOperatorMessageText = stored
100
- .map(m => m.message.text ?? m.message.caption ?? "")
101
- .filter(Boolean)
102
- .join("\n")
103
- .slice(0, 2000) || "";
104
- // Clear only the consumed IDs from the previewed set (scoped clear).
105
- // This is safe because Node.js is single-threaded — no report_progress
106
- // call can interleave between readThreadMessages and this cleanup.
107
- for (const msg of stored) {
108
- state.previewedUpdateIds.delete(msg.update_id);
109
- }
110
- // React with šŸ‘€ on each consumed message to signal "seen" to the operator.
111
- for (const msg of stored) {
112
- void telegram.setMessageReaction(telegramChatId, msg.message.message_id).catch(() => { });
113
- }
114
- const contentBlocks = [];
115
- let hasVoiceMessages = false;
116
- // Track which messages already had episodes saved (voice/video handlers)
117
- const savedEpisodeUpdateIds = new Set();
118
- for (const msg of stored) {
119
- // Photos: download the largest size, persist to disk, and embed as base64.
120
- if (msg.message.photo && msg.message.photo.length > 0) {
121
- const largest = msg.message.photo[msg.message.photo.length - 1];
122
- try {
123
- const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(largest.file_id);
124
- const ext = telegramPath.split(".").pop()?.toLowerCase() ?? "jpg";
125
- const mimeType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
126
- const base64 = buffer.toString("base64");
127
- const diskPath = saveFileToDisk(buffer, `photo.${ext}`);
128
- contentBlocks.push({ type: "image", data: base64, mimeType });
129
- contentBlocks.push({
130
- type: "text",
131
- text: `[Photo saved to: ${diskPath}]` +
132
- (msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
133
- });
134
- }
135
- catch (err) {
136
- contentBlocks.push({
137
- type: "text",
138
- text: `[Photo received but could not be downloaded: ${errorMessage(err)}]`,
139
- });
140
- }
141
- }
142
- // Documents: download, persist to disk, and embed as base64.
143
- if (msg.message.document) {
144
- const doc = msg.message.document;
145
- try {
146
- const { buffer, filePath: telegramPath } = await telegram.downloadFileAsBuffer(doc.file_id);
147
- const filename = doc.file_name ?? basename(telegramPath);
148
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
149
- const mimeType = doc.mime_type ?? (IMAGE_EXTENSIONS.has(ext) ? `image/${ext === "jpg" ? "jpeg" : ext}` : "application/octet-stream");
150
- const base64 = buffer.toString("base64");
151
- const diskPath = saveFileToDisk(buffer, filename);
152
- const isImage = mimeType.startsWith("image/");
153
- if (isImage) {
154
- contentBlocks.push({ type: "image", data: base64, mimeType });
155
- contentBlocks.push({
156
- type: "text",
157
- text: `[File saved to: ${diskPath}]` +
158
- (msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
159
- });
160
- }
161
- else {
162
- // Non-image documents: provide the disk path instead of
163
- // dumping potentially huge base64 into the LLM context.
164
- contentBlocks.push({
165
- type: "text",
166
- text: `[Document: ${filename} (${mimeType}) — saved to: ${diskPath}]` +
167
- (msg.message.caption ? ` Caption: ${msg.message.caption}` : ""),
168
- });
169
- }
170
- }
171
- catch (err) {
172
- contentBlocks.push({
173
- type: "text",
174
- text: `[Document "${doc.file_name ?? "file"}" received but could not be downloaded: ${errorMessage(err)}]`,
175
- });
176
- }
177
- }
178
- // Text messages.
179
- if (msg.message.text) {
180
- contentBlocks.push({ type: "text", text: msg.message.text });
181
- }
182
- // Voice messages: transcribe using OpenAI Whisper.
183
- if (msg.message.voice) {
184
- hasVoiceMessages = true;
185
- if (OPENAI_API_KEY) {
186
- try {
187
- log.verbose("voice", `Downloading voice file ${msg.message.voice.file_id}...`);
188
- const { buffer } = await telegram.downloadFileAsBuffer(msg.message.voice.file_id);
189
- log.verbose("voice", `Downloaded ${buffer.length} bytes. Starting transcription + analysis...`);
190
- // Run transcription and voice analysis in parallel.
191
- const [transcript, analysis] = await Promise.all([
192
- transcribeAudio(buffer, OPENAI_API_KEY),
193
- VOICE_ANALYSIS_URL
194
- ? analyzeVoiceEmotion(buffer, VOICE_ANALYSIS_URL)
195
- : Promise.resolve(null),
196
- ]);
197
- // Build rich voice analysis tag from VANPY results.
198
- const tags = buildAnalysisTags(analysis);
199
- const analysisTag = tags.length > 0 ? ` | ${tags.join(", ")}` : "";
200
- contentBlocks.push({
201
- type: "text",
202
- text: transcript
203
- ? `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: ${transcript}`
204
- : `[Voice message — ${msg.message.voice.duration}s${analysisTag}, transcribed]: (empty — no speech detected)`,
205
- });
206
- // Auto-save voice signature
207
- if (analysis && effectiveThreadId !== undefined) {
208
- try {
209
- const db = getMemoryDb();
210
- const sessionId = `session_${state.sessionStartedAt}`;
211
- const epId = saveEpisode(db, {
212
- sessionId,
213
- threadId: effectiveThreadId,
214
- type: "operator_message",
215
- modality: "voice",
216
- content: { text: transcript ?? "", duration: msg.message.voice.duration },
217
- importance: 0.6,
218
- });
219
- saveVoiceSignature(db, {
220
- episodeId: epId,
221
- emotion: analysis.emotion ?? undefined,
222
- arousal: analysis.arousal ?? undefined,
223
- dominance: analysis.dominance ?? undefined,
224
- valence: analysis.valence ?? undefined,
225
- speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
226
- meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
227
- pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
228
- jitter: analysis.paralinguistics?.jitter ?? undefined,
229
- shimmer: analysis.paralinguistics?.shimmer ?? undefined,
230
- hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
231
- audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
232
- durationSec: msg.message.voice.duration,
233
- });
234
- savedEpisodeUpdateIds.add(msg.update_id);
235
- }
236
- catch (_) { /* non-fatal */ }
237
- }
238
- }
239
- catch (err) {
240
- contentBlocks.push({
241
- type: "text",
242
- text: `[Voice message — ${msg.message.voice.duration}s — transcription failed: ${errorMessage(err)}]`,
243
- });
244
- }
245
- }
246
- else {
247
- contentBlocks.push({
248
- type: "text",
249
- text: `[Voice message received — ${msg.message.voice.duration}s — cannot transcribe: OPENAI_API_KEY not set]`,
250
- });
251
- }
252
- }
253
- // Stickers: deliver as text with emoji, set name, and file_id (so agent can re-use it).
254
- if (msg.message.sticker) {
255
- const emoji = msg.message.sticker.emoji || "šŸ·ļø";
256
- const setName = msg.message.sticker.set_name || "unknown";
257
- const fileId = msg.message.sticker.file_id;
258
- contentBlocks.push({
259
- type: "text",
260
- text: `(The operator sent a sticker: ${emoji} from pack "${setName}", file_id: "${fileId}")`,
261
- });
262
- }
263
- // Animations / GIFs: download full file, extract frames, run multi-frame vision analysis
264
- // (same pipeline as video_notes — uses extractVideoFrames + analyzeVideoFrames).
265
- if (msg.message.animation) {
266
- const anim = msg.message.animation;
267
- const animDuration = anim.duration ?? 3; // default to 3s if Telegram omits duration
268
- if (OPENAI_API_KEY) {
269
- try {
270
- log.verbose("gif", `Downloading animation ${anim.file_id} (~${animDuration}s)...`);
271
- const { buffer } = await telegram.downloadFileAsBuffer(anim.file_id);
272
- const diskPath = saveFileToDisk(buffer, "gif-animation.mp4");
273
- log.verbose("gif", `Downloaded ${buffer.length} bytes. Extracting frames...`);
274
- // Extract frames with ffmpeg (same as video_notes).
275
- const frames = await extractVideoFrames(buffer, animDuration).catch((err) => {
276
- log.error(`[gif] Frame extraction failed: ${errorMessage(err)}`);
277
- return [];
278
- });
279
- // Analyze frames with GPT-4o-mini vision (same as video_notes).
280
- let sceneDescription = null;
281
- if (frames.length > 0) {
282
- try {
283
- log.verbose("gif", `Analyzing ${frames.length} frames with GPT-4o-mini vision...`);
284
- sceneDescription = await analyzeVideoFrames(frames, animDuration, OPENAI_API_KEY);
285
- log.verbose("gif", `Vision analysis complete.`);
286
- }
287
- catch (visionErr) {
288
- log.error(`[gif] Vision analysis failed: ${visionErr}`);
289
- sceneDescription = null;
290
- }
291
- }
292
- const caption = msg.message.caption || "";
293
- const parts = [];
294
- parts.push(`(The operator sent a GIF — ${animDuration}s)`);
295
- if (sceneDescription)
296
- parts.push(`Scene: ${sceneDescription}`);
297
- if (!sceneDescription)
298
- parts.push("(no visual content could be extracted)");
299
- parts.push(`Saved to: ${diskPath}`);
300
- if (caption)
301
- parts.push(`Caption: ${caption}`);
302
- contentBlocks.push({ type: "text", text: parts.join("\n") });
303
- }
304
- catch (err) {
305
- contentBlocks.push({
306
- type: "text",
307
- text: `(The operator sent a GIF — analysis failed: ${errorMessage(err)})`,
308
- });
309
- }
310
- }
311
- else {
312
- contentBlocks.push({
313
- type: "text",
314
- text: `(The operator sent a GIF — cannot analyze: OPENAI_API_KEY not set)`,
315
- });
316
- }
317
- }
318
- // Video notes (circle videos): extract frames, analyze with GPT-4.1 vision,
319
- // optionally transcribe the audio track.
320
- if (msg.message.video_note) {
321
- hasVoiceMessages = true; // Video notes often contain speech
322
- const vn = msg.message.video_note;
323
- if (OPENAI_API_KEY) {
324
- try {
325
- log.verbose("video-note", `Downloading circle video ${vn.file_id} (${vn.duration}s)...`);
326
- const { buffer } = await telegram.downloadFileAsBuffer(vn.file_id);
327
- log.verbose("video-note", `Downloaded ${buffer.length} bytes. Extracting frames + transcribing...`);
328
- // Run frame extraction, audio transcription, and voice analysis in parallel.
329
- const [frames, transcript, analysis] = await Promise.all([
330
- extractVideoFrames(buffer, vn.duration).catch((err) => {
331
- log.error(`[video-note] Frame extraction failed: ${errorMessage(err)}`);
332
- return [];
333
- }),
334
- transcribeAudio(buffer, OPENAI_API_KEY, "video.mp4").catch(() => ""),
335
- VOICE_ANALYSIS_URL
336
- ? analyzeVoiceEmotion(buffer, VOICE_ANALYSIS_URL, {
337
- mimeType: "video/mp4",
338
- filename: "video.mp4",
339
- }).catch(() => null)
340
- : Promise.resolve(null),
341
- ]);
342
- // Analyze frames with GPT-4o-mini vision.
343
- let sceneDescription = "";
344
- if (frames.length > 0) {
345
- try {
346
- log.verbose("video-note", `Analyzing ${frames.length} frames with GPT-4o-mini vision...`);
347
- sceneDescription = await analyzeVideoFrames(frames, vn.duration, OPENAI_API_KEY);
348
- log.verbose("video-note", `Vision analysis complete.`);
349
- }
350
- catch (visionErr) {
351
- log.error(`[video-note] Vision analysis failed: ${visionErr}`);
352
- sceneDescription = null;
353
- }
354
- }
355
- // Build analysis tags (same as voice messages).
356
- const tags = buildAnalysisTags(analysis);
357
- const analysisTag = tags.length > 0 ? ` | ${tags.join(", ")}` : "";
358
- const parts = [];
359
- parts.push(`[Video note — ${vn.duration}s${analysisTag}]`);
360
- if (sceneDescription)
361
- parts.push(`Scene: ${sceneDescription}`);
362
- if (transcript)
363
- parts.push(`Audio: "${transcript}"`);
364
- if (!sceneDescription && !transcript)
365
- parts.push("(no visual or audio content could be extracted)");
366
- contentBlocks.push({ type: "text", text: parts.join("\n") });
367
- // Auto-save voice signature for video notes
368
- if (analysis && effectiveThreadId !== undefined) {
369
- try {
370
- const db = getMemoryDb();
371
- const sessionId = `session_${state.sessionStartedAt}`;
372
- const epId = saveEpisode(db, {
373
- sessionId,
374
- threadId: effectiveThreadId,
375
- type: "operator_message",
376
- modality: "video_note",
377
- content: { text: transcript ?? "", scene: sceneDescription ?? "", duration: vn.duration },
378
- importance: 0.6,
379
- });
380
- saveVoiceSignature(db, {
381
- episodeId: epId,
382
- emotion: analysis.emotion ?? undefined,
383
- arousal: analysis.arousal ?? undefined,
384
- dominance: analysis.dominance ?? undefined,
385
- valence: analysis.valence ?? undefined,
386
- speechRate: analysis.paralinguistics?.speech_rate ?? undefined,
387
- meanPitchHz: analysis.paralinguistics?.mean_pitch_hz ?? undefined,
388
- pitchStdHz: analysis.paralinguistics?.pitch_std_hz ?? undefined,
389
- jitter: analysis.paralinguistics?.jitter ?? undefined,
390
- shimmer: analysis.paralinguistics?.shimmer ?? undefined,
391
- hnrDb: analysis.paralinguistics?.hnr_db ?? undefined,
392
- audioEvents: analysis.audio_events?.map(e => ({ label: e.label, confidence: e.score })),
393
- durationSec: vn.duration,
394
- });
395
- savedEpisodeUpdateIds.add(msg.update_id);
396
- }
397
- catch (_) { /* non-fatal */ }
398
- }
399
- }
400
- catch (err) {
401
- contentBlocks.push({
402
- type: "text",
403
- text: `[Video note — ${vn.duration}s — analysis failed: ${errorMessage(err)}]`,
404
- });
405
- }
406
- }
407
- else {
408
- contentBlocks.push({
409
- type: "text",
410
- text: `[Video note received — ${vn.duration}s — cannot analyze: OPENAI_API_KEY not set]`,
411
- });
412
- }
413
- }
414
- }
415
- if (contentBlocks.length === 0) {
416
- const msgKeys = stored.map(m => Object.keys(m.message).filter(k => m.message[k] != null).join(",")).join(" | ");
417
- log.warn(`[wait] No content blocks from ${stored.length} messages. Fields: ${msgKeys}`);
418
- contentBlocks.push({
419
- type: "text",
420
- text: "[Unsupported message type received — the operator sent a message type that cannot be processed (e.g., sticker, location, contact). Please ask them to resend as text, photo, document, or voice.]",
421
- });
422
- }
423
- log.info(`[wait] ${contentBlocks.length} content blocks built. Saving episodes...`);
424
- // Auto-ingest episodes for messages not already saved by voice/video handlers
425
- try {
426
- const db = getMemoryDb();
427
- const sessionId = `session_${state.sessionStartedAt}`;
428
- if (effectiveThreadId !== undefined) {
429
- // Collect text from messages that didn't already get an episode
430
- const unsavedMsgs = stored.filter(m => !savedEpisodeUpdateIds.has(m.update_id));
431
- if (unsavedMsgs.length > 0) {
432
- const textContent = unsavedMsgs
433
- .map(m => m.message.text ?? m.message.caption ?? "")
434
- .filter(Boolean)
435
- .join("\n")
436
- .slice(0, 2000);
437
- if (textContent) {
438
- saveEpisode(db, {
439
- sessionId,
440
- threadId: effectiveThreadId,
441
- type: "operator_message",
442
- modality: "text",
443
- content: { text: textContent },
444
- importance: 0.5,
445
- });
446
- }
447
- }
448
- }
449
- }
450
- catch (_) { /* memory write failures should never break the main flow */ }
451
- // ── Check for pending operator reactions ─────────────────────────
452
- const pendingReaction = readPendingReaction() ?? telegram.lastReaction;
453
- if (pendingReaction) {
454
- const emoji = "emoji" in pendingReaction ? pendingReaction.emoji : "";
455
- const messageId = "messageId" in pendingReaction ? pendingReaction.messageId : 0;
456
- const reactionDate = "date" in pendingReaction ? pendingReaction.date : 0;
457
- if (emoji) {
458
- const snippet = telegram.lookupSentMessage(messageId);
459
- const reactionNote = snippet
460
- ? `(The operator reacted with ${emoji} to your message: '${snippet}')`
461
- : `(The operator reacted with ${emoji} to message #${messageId})`;
462
- // Inline the reaction with the last text block if messages exist,
463
- // otherwise add it as a standalone block.
464
- const lastTextIdx = contentBlocks.map(b => b.type).lastIndexOf("text");
465
- if (lastTextIdx >= 0) {
466
- const prev = contentBlocks[lastTextIdx];
467
- prev.text = `${prev.text}\n${reactionNote}`;
468
- }
469
- else {
470
- contentBlocks.push({ type: "text", text: reactionNote });
471
- }
472
- // Save reaction as episodic memory
473
- try {
474
- const db = getMemoryDb();
475
- const sessionId = `session_${state.sessionStartedAt}`;
476
- if (effectiveThreadId !== undefined) {
477
- saveEpisode(db, {
478
- sessionId,
479
- threadId: effectiveThreadId,
480
- type: "operator_reaction",
481
- modality: "reaction",
482
- content: { emoji, messageId, date: reactionDate },
483
- importance: 0.3,
484
- });
485
- }
486
- }
487
- catch (_) { /* non-fatal */ }
488
- }
489
- // Clear the reaction after delivery
490
- telegram.lastReaction = null;
491
- }
492
- log.info(`[wait] Episodes saved. Building auto-memory context...`);
493
- // Extract operator text for memory search and intent classification.
494
- const operatorText = stored
495
- .map(m => m.message.text ?? m.message.caption ?? "")
496
- .filter(Boolean)
497
- .join(" ")
498
- .slice(0, 500);
499
- // ── Smart context injection (GPT-4o-mini preprocessor) ──────────
500
- // Retrieves candidate notes via embedding search, then uses GPT-4o-mini
501
- // to select ONLY the notes truly relevant to the operator's message.
502
- // This prevents context contamination from near-miss semantic matches.
503
- let autoMemoryContext = "";
504
- try {
505
- const db = getMemoryDb();
506
- const apiKey = process.env.OPENAI_API_KEY;
507
- if (operatorText.length > 10 && apiKey) {
508
- // Phase 1: Broad retrieval — get 10 candidates via embedding search
509
- let candidates = [];
510
- try {
511
- const queryEmb = await generateEmbedding(operatorText, apiKey);
512
- const embResults = searchByEmbedding(db, queryEmb, { maxResults: 10, minSimilarity: 0.25, skipAccessTracking: true, threadId: effectiveThreadId });
513
- candidates = embResults.map(n => ({ type: n.type, content: n.content.slice(0, 200), confidence: n.confidence, similarity: n.similarity }));
514
- }
515
- catch {
516
- // Fallback to keyword search
517
- const searchQuery = extractSearchKeywords(operatorText);
518
- if (searchQuery.trim().length > 0) {
519
- const kwResults = searchSemanticNotesRanked(db, searchQuery, { maxResults: 10, skipAccessTracking: true, threadId: effectiveThreadId });
520
- candidates = kwResults.map(n => ({ type: n.type, content: n.content.slice(0, 200), confidence: n.confidence }));
521
- }
522
- }
523
- if (candidates.length > 0) {
524
- // Phase 2: GPT-4o-mini filters and compresses
525
- try {
526
- const noteList = candidates.map((c, i) => `[${i}] [${c.type}] ${c.content}`).join("\n");
527
- const filterResponse = await chatCompletion([
528
- {
529
- role: "system",
530
- content: "You are a context filter for an AI assistant. Given an operator's message and candidate memory notes, " +
531
- "select ONLY the notes that are directly relevant to the operator's current instruction or question. " +
532
- "Discard notes that are tangentially related, duplicates, or noise. " +
533
- "Return a JSON array of objects: [{\"i\": <index>, \"s\": \"<compressed one-liner>\"}] " +
534
- "where 'i' is the note index and 's' is a compressed summary (max 80 chars). " +
535
- "Return [] if no notes are relevant. Return at most 3 notes. Be aggressive about filtering.",
536
- },
537
- {
538
- role: "user",
539
- content: `Operator message: "${operatorText.slice(0, 300)}"\n\nCandidate notes:\n${noteList}`,
540
- },
541
- ], apiKey, { maxTokens: 200, temperature: 0 });
542
- // Parse the response — expect JSON array
543
- const jsonMatch = filterResponse.match(/\[.*\]/s);
544
- if (jsonMatch) {
545
- const filtered = JSON.parse(jsonMatch[0]);
546
- if (filtered.length > 0) {
547
- const lines = filtered
548
- .filter(f => f.i >= 0 && f.i < candidates.length)
549
- .slice(0, 3)
550
- .map(f => {
551
- const c = candidates[f.i];
552
- return `- **[${c.type}]** ${f.s} _(conf: ${c.confidence})_`;
553
- });
554
- if (lines.length > 0) {
555
- autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
556
- }
557
- }
558
- }
559
- log.verbose("memory", `Smart filter: ${candidates.length} candidates → ${(jsonMatch ? JSON.parse(jsonMatch[0]) : []).length} selected`);
560
- }
561
- catch (filterErr) {
562
- // GPT-4o-mini filter failed — fall back to top-3 raw notes
563
- log.warn(`[memory] Smart filter failed, using raw top-3: ${filterErr instanceof Error ? filterErr.message : String(filterErr)}`);
564
- const lines = candidates.slice(0, 3).map(c => `- **[${c.type}]** ${c.content} _(conf: ${c.confidence})_`);
565
- autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
566
- }
567
- }
568
- }
569
- else if (operatorText.length > 10) {
570
- // No API key — keyword search, raw top-3
571
- const searchQuery = extractSearchKeywords(operatorText);
572
- if (searchQuery.trim().length > 0) {
573
- const kwResults = searchSemanticNotesRanked(db, searchQuery, { maxResults: 3, skipAccessTracking: true, threadId: effectiveThreadId });
574
- if (kwResults.length > 0) {
575
- const lines = kwResults.map(n => `- **[${n.type}]** ${n.content.slice(0, 200)} _(conf: ${n.confidence})_`);
576
- autoMemoryContext = `\n\n## Relevant Memory (auto-injected)\n${lines.join("\n")}`;
577
- }
578
- }
579
- }
580
- }
581
- catch (_) { /* memory search failures should never break message delivery */ }
582
- log.info(`[wait] Returning response with ${contentBlocks.length} blocks to agent.`);
583
- const intent = classifyIntent(operatorText);
584
- log.verbose("intent", `Classified "${operatorText.substring(0, 50)}" as ${intent}`);
585
- const reminder = intent === "conversational"
586
- ? getMediumReminder(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE)
587
- : getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE);
588
- return {
589
- content: [
590
- {
591
- type: "text",
592
- text: "Follow the operator's instructions below.",
593
- },
594
- { type: "text", text: "<<< OPERATOR MESSAGE >>>" },
595
- ...contentBlocks,
596
- ...(hasVoiceMessages
597
- ? [{
598
- type: "text",
599
- text: "(Operator sent voice — respond with `send_voice`.)",
600
- }]
601
- : []),
602
- { type: "text", text: reminder },
603
- { type: "text", text: "<<< END OPERATOR MESSAGE >>>" },
604
- ...(autoMemoryContext
605
- ? [{ type: "text", text: autoMemoryContext }]
606
- : []),
607
- ],
608
- };
609
- }
610
- // ── Reaction-only wake-up ───────────────────────────────────────
611
- // If no text messages arrived but a reaction is pending, return
612
- // immediately so the agent can reflect on the reaction as a CTA.
613
- const pendingReactionOnly = readPendingReaction() ?? telegram.lastReaction;
614
- if (pendingReactionOnly) {
615
- const rEmoji = "emoji" in pendingReactionOnly ? pendingReactionOnly.emoji : "";
616
- const rMsgId = "messageId" in pendingReactionOnly ? pendingReactionOnly.messageId : 0;
617
- const rDate = "date" in pendingReactionOnly ? pendingReactionOnly.date : 0;
618
- if (rEmoji) {
619
- // Clear in-memory reaction after consumption
620
- telegram.lastReaction = null;
621
- const snippet = telegram.lookupSentMessage(rMsgId);
622
- const reactionNote = snippet
623
- ? `(The operator reacted with ${rEmoji} to your message: '${snippet}')`
624
- : `(The operator reacted with ${rEmoji} to message #${rMsgId})`;
625
- // Save reaction as episodic memory
626
- try {
627
- const db = getMemoryDb();
628
- const sessionId = `session_${state.sessionStartedAt}`;
629
- if (effectiveThreadId !== undefined) {
630
- saveEpisode(db, {
631
- sessionId,
632
- threadId: effectiveThreadId,
633
- type: "operator_reaction",
634
- modality: "reaction",
635
- content: { emoji: rEmoji, messageId: rMsgId, date: rDate },
636
- importance: 0.3,
637
- });
638
- }
639
- }
640
- catch (_) { /* non-fatal */ }
641
- log.info(`[wait] Reaction-only wake-up: ${rEmoji} on message ${rMsgId}`);
642
- return {
643
- content: [
644
- { type: "text", text: "<<< OPERATOR REACTION >>>" },
645
- { type: "text", text: reactionNote },
646
- {
647
- type: "text",
648
- text: "The operator reacted to your message without sending a text reply. " +
649
- "This may be a confirmation, approval, or acknowledgment. " +
650
- "Reflect on what your last message said and whether this reaction is a call to action " +
651
- "(e.g., proceed with a plan, continue what you were doing, etc.). " +
652
- "If no action is needed, call `remote_copilot_wait_for_instructions` to resume waiting.",
653
- },
654
- { type: "text", text: "<<< END OPERATOR REACTION >>>" },
655
- {
656
- type: "text",
657
- text: getMediumReminder(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE),
658
- },
659
- ],
660
- };
661
- }
662
- }
663
- // Check scheduled tasks every ~60s during idle polling.
664
- if (effectiveThreadId !== undefined && Date.now() - lastScheduleCheck >= 60_000) {
665
- lastScheduleCheck = Date.now();
666
- const dueTask = checkDueTasks(effectiveThreadId, state.lastOperatorMessageAt, false);
667
- if (dueTask) {
668
- // DMN sentinel: generate dynamic first-person reflection
669
- const taskPrompt = dueTask.prompt === "__DMN__"
670
- ? ctx.generateDmnReflection(effectiveThreadId)
671
- : `ā° **Scheduled task fired: "${dueTask.task.label}"**\n\n` +
672
- `This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
673
- `Task prompt: ${dueTask.prompt}`;
674
- return {
675
- content: [
676
- {
677
- type: "text",
678
- text: taskPrompt + getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE),
679
- },
680
- ],
681
- };
682
- }
683
- }
684
- // No messages yet — sleep briefly and check again.
685
- // Send SSE keepalive to prevent silent connection death during long polls.
686
- if (Date.now() - lastKeepalive >= SSE_KEEPALIVE_INTERVAL_MS) {
687
- lastKeepalive = Date.now();
688
- state.lastToolCallAt = Date.now();
689
- try {
690
- await extra.sendNotification({
691
- method: "notifications/progress",
692
- params: {
693
- progressToken: extra.requestId,
694
- progress: 0,
695
- total: 0,
696
- },
697
- });
698
- }
699
- catch {
700
- // If notification fails, the SSE stream is already dead.
701
- // Return immediately so the agent can reconnect.
702
- log.warn(`[wait] SSE keepalive failed — connection dead. Returning early.`);
703
- state.lastToolCallAt = Date.now();
704
- return {
705
- content: [{
706
- type: "text",
707
- text: "The connection was interrupted. Please call wait_for_instructions again immediately to resume polling.",
708
- }],
709
- };
710
- }
711
- }
712
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
713
- }
714
- // Timeout elapsed with no actionable message.
715
- const now = new Date().toISOString();
716
- // Check for scheduled wake-up tasks.
717
- if (effectiveThreadId !== undefined) {
718
- const dueTask = checkDueTasks(effectiveThreadId, state.lastOperatorMessageAt, false);
719
- if (dueTask) {
720
- // DMN sentinel: generate dynamic first-person reflection
721
- const taskPrompt = dueTask.prompt === "__DMN__"
722
- ? ctx.generateDmnReflection(effectiveThreadId)
723
- : `ā° **Scheduled task fired: "${dueTask.task.label}"**\n\n` +
724
- `This task was scheduled by you. Execute it now using subagents, then report progress and continue waiting.\n\n` +
725
- `Task prompt: ${dueTask.prompt}`;
726
- return {
727
- content: [
728
- {
729
- type: "text",
730
- text: taskPrompt + getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE),
731
- },
732
- ],
733
- };
734
- }
735
- }
736
- const idleMinutes = Math.round((Date.now() - state.lastOperatorMessageAt) / 60000);
737
- // Show pending scheduled tasks if any exist.
738
- let scheduleHint = "";
739
- if (effectiveThreadId !== undefined) {
740
- const pending = listSchedules(effectiveThreadId);
741
- if (pending.length > 0) {
742
- const taskList = pending.map(t => {
743
- let trigger = "";
744
- if (t.runAt) {
745
- trigger = `at ${new Date(t.runAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}`;
746
- }
747
- else if (t.cron) {
748
- trigger = `cron: ${t.cron}`;
749
- }
750
- else if (t.afterIdleMinutes) {
751
- trigger = `after ${t.afterIdleMinutes}min idle`;
752
- }
753
- return ` • "${t.label}" (${trigger})`;
754
- }).join("\n");
755
- scheduleHint = `\n\nšŸ“‹ **Pending scheduled tasks:**\n${taskList}`;
756
- }
757
- }
758
- // ── Auto-consolidation during idle (fire-and-forget) ────────────────────
759
- // Don't await — consolidation can take 10-30s (OpenAI call) and would
760
- // stall the agent's poll loop, silently delaying the timeout response.
761
- try {
762
- const idleMs = Date.now() - state.lastOperatorMessageAt;
763
- if (idleMs > 15 * 60 * 1000 && effectiveThreadId !== undefined && Date.now() - state.lastConsolidationAt > 30 * 60 * 1000) {
764
- state.lastConsolidationAt = Date.now();
765
- const db = getMemoryDb();
766
- void runIntelligentConsolidation(db, effectiveThreadId).then(async (report) => {
767
- if (report.episodesProcessed > 0) {
768
- log.info(`[memory] Consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes`);
769
- }
770
- await backfillEmbeddings(db);
771
- }).catch(err => {
772
- log.error(`[memory] Consolidation error: ${err instanceof Error ? err.message : String(err)}`);
773
- });
774
- }
775
- }
776
- catch (_) { /* consolidation failure is non-fatal */ }
777
- // ── Episode-count consolidation — don't wait for idle ──────────────────
778
- // If many episodes accumulated during active use, consolidate now.
779
- // This prevents stale/contradictory knowledge from persisting.
780
- try {
781
- if (effectiveThreadId !== undefined && Date.now() - state.lastConsolidationAt > 30 * 60 * 1000) {
782
- const db = getMemoryDb();
783
- const uncons = db.prepare("SELECT COUNT(*) as c FROM episodes WHERE consolidated = 0 AND thread_id = ?").get(effectiveThreadId);
784
- if (uncons.c >= 15) {
785
- state.lastConsolidationAt = Date.now();
786
- void runIntelligentConsolidation(db, effectiveThreadId).then(async (report) => {
787
- if (report.episodesProcessed > 0) {
788
- log.info(`[memory] Episode-count consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes`);
789
- }
790
- await backfillEmbeddings(db);
791
- }).catch(err => {
792
- log.error(`[memory] Episode-count consolidation error: ${err instanceof Error ? err.message : String(err)}`);
793
- });
794
- }
795
- }
796
- }
797
- catch (_) { /* non-fatal */ }
798
- // ── Time-based consolidation — every 4 hours regardless ────────────────
799
- // Ensures stale knowledge gets cleaned up even during low-activity periods.
800
- try {
801
- const TIME_CONSOLIDATION_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours
802
- if (effectiveThreadId !== undefined && Date.now() - state.lastConsolidationAt > TIME_CONSOLIDATION_INTERVAL) {
803
- state.lastConsolidationAt = Date.now();
804
- const db = getMemoryDb();
805
- log.info(`[memory] Time-based consolidation triggered (4h since last)`);
806
- void runIntelligentConsolidation(db, effectiveThreadId).then(async (report) => {
807
- if (report.episodesProcessed > 0) {
808
- log.info(`[memory] Time-based consolidation: ${report.episodesProcessed} episodes → ${report.notesCreated} notes`);
809
- }
810
- await backfillEmbeddings(db);
811
- }).catch(err => {
812
- log.error(`[memory] Time-based consolidation error: ${err instanceof Error ? err.message : String(err)}`);
813
- });
814
- }
815
- }
816
- catch (_) { /* non-fatal */ }
817
- // Periodic memory refresh — re-ground the agent every 10 polls (~5h)
818
- // (reduced from 5 since auto-inject now handles per-message context)
819
- let memoryRefresh = "";
820
- if (callNumber % 10 === 0 && effectiveThreadId !== undefined) {
821
- try {
822
- const db = getMemoryDb();
823
- const refresh = assembleCompactRefresh(db, effectiveThreadId);
824
- if (refresh)
825
- memoryRefresh = `\n\n${refresh}`;
826
- }
827
- catch (_) { /* non-fatal */ }
828
- }
829
- // ── 3-Phase Probabilistic Autonomous Drive ──────────────────────────────
830
- const DRIVE_ACTIVATION_MS = config.DMN_ACTIVATION_HOURS * 60 * 60 * 1000;
831
- const DRIVE_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes between probability rolls
832
- const idleMs = Date.now() - state.lastOperatorMessageAt;
833
- // Phase 3: If Phase 2 just fired and the agent came back without engaging
834
- if (state.drivePhase2Fired) {
835
- state.drivePhase2Fired = false; // Reset — only approve once
836
- return {
837
- content: [
838
- {
839
- type: "text",
840
- text: PHASE3_APPROVAL_PROMPT +
841
- memoryRefresh +
842
- scheduleHint +
843
- getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE),
844
- },
845
- ],
846
- };
847
- }
848
- // Phase 1: Probability gate (only if past threshold and cooldown elapsed)
849
- if (idleMs >= DRIVE_ACTIVATION_MS && Date.now() - state.lastDriveAttemptAt >= DRIVE_COOLDOWN_MS) {
850
- state.lastDriveAttemptAt = Date.now();
851
- const driveResult = formatDrivePrompt(idleMs, config.DMN_ACTIVATION_HOURS);
852
- if (driveResult.activated && driveResult.prompt) {
853
- // Phase 2: Intention Elicitation — give the agent full autonomy
854
- state.drivePhase2Fired = true;
855
- return {
856
- content: [
857
- {
858
- type: "text",
859
- text: driveResult.prompt,
860
- },
861
- ...(memoryRefresh ? [{ type: "text", text: memoryRefresh.replace(/^\n\n/, "") }] : []),
862
- { type: "text", text: scheduleHint + getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE) },
863
- ],
864
- };
865
- }
866
- }
867
- return {
868
- content: [
869
- {
870
- type: "text",
871
- text: `No new instructions. Call \`remote_copilot_wait_for_instructions\` again to keep listening.` +
872
- memoryRefresh +
873
- scheduleHint +
874
- getReminders(effectiveThreadId, state.sessionStartedAt, AUTONOMOUS_MODE),
875
- },
876
- ],
877
- };
878
- }
5
+ export { handleWaitForInstructions } from "./wait/index.js";
879
6
  //# sourceMappingURL=wait-tool.js.map