@dotsetlabs/dotclaw 1.9.0 → 2.1.0

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 (171) hide show
  1. package/.env.example +6 -0
  2. package/README.md +13 -8
  3. package/config-examples/groups/global/CLAUDE.md +6 -14
  4. package/config-examples/groups/main/CLAUDE.md +8 -39
  5. package/config-examples/runtime.json +16 -122
  6. package/config-examples/tool-policy.json +2 -15
  7. package/container/agent-runner/package-lock.json +258 -0
  8. package/container/agent-runner/package.json +2 -1
  9. package/container/agent-runner/src/agent-config.ts +62 -47
  10. package/container/agent-runner/src/browser.ts +180 -0
  11. package/container/agent-runner/src/container-protocol.ts +4 -9
  12. package/container/agent-runner/src/id.ts +3 -2
  13. package/container/agent-runner/src/index.ts +331 -846
  14. package/container/agent-runner/src/ipc.ts +3 -33
  15. package/container/agent-runner/src/mcp-client.ts +222 -0
  16. package/container/agent-runner/src/mcp-registry.ts +163 -0
  17. package/container/agent-runner/src/skill-loader.ts +375 -0
  18. package/container/agent-runner/src/tools.ts +154 -184
  19. package/container/agent-runner/src/tts.ts +61 -0
  20. package/dist/admin-commands.d.ts.map +1 -1
  21. package/dist/admin-commands.js +12 -0
  22. package/dist/admin-commands.js.map +1 -1
  23. package/dist/agent-execution.d.ts +5 -9
  24. package/dist/agent-execution.d.ts.map +1 -1
  25. package/dist/agent-execution.js +32 -20
  26. package/dist/agent-execution.js.map +1 -1
  27. package/dist/cli.js +61 -16
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +1 -4
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +2 -5
  32. package/dist/config.js.map +1 -1
  33. package/dist/container-protocol.d.ts +4 -9
  34. package/dist/container-protocol.d.ts.map +1 -1
  35. package/dist/container-runner.d.ts.map +1 -1
  36. package/dist/container-runner.js +3 -8
  37. package/dist/container-runner.js.map +1 -1
  38. package/dist/dashboard.d.ts +5 -6
  39. package/dist/dashboard.d.ts.map +1 -1
  40. package/dist/dashboard.js +12 -60
  41. package/dist/dashboard.js.map +1 -1
  42. package/dist/db.d.ts +1 -59
  43. package/dist/db.d.ts.map +1 -1
  44. package/dist/db.js +41 -262
  45. package/dist/db.js.map +1 -1
  46. package/dist/error-messages.d.ts.map +1 -1
  47. package/dist/error-messages.js +5 -1
  48. package/dist/error-messages.js.map +1 -1
  49. package/dist/hooks.d.ts +7 -0
  50. package/dist/hooks.d.ts.map +1 -0
  51. package/dist/hooks.js +93 -0
  52. package/dist/hooks.js.map +1 -0
  53. package/dist/id.d.ts.map +1 -1
  54. package/dist/id.js +2 -1
  55. package/dist/id.js.map +1 -1
  56. package/dist/index.js +673 -2790
  57. package/dist/index.js.map +1 -1
  58. package/dist/ipc-dispatcher.d.ts +26 -0
  59. package/dist/ipc-dispatcher.d.ts.map +1 -0
  60. package/dist/ipc-dispatcher.js +861 -0
  61. package/dist/ipc-dispatcher.js.map +1 -0
  62. package/dist/local-embeddings.d.ts +7 -0
  63. package/dist/local-embeddings.d.ts.map +1 -0
  64. package/dist/local-embeddings.js +60 -0
  65. package/dist/local-embeddings.js.map +1 -0
  66. package/dist/maintenance.d.ts.map +1 -1
  67. package/dist/maintenance.js +3 -7
  68. package/dist/maintenance.js.map +1 -1
  69. package/dist/memory-embeddings.d.ts +1 -1
  70. package/dist/memory-embeddings.d.ts.map +1 -1
  71. package/dist/memory-embeddings.js +59 -31
  72. package/dist/memory-embeddings.js.map +1 -1
  73. package/dist/memory-store.d.ts +0 -10
  74. package/dist/memory-store.d.ts.map +1 -1
  75. package/dist/memory-store.js +11 -27
  76. package/dist/memory-store.js.map +1 -1
  77. package/dist/message-pipeline.d.ts +47 -0
  78. package/dist/message-pipeline.d.ts.map +1 -0
  79. package/dist/message-pipeline.js +652 -0
  80. package/dist/message-pipeline.js.map +1 -0
  81. package/dist/metrics.d.ts +7 -10
  82. package/dist/metrics.d.ts.map +1 -1
  83. package/dist/metrics.js +2 -33
  84. package/dist/metrics.js.map +1 -1
  85. package/dist/model-registry.d.ts +0 -14
  86. package/dist/model-registry.d.ts.map +1 -1
  87. package/dist/model-registry.js +0 -36
  88. package/dist/model-registry.js.map +1 -1
  89. package/dist/paths.d.ts.map +1 -1
  90. package/dist/paths.js +2 -0
  91. package/dist/paths.js.map +1 -1
  92. package/dist/providers/discord/discord-format.d.ts +16 -0
  93. package/dist/providers/discord/discord-format.d.ts.map +1 -0
  94. package/dist/providers/discord/discord-format.js +153 -0
  95. package/dist/providers/discord/discord-format.js.map +1 -0
  96. package/dist/providers/discord/discord-provider.d.ts +50 -0
  97. package/dist/providers/discord/discord-provider.d.ts.map +1 -0
  98. package/dist/providers/discord/discord-provider.js +607 -0
  99. package/dist/providers/discord/discord-provider.js.map +1 -0
  100. package/dist/providers/discord/index.d.ts +4 -0
  101. package/dist/providers/discord/index.d.ts.map +1 -0
  102. package/dist/providers/discord/index.js +3 -0
  103. package/dist/providers/discord/index.js.map +1 -0
  104. package/dist/providers/registry.d.ts +14 -0
  105. package/dist/providers/registry.d.ts.map +1 -0
  106. package/dist/providers/registry.js +49 -0
  107. package/dist/providers/registry.js.map +1 -0
  108. package/dist/providers/telegram/index.d.ts +4 -0
  109. package/dist/providers/telegram/index.d.ts.map +1 -0
  110. package/dist/providers/telegram/index.js +3 -0
  111. package/dist/providers/telegram/index.js.map +1 -0
  112. package/dist/providers/telegram/telegram-format.d.ts +3 -0
  113. package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
  114. package/dist/providers/telegram/telegram-format.js +215 -0
  115. package/dist/providers/telegram/telegram-format.js.map +1 -0
  116. package/dist/providers/telegram/telegram-provider.d.ts +51 -0
  117. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
  118. package/dist/providers/telegram/telegram-provider.js +824 -0
  119. package/dist/providers/telegram/telegram-provider.js.map +1 -0
  120. package/dist/providers/types.d.ts +107 -0
  121. package/dist/providers/types.d.ts.map +1 -0
  122. package/dist/providers/types.js +2 -0
  123. package/dist/providers/types.js.map +1 -0
  124. package/dist/request-router.d.ts +9 -31
  125. package/dist/request-router.d.ts.map +1 -1
  126. package/dist/request-router.js +12 -142
  127. package/dist/request-router.js.map +1 -1
  128. package/dist/runtime-config.d.ts +79 -101
  129. package/dist/runtime-config.d.ts.map +1 -1
  130. package/dist/runtime-config.js +140 -208
  131. package/dist/runtime-config.js.map +1 -1
  132. package/dist/skill-manager.d.ts +39 -0
  133. package/dist/skill-manager.d.ts.map +1 -0
  134. package/dist/skill-manager.js +286 -0
  135. package/dist/skill-manager.js.map +1 -0
  136. package/dist/streaming.d.ts +58 -0
  137. package/dist/streaming.d.ts.map +1 -0
  138. package/dist/streaming.js +196 -0
  139. package/dist/streaming.js.map +1 -0
  140. package/dist/task-scheduler.d.ts.map +1 -1
  141. package/dist/task-scheduler.js +11 -45
  142. package/dist/task-scheduler.js.map +1 -1
  143. package/dist/tool-policy.d.ts.map +1 -1
  144. package/dist/tool-policy.js +13 -5
  145. package/dist/tool-policy.js.map +1 -1
  146. package/dist/transcription.d.ts +8 -0
  147. package/dist/transcription.d.ts.map +1 -0
  148. package/dist/transcription.js +174 -0
  149. package/dist/transcription.js.map +1 -0
  150. package/dist/types.d.ts +2 -50
  151. package/dist/types.d.ts.map +1 -1
  152. package/package.json +15 -4
  153. package/scripts/bootstrap.js +40 -4
  154. package/scripts/configure.js +129 -7
  155. package/scripts/doctor.js +30 -4
  156. package/scripts/init.js +13 -6
  157. package/scripts/install.sh +1 -1
  158. package/config-examples/plugin-http.json +0 -18
  159. package/container/skills/agent-browser.md +0 -159
  160. package/dist/background-job-classifier.d.ts +0 -20
  161. package/dist/background-job-classifier.d.ts.map +0 -1
  162. package/dist/background-job-classifier.js +0 -145
  163. package/dist/background-job-classifier.js.map +0 -1
  164. package/dist/background-jobs.d.ts +0 -56
  165. package/dist/background-jobs.d.ts.map +0 -1
  166. package/dist/background-jobs.js +0 -550
  167. package/dist/background-jobs.js.map +0 -1
  168. package/dist/planner-probe.d.ts +0 -14
  169. package/dist/planner-probe.d.ts.map +0 -1
  170. package/dist/planner-probe.js +0 -97
  171. package/dist/planner-probe.js.map +0 -1
@@ -0,0 +1,652 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getMessagesSinceCursor, getChatState, updateChatState, enqueueMessageItem, claimBatchForChat, completeQueuedMessages, failQueuedMessages, requeueQueuedMessages, linkMessageToTrace, getPendingMessageCount, } from './db.js';
4
+ import { hostPathToContainerGroupPath } from './path-mapping.js';
5
+ import { recordMessage, recordError, recordStageLatency } from './metrics.js';
6
+ import { synthesizeSpeechHost } from './transcription.js';
7
+ import { emitHook } from './hooks.js';
8
+ import { createTraceBase, executeAgentRun, recordAgentTelemetry, AgentExecutionError } from './agent-execution.js';
9
+ import { logger } from './logger.js';
10
+ import { setLastMessageTime, setMessageQueueDepth } from './dashboard.js';
11
+ import { humanizeError } from './error-messages.js';
12
+ import { routeRequest } from './request-router.js';
13
+ import { GROUPS_DIR, BATCH_WINDOW_MS, MAX_BATCH_SIZE, } from './config.js';
14
+ import { loadRuntimeConfig } from './runtime-config.js';
15
+ import { ProviderRegistry as ProviderRegistryClass } from './providers/registry.js';
16
+ import { StreamingDelivery, watchStreamChunks } from './streaming.js';
17
+ const runtime = loadRuntimeConfig();
18
+ const INTERRUPT_ON_NEW_MESSAGE = runtime.host.messageQueue.interruptOnNewMessage ?? true;
19
+ const MESSAGE_QUEUE_MAX_RETRIES = Math.max(1, runtime.host.messageQueue.maxRetries ?? 4);
20
+ const MESSAGE_QUEUE_RETRY_BASE_MS = Math.max(250, runtime.host.messageQueue.retryBaseMs ?? 3_000);
21
+ const MESSAGE_QUEUE_RETRY_MAX_MS = Math.max(MESSAGE_QUEUE_RETRY_BASE_MS, runtime.host.messageQueue.retryMaxMs ?? 60_000);
22
+ const MAX_DRAIN_ITERATIONS = 50;
23
+ const CANCEL_PHRASES = new Set([
24
+ 'cancel', 'stop', 'abort', 'cancel request', 'stop request'
25
+ ]);
26
+ function sleep(ms) {
27
+ return new Promise(resolve => setTimeout(resolve, ms));
28
+ }
29
+ function isCancelMessage(content) {
30
+ if (!content)
31
+ return false;
32
+ const trimmed = content.trim();
33
+ if (trimmed.length > 20)
34
+ return false;
35
+ return CANCEL_PHRASES.has(trimmed.toLowerCase());
36
+ }
37
+ function clampInputMessage(content, maxChars) {
38
+ if (!content)
39
+ return '';
40
+ if (!Number.isFinite(maxChars) || maxChars <= 0)
41
+ return content;
42
+ if (content.length <= maxChars)
43
+ return content;
44
+ return `${content.slice(0, maxChars)}\n\n[Message truncated for length]`;
45
+ }
46
+ function computeMessageQueueRetryDelayMs(attempt) {
47
+ const exp = Math.max(0, attempt - 1);
48
+ const base = Math.min(MESSAGE_QUEUE_RETRY_MAX_MS, MESSAGE_QUEUE_RETRY_BASE_MS * Math.pow(2, exp));
49
+ const jitter = base * (0.8 + Math.random() * 0.4);
50
+ return Math.max(250, Math.round(jitter));
51
+ }
52
+ class RetryableMessageProcessingError extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = 'RetryableMessageProcessingError';
56
+ }
57
+ }
58
+ function hostPathToContainerPath(hostPath, groupFolder) {
59
+ return hostPathToContainerGroupPath(hostPath, groupFolder, GROUPS_DIR);
60
+ }
61
+ export function buildAttachmentsXml(attachments, groupFolder) {
62
+ if (!attachments || attachments.length === 0)
63
+ return '';
64
+ const escapeXml = (s) => s
65
+ .replace(/&/g, '&amp;')
66
+ .replace(/</g, '&lt;')
67
+ .replace(/>/g, '&gt;')
68
+ .replace(/"/g, '&quot;');
69
+ return attachments.map(a => {
70
+ const attrs = [`type="${escapeXml(a.type)}"`];
71
+ const containerPath = a.local_path ? hostPathToContainerPath(a.local_path, groupFolder) : null;
72
+ if (containerPath)
73
+ attrs.push(`path="${escapeXml(containerPath)}"`);
74
+ if (a.file_name)
75
+ attrs.push(`filename="${escapeXml(a.file_name)}"`);
76
+ if (a.mime_type)
77
+ attrs.push(`mime="${escapeXml(a.mime_type)}"`);
78
+ if (a.file_size)
79
+ attrs.push(`size="${a.file_size}"`);
80
+ if (a.duration)
81
+ attrs.push(`duration="${a.duration}"`);
82
+ if (a.width)
83
+ attrs.push(`width="${a.width}"`);
84
+ if (a.height)
85
+ attrs.push(`height="${a.height}"`);
86
+ if (a.transcript) {
87
+ return `<attachment ${attrs.join(' ')}>\n <transcript>${escapeXml(a.transcript)}</transcript>\n</attachment>`;
88
+ }
89
+ return `<attachment ${attrs.join(' ')} />`;
90
+ }).join('\n');
91
+ }
92
+ export function providerAttachmentToMessageAttachment(pa) {
93
+ return {
94
+ type: pa.type,
95
+ provider_file_ref: pa.providerFileRef,
96
+ file_name: pa.fileName,
97
+ mime_type: pa.mimeType,
98
+ file_size: pa.fileSize,
99
+ local_path: pa.localPath,
100
+ duration: pa.duration,
101
+ width: pa.width,
102
+ height: pa.height,
103
+ transcript: pa.transcript,
104
+ };
105
+ }
106
+ const activeDrains = new Set();
107
+ const activeRuns = new Map();
108
+ export function getActiveDrains() {
109
+ return activeDrains;
110
+ }
111
+ export function getActiveRuns() {
112
+ return activeRuns;
113
+ }
114
+ export function createMessagePipeline(deps) {
115
+ async function sendMessageForQueue(chatId, text, options) {
116
+ const provider = deps.registry.getProviderForChat(chatId);
117
+ const result = await provider.sendMessage(chatId, text, {
118
+ threadId: options?.threadId,
119
+ parseMode: options?.parseMode,
120
+ replyToMessageId: options?.replyToMessageId,
121
+ });
122
+ if (!result.success) {
123
+ throw new RetryableMessageProcessingError('Failed to deliver message');
124
+ }
125
+ return { success: true, messageId: result.messageId };
126
+ }
127
+ function enqueueMessage(msg) {
128
+ if (isCancelMessage(msg.content)) {
129
+ const controller = activeRuns.get(msg.chatId);
130
+ if (controller) {
131
+ controller.abort();
132
+ activeRuns.delete(msg.chatId);
133
+ const provider = deps.registry.getProviderForChat(msg.chatId);
134
+ void provider.sendMessage(msg.chatId, 'Canceled.', { threadId: msg.threadId });
135
+ return;
136
+ }
137
+ const provider = deps.registry.getProviderForChat(msg.chatId);
138
+ void provider.sendMessage(msg.chatId, "Nothing's running right now.", { threadId: msg.threadId });
139
+ return;
140
+ }
141
+ if (INTERRUPT_ON_NEW_MESSAGE) {
142
+ const controller = activeRuns.get(msg.chatId);
143
+ if (controller) {
144
+ logger.info({ chatId: msg.chatId }, 'Interrupting active run for new message');
145
+ controller.abort('interrupted');
146
+ activeRuns.delete(msg.chatId);
147
+ }
148
+ }
149
+ enqueueMessageItem({
150
+ chat_jid: msg.chatId,
151
+ message_id: msg.messageId,
152
+ sender_id: msg.senderId,
153
+ sender_name: msg.senderName,
154
+ content: msg.content,
155
+ timestamp: msg.timestamp,
156
+ is_group: msg.isGroup,
157
+ chat_type: msg.chatType,
158
+ message_thread_id: msg.threadId ? Number(msg.threadId) : undefined,
159
+ });
160
+ setMessageQueueDepth(getPendingMessageCount());
161
+ if (!activeDrains.has(msg.chatId)) {
162
+ void drainQueue(msg.chatId);
163
+ }
164
+ }
165
+ async function drainQueue(chatId) {
166
+ if (activeDrains.has(chatId))
167
+ return;
168
+ activeDrains.add(chatId);
169
+ setMessageQueueDepth(getPendingMessageCount());
170
+ let reschedule = false;
171
+ try {
172
+ let iterations = 0;
173
+ while (iterations < MAX_DRAIN_ITERATIONS) {
174
+ const batch = claimBatchForChat(chatId, BATCH_WINDOW_MS, MAX_BATCH_SIZE);
175
+ if (batch.length === 0)
176
+ break;
177
+ iterations++;
178
+ const last = batch[batch.length - 1];
179
+ const triggerMsg = {
180
+ chatId: last.chat_jid,
181
+ messageId: last.message_id,
182
+ senderId: last.sender_id,
183
+ senderName: last.sender_name,
184
+ content: last.content,
185
+ timestamp: last.timestamp,
186
+ isGroup: last.is_group === 1,
187
+ chatType: last.chat_type,
188
+ threadId: last.message_thread_id != null ? String(last.message_thread_id) : undefined,
189
+ };
190
+ const batchIds = batch.map(b => b.id);
191
+ try {
192
+ await processMessage(triggerMsg);
193
+ completeQueuedMessages(batchIds);
194
+ }
195
+ catch (err) {
196
+ const errMsg = err instanceof Error ? err.message : String(err);
197
+ const attempt = Math.max(1, ...batch.map(row => {
198
+ const previousAttempts = Number.isFinite(row.attempt_count) ? Number(row.attempt_count) : 0;
199
+ return previousAttempts + 1;
200
+ }));
201
+ const isRetryable = err instanceof RetryableMessageProcessingError;
202
+ if (isRetryable && attempt < MESSAGE_QUEUE_MAX_RETRIES) {
203
+ requeueQueuedMessages(batchIds, errMsg);
204
+ const delayMs = computeMessageQueueRetryDelayMs(attempt);
205
+ logger.warn({
206
+ chatId,
207
+ attempt,
208
+ maxRetries: MESSAGE_QUEUE_MAX_RETRIES,
209
+ delayMs,
210
+ error: errMsg
211
+ }, 'Retryable batch failure; re-queued for retry');
212
+ await sleep(delayMs);
213
+ continue;
214
+ }
215
+ failQueuedMessages(batchIds, errMsg);
216
+ logger.error({ chatId, attempt, err }, 'Error processing message batch');
217
+ }
218
+ }
219
+ if (iterations >= MAX_DRAIN_ITERATIONS) {
220
+ reschedule = true;
221
+ logger.warn({ chatId, iterations }, 'Drain loop hit iteration limit; re-scheduling');
222
+ setTimeout(() => {
223
+ activeDrains.delete(chatId);
224
+ void drainQueue(chatId);
225
+ }, 1000);
226
+ }
227
+ }
228
+ finally {
229
+ if (!reschedule) {
230
+ activeDrains.delete(chatId);
231
+ }
232
+ setMessageQueueDepth(getPendingMessageCount());
233
+ }
234
+ }
235
+ async function processMessage(msg) {
236
+ const registeredGroups = deps.registeredGroups();
237
+ const sessions = deps.sessions();
238
+ const group = registeredGroups[msg.chatId];
239
+ if (!group) {
240
+ logger.debug({ chatId: msg.chatId }, 'Message from unregistered chat');
241
+ return false;
242
+ }
243
+ const providerName = ProviderRegistryClass.getPrefix(msg.chatId);
244
+ recordMessage(providerName);
245
+ setLastMessageTime(msg.timestamp);
246
+ const chatState = getChatState(msg.chatId);
247
+ let missedMessages = getMessagesSinceCursor(msg.chatId, chatState?.last_agent_timestamp || null, chatState?.last_agent_message_id || null);
248
+ const triggerMessageId = Number.parseInt(msg.messageId, 10);
249
+ missedMessages = missedMessages.filter((message) => {
250
+ if (message.timestamp < msg.timestamp)
251
+ return true;
252
+ if (message.timestamp !== msg.timestamp)
253
+ return false;
254
+ const numericId = Number.parseInt(message.id, 10);
255
+ if (Number.isFinite(triggerMessageId) && Number.isFinite(numericId)) {
256
+ return numericId <= triggerMessageId;
257
+ }
258
+ return message.id <= msg.messageId;
259
+ });
260
+ if (missedMessages.length === 0) {
261
+ logger.warn({ chatId: msg.chatId }, 'No missed messages found; falling back to current message');
262
+ const fallbackAttachments = msg.attachments && msg.attachments.length > 0
263
+ ? JSON.stringify(msg.attachments)
264
+ : null;
265
+ missedMessages = [{
266
+ id: msg.messageId,
267
+ chat_jid: msg.chatId,
268
+ sender: msg.senderId,
269
+ sender_name: msg.senderName,
270
+ content: msg.content,
271
+ timestamp: msg.timestamp,
272
+ attachments_json: fallbackAttachments
273
+ }];
274
+ }
275
+ const inputMaxChars = deps.registry.getProviderForChat(msg.chatId).capabilities.maxMessageLength;
276
+ const lines = missedMessages.map(m => {
277
+ const escapeXml = (s) => s
278
+ .replace(/&/g, '&amp;')
279
+ .replace(/</g, '&lt;')
280
+ .replace(/>/g, '&gt;')
281
+ .replace(/"/g, '&quot;');
282
+ const safeContent = clampInputMessage(m.content, inputMaxChars);
283
+ let attachments = [];
284
+ if (m.attachments_json) {
285
+ try {
286
+ attachments = JSON.parse(m.attachments_json);
287
+ }
288
+ catch { /* ignore */ }
289
+ }
290
+ const attachmentXml = buildAttachmentsXml(attachments, group.folder);
291
+ const inner = attachmentXml
292
+ ? `${escapeXml(safeContent)}\n${attachmentXml}`
293
+ : escapeXml(safeContent);
294
+ return `<message sender="${escapeXml(m.sender_name)}" sender_id="${escapeXml(m.sender)}" time="${m.timestamp}">${inner}</message>`;
295
+ });
296
+ const prompt = `<messages>\n${lines.join('\n')}\n</messages>`;
297
+ const replyToMessageId = msg.messageId;
298
+ const containerAttachments = (() => {
299
+ for (let idx = missedMessages.length - 1; idx >= 0; idx -= 1) {
300
+ const raw = missedMessages[idx].attachments_json;
301
+ if (!raw)
302
+ continue;
303
+ try {
304
+ const parsed = JSON.parse(raw);
305
+ if (!Array.isArray(parsed) || parsed.length === 0)
306
+ continue;
307
+ const mapped = parsed.flatMap(attachment => {
308
+ if (!attachment?.local_path)
309
+ return [];
310
+ const containerPath = hostPathToContainerPath(attachment.local_path, group.folder);
311
+ if (!containerPath)
312
+ return [];
313
+ return [{
314
+ type: attachment.type,
315
+ path: containerPath,
316
+ file_name: attachment.file_name,
317
+ mime_type: attachment.mime_type,
318
+ file_size: attachment.file_size,
319
+ duration: attachment.duration,
320
+ width: attachment.width,
321
+ height: attachment.height
322
+ }];
323
+ });
324
+ if (mapped.length > 0)
325
+ return mapped;
326
+ }
327
+ catch {
328
+ // ignore malformed attachment payloads
329
+ }
330
+ }
331
+ return undefined;
332
+ })();
333
+ // Single routing decision — no probes, no profiles
334
+ const routingStartedAt = Date.now();
335
+ const routing = routeRequest();
336
+ const routerMs = Date.now() - routingStartedAt;
337
+ recordStageLatency('router', routerMs, providerName);
338
+ logger.info({
339
+ chatId: msg.chatId,
340
+ model: routing.model,
341
+ }, 'Routing decision');
342
+ const traceBase = createTraceBase({
343
+ chatId: msg.chatId,
344
+ groupFolder: group.folder,
345
+ userId: msg.senderId,
346
+ inputText: prompt,
347
+ source: 'dotclaw'
348
+ });
349
+ logger.info({ group: group.name, messageCount: missedMessages.length }, 'Processing message');
350
+ void emitHook('message:processing', {
351
+ chat_id: msg.chatId,
352
+ message_id: msg.messageId,
353
+ sender_id: msg.senderId,
354
+ group_folder: group.folder,
355
+ message_count: missedMessages.length
356
+ });
357
+ const provider = deps.registry.getProviderForChat(msg.chatId);
358
+ await provider.setTyping(msg.chatId);
359
+ const recallQuery = missedMessages.map(entry => entry.content).join('\n');
360
+ let output = null;
361
+ let context = null;
362
+ let errorMessage = null;
363
+ // Set up streaming delivery
364
+ const streamingConfig = runtime.host.streaming;
365
+ const streaming = streamingConfig.enabled
366
+ ? new StreamingDelivery(provider, msg.chatId, streamingConfig, {
367
+ threadId: msg.threadId,
368
+ replyToMessageId,
369
+ })
370
+ : null;
371
+ // Refresh typing indicator (cleared on first stream chunk or completion)
372
+ const typingInterval = setInterval(() => { void provider.setTyping(msg.chatId); }, 4_000);
373
+ const abortController = new AbortController();
374
+ activeRuns.set(msg.chatId, abortController);
375
+ // Prepare stream directory for IPC-based streaming
376
+ let streamDir;
377
+ if (streamingConfig.enabled) {
378
+ const { DATA_DIR } = await import('./config.js');
379
+ streamDir = path.join(DATA_DIR, 'ipc', group.folder, 'stream', traceBase.trace_id);
380
+ fs.mkdirSync(streamDir, { recursive: true });
381
+ }
382
+ try {
383
+ // Launch agent run (single call — no probes, no retries)
384
+ const executionPromise = executeAgentRun({
385
+ group,
386
+ prompt,
387
+ chatJid: msg.chatId,
388
+ userId: msg.senderId,
389
+ userName: msg.senderName,
390
+ recallQuery: recallQuery || msg.content,
391
+ recallMaxResults: routing.recallMaxResults,
392
+ recallMaxTokens: routing.recallMaxTokens,
393
+ sessionId: sessions[group.folder],
394
+ onSessionUpdate: (sessionId) => { deps.setSession(group.folder, sessionId); },
395
+ availableGroups: deps.buildAvailableGroupsSnapshot(),
396
+ modelOverride: routing.model,
397
+ modelFallbacks: routing.fallbacks,
398
+ reasoningEffort: loadRuntimeConfig().agent.reasoning.effort,
399
+ modelMaxOutputTokens: routing.maxOutputTokens,
400
+ maxToolSteps: routing.maxToolSteps,
401
+ attachments: containerAttachments,
402
+ abortSignal: abortController.signal,
403
+ streamDir,
404
+ });
405
+ // Concurrently watch stream chunks and deliver in real-time
406
+ if (streaming && streamDir) {
407
+ const chunkWatcher = (async () => {
408
+ try {
409
+ let firstChunk = true;
410
+ for await (const chunk of watchStreamChunks(streamDir, abortController.signal)) {
411
+ if (firstChunk) {
412
+ clearInterval(typingInterval);
413
+ firstChunk = false;
414
+ }
415
+ await streaming.onChunk(chunk);
416
+ }
417
+ }
418
+ catch (err) {
419
+ // Stream watching errors are non-fatal
420
+ if (!(err instanceof Error && err.name === 'AbortError')) {
421
+ logger.debug({ chatId: msg.chatId, err }, 'Stream chunk watcher error');
422
+ }
423
+ }
424
+ })();
425
+ // Wait for agent to complete
426
+ const execution = await executionPromise;
427
+ output = execution.output;
428
+ context = execution.context;
429
+ // Wait briefly for any remaining chunks
430
+ await Promise.race([chunkWatcher, sleep(500)]);
431
+ }
432
+ else {
433
+ const execution = await executionPromise;
434
+ output = execution.output;
435
+ context = execution.context;
436
+ }
437
+ if (output.status === 'error') {
438
+ errorMessage = output.error || 'Unknown error';
439
+ }
440
+ }
441
+ catch (err) {
442
+ // Check if run was interrupted by a new message
443
+ if (abortController.signal.aborted && abortController.signal.reason === 'interrupted') {
444
+ logger.debug({ chatId: msg.chatId }, 'Run interrupted by new message');
445
+ if (streaming) {
446
+ try {
447
+ await streaming.cleanup();
448
+ }
449
+ catch { /* best effort */ }
450
+ }
451
+ clearInterval(typingInterval);
452
+ activeRuns.delete(msg.chatId);
453
+ if (streamDir) {
454
+ try {
455
+ fs.rmSync(streamDir, { recursive: true, force: true });
456
+ }
457
+ catch { /* ignore */ }
458
+ }
459
+ return false;
460
+ }
461
+ if (err instanceof AgentExecutionError) {
462
+ context = err.context;
463
+ errorMessage = err.message;
464
+ }
465
+ else {
466
+ errorMessage = err instanceof Error ? err.message : String(err);
467
+ }
468
+ logger.error({ group: group.name, err }, 'Agent error');
469
+ }
470
+ finally {
471
+ clearInterval(typingInterval);
472
+ activeRuns.delete(msg.chatId);
473
+ // Clean up stream directory
474
+ if (streamDir) {
475
+ try {
476
+ fs.rmSync(streamDir, { recursive: true, force: true });
477
+ }
478
+ catch { /* ignore */ }
479
+ }
480
+ }
481
+ const extraTimings = {};
482
+ extraTimings.router_ms = routerMs;
483
+ if (!output) {
484
+ const message = errorMessage || 'No output from agent';
485
+ if (context) {
486
+ recordAgentTelemetry({
487
+ traceBase,
488
+ output: null,
489
+ context,
490
+ metricsSource: providerName,
491
+ toolAuditSource: 'message',
492
+ errorMessage: message,
493
+ errorType: 'agent',
494
+ extraTimings
495
+ });
496
+ }
497
+ else {
498
+ recordError('agent');
499
+ const { writeTrace } = await import('./trace-writer.js');
500
+ writeTrace({
501
+ trace_id: traceBase.trace_id,
502
+ timestamp: traceBase.timestamp,
503
+ created_at: traceBase.created_at,
504
+ chat_id: traceBase.chat_id,
505
+ group_folder: traceBase.group_folder,
506
+ user_id: traceBase.user_id,
507
+ input_text: traceBase.input_text,
508
+ output_text: null,
509
+ model_id: 'unknown',
510
+ memory_recall: [],
511
+ error_code: message,
512
+ source: traceBase.source
513
+ });
514
+ }
515
+ const userMessage = humanizeError(errorMessage || 'Unknown error');
516
+ // Finalize streaming or send error
517
+ if (streaming) {
518
+ await streaming.finalize(userMessage);
519
+ }
520
+ else {
521
+ await sendMessageForQueue(msg.chatId, userMessage, { threadId: msg.threadId, replyToMessageId });
522
+ }
523
+ return false;
524
+ }
525
+ if (output.status === 'error') {
526
+ if (context) {
527
+ recordAgentTelemetry({
528
+ traceBase,
529
+ output,
530
+ context,
531
+ metricsSource: providerName,
532
+ toolAuditSource: 'message',
533
+ errorMessage: errorMessage || output.error || 'Unknown error',
534
+ errorType: 'agent',
535
+ extraTimings
536
+ });
537
+ }
538
+ logger.error({ group: group.name, error: output.error }, 'Container agent error');
539
+ const errorText = errorMessage || output.error || 'Unknown error';
540
+ const userMessage = humanizeError(errorText);
541
+ if (streaming) {
542
+ await streaming.finalize(userMessage);
543
+ }
544
+ else {
545
+ await sendMessageForQueue(msg.chatId, userMessage, { threadId: msg.threadId, replyToMessageId });
546
+ }
547
+ return false;
548
+ }
549
+ updateChatState(msg.chatId, msg.timestamp, msg.messageId);
550
+ if (output.result && output.result.trim()) {
551
+ const hasVoiceAttachment = missedMessages.some(m => {
552
+ if (!m.attachments_json)
553
+ return false;
554
+ try {
555
+ const atts = JSON.parse(m.attachments_json);
556
+ return atts.some(a => a.type === 'voice');
557
+ }
558
+ catch {
559
+ return false;
560
+ }
561
+ });
562
+ if (hasVoiceAttachment) {
563
+ const inboxDir = path.join(GROUPS_DIR, group.folder, 'inbox');
564
+ const voicePath = await synthesizeSpeechHost(output.result, inboxDir);
565
+ if (voicePath) {
566
+ try {
567
+ await provider.sendVoice(msg.chatId, voicePath);
568
+ fs.unlinkSync(voicePath);
569
+ }
570
+ catch (err) {
571
+ logger.warn({ error: err instanceof Error ? err.message : String(err) }, 'Failed to send TTS voice reply');
572
+ if (streaming) {
573
+ await streaming.finalize(output.result);
574
+ }
575
+ else {
576
+ await sendMessageForQueue(msg.chatId, output.result, { threadId: msg.threadId, replyToMessageId });
577
+ }
578
+ }
579
+ }
580
+ else {
581
+ if (streaming) {
582
+ await streaming.finalize(output.result);
583
+ }
584
+ else {
585
+ await sendMessageForQueue(msg.chatId, output.result, { threadId: msg.threadId, replyToMessageId });
586
+ }
587
+ }
588
+ }
589
+ else {
590
+ // Finalize streaming with the complete text, or send normally
591
+ if (streaming) {
592
+ const sentMessageId = await streaming.finalize(output.result);
593
+ if (sentMessageId) {
594
+ try {
595
+ linkMessageToTrace(sentMessageId, msg.chatId, traceBase.trace_id);
596
+ }
597
+ catch {
598
+ // Don't fail if linking fails
599
+ }
600
+ }
601
+ }
602
+ else {
603
+ const sendResult = await sendMessageForQueue(msg.chatId, output.result, { threadId: msg.threadId, replyToMessageId });
604
+ const sentMessageId = sendResult.messageId;
605
+ if (sentMessageId) {
606
+ try {
607
+ linkMessageToTrace(sentMessageId, msg.chatId, traceBase.trace_id);
608
+ }
609
+ catch {
610
+ // Don't fail if linking fails
611
+ }
612
+ }
613
+ }
614
+ }
615
+ if (output.stdoutTruncated) {
616
+ await sendMessageForQueue(msg.chatId, 'That response was cut short because it was too large. Ask me to continue or try a smaller request.', { threadId: msg.threadId });
617
+ }
618
+ }
619
+ else if (output.tool_calls && output.tool_calls.length > 0) {
620
+ await sendMessageForQueue(msg.chatId, "I ran out of steps before I could finish. Try narrowing the scope or asking for a specific part.", { threadId: msg.threadId, replyToMessageId });
621
+ }
622
+ else {
623
+ logger.warn({ chatId: msg.chatId }, 'Agent returned empty/whitespace response');
624
+ await sendMessageForQueue(msg.chatId, "I wasn't able to come up with a response. Could you try rephrasing?", { threadId: msg.threadId, replyToMessageId });
625
+ }
626
+ if (context) {
627
+ recordAgentTelemetry({
628
+ traceBase,
629
+ output,
630
+ context,
631
+ metricsSource: providerName,
632
+ toolAuditSource: 'message',
633
+ extraTimings
634
+ });
635
+ }
636
+ void emitHook('message:responded', {
637
+ chat_id: msg.chatId,
638
+ message_id: msg.messageId,
639
+ group_folder: group.folder,
640
+ has_result: !!output.result?.trim(),
641
+ model: context?.resolvedModel?.model || 'unknown'
642
+ });
643
+ return true;
644
+ }
645
+ return {
646
+ enqueueMessage,
647
+ drainQueue,
648
+ processMessage,
649
+ sendMessageForQueue,
650
+ };
651
+ }
652
+ //# sourceMappingURL=message-pipeline.js.map