neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +12 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -1,3 +1,5 @@
1
+ 'use strict';
2
+
1
3
  const EventEmitter = require('events');
2
4
  const db = require('../../db/database');
3
5
  const fs = require('fs');
@@ -36,8 +38,30 @@ const {
36
38
  } = require('./access_policy');
37
39
  const { decryptValue, encryptValue } = require('../integrations/secrets');
38
40
  const { readMeshtasticEnabled } = require('./meshtastic_env');
41
+ const {
42
+ createLinkedAbortController,
43
+ isAbortError,
44
+ throwIfAborted,
45
+ } = require('../../utils/abort');
46
+ const { waitForAbortableResult, waitForBoundedResult } = require('../network/http');
47
+ const {
48
+ claimInboundJob,
49
+ enqueueInboundMessage,
50
+ listPendingInboundJobs,
51
+ payloadForInboundJob,
52
+ reconcileInterruptedInboundJobs,
53
+ settleInboundJob,
54
+ } = require('./inbound_store');
39
55
 
40
56
  const LEGACY_WHATSAPP_AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
57
+ const MESSAGING_OPERATION_TIMEOUT_MS = 60000;
58
+
59
+ function messagingShutdownError() {
60
+ const error = new Error('Messaging is shutting down and cannot accept new work.');
61
+ error.name = 'AbortError';
62
+ error.code = 'MESSAGING_SHUTTING_DOWN';
63
+ return error;
64
+ }
41
65
 
42
66
  class IrcMessagingPlatform extends IrcPlatform {
43
67
  constructor(config = {}) { super('irc', config); }
@@ -64,6 +88,12 @@ class MessagingManager extends EventEmitter {
64
88
  this.accessSuggestions = new Map();
65
89
  this.messageHandlers = [];
66
90
  this.isShuttingDown = false;
91
+ this.shutdownPromise = null;
92
+ this.lifecycleAbortController = new AbortController();
93
+ this.activeOperations = new Set();
94
+ this.activeInboundJobs = new Set();
95
+ this.activeInboundRecoveries = new Map();
96
+ this.inboundJobsReconciled = false;
67
97
  this.platformTypes = {
68
98
  whatsapp: WhatsAppPlatform,
69
99
  telnyx: TelnyxVoicePlatform,
@@ -94,9 +124,48 @@ class MessagingManager extends EventEmitter {
94
124
  }
95
125
 
96
126
  registerHandler(handler) {
127
+ if (this.isShuttingDown) return false;
97
128
  if (!this.messageHandlers.includes(handler)) {
98
129
  this.messageHandlers.push(handler);
99
130
  }
131
+ return true;
132
+ }
133
+
134
+ _assertRunning() {
135
+ if (this.isShuttingDown || this.lifecycleAbortController.signal.aborted) {
136
+ throw messagingShutdownError();
137
+ }
138
+ }
139
+
140
+ async _runOperation(options, serviceName, operation, timeoutMs = MESSAGING_OPERATION_TIMEOUT_MS) {
141
+ this._assertRunning();
142
+ const timeoutController = new AbortController();
143
+ const linked = createLinkedAbortController([
144
+ options?.signal,
145
+ this.lifecycleAbortController.signal,
146
+ timeoutController.signal,
147
+ ]);
148
+ throwIfAborted(linked.signal, `${serviceName} aborted.`);
149
+ const timer = setTimeout(() => {
150
+ const error = new Error(`${serviceName} timed out after ${timeoutMs}ms.`);
151
+ error.code = 'MESSAGING_TIMEOUT';
152
+ timeoutController.abort(error);
153
+ }, timeoutMs);
154
+
155
+ const operationPromise = waitForAbortableResult(
156
+ Promise.resolve().then(() => operation(linked.signal)),
157
+ linked.signal,
158
+ `${serviceName} aborted.`,
159
+ );
160
+ this.activeOperations.add(operationPromise);
161
+ const cleanupOperation = () => this.activeOperations.delete(operationPromise);
162
+ operationPromise.then(cleanupOperation, cleanupOperation);
163
+ try {
164
+ return await operationPromise;
165
+ } finally {
166
+ clearTimeout(timer);
167
+ linked.cleanup();
168
+ }
100
169
  }
101
170
 
102
171
  async ingestMessage(userId, platformName, msg, options = {}) {
@@ -118,54 +187,188 @@ class MessagingManager extends EventEmitter {
118
187
  senderUsername: msg.senderUsername,
119
188
  senderTag: msg.senderTag,
120
189
  isGroup: msg.isGroup,
190
+ wasMentioned: msg.wasMentioned === true,
191
+ repliedToAgent: msg.repliedToAgent === true,
192
+ groupId: msg.groupId || null,
193
+ channelId: msg.channelId || null,
194
+ serverId: msg.serverId || msg.guildId || null,
195
+ roomId: msg.roomId || null,
196
+ roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
121
197
  mediaType: msg.mediaType,
198
+ localMediaPath: msg.localMediaPath || null,
122
199
  ...(msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : {}),
123
200
  };
124
- // Deduplicate against platform_msg_id — webhook retries (at-least-once delivery)
125
- // would otherwise trigger a second agent run for the same user message.
126
- if (msg.messageId) {
127
- const already = db.prepare(
128
- "SELECT id FROM messages WHERE user_id = ? AND platform = ? AND platform_msg_id = ? AND role = 'user' LIMIT 1"
129
- ).get(userId, platformName, msg.messageId);
130
- if (already) {
131
- console.warn(`[Messaging] Duplicate platform_msg_id ${msg.messageId} on ${platformName} for user ${userId} — skipping handlers`);
132
- return { ...msg, agentId, platform: platformName };
133
- }
201
+ const enrichedMsg = {
202
+ agentId,
203
+ platform: platformName,
204
+ chatId: msg.chatId,
205
+ messageId: msg.messageId || null,
206
+ sender: msg.sender,
207
+ senderName: msg.senderName || null,
208
+ senderDisplayName: msg.senderDisplayName || null,
209
+ senderUsername: msg.senderUsername || null,
210
+ senderTag: msg.senderTag || null,
211
+ wasMentioned: msg.wasMentioned === true,
212
+ repliedToAgent: msg.repliedToAgent === true,
213
+ content: normalizedIncomingContent,
214
+ mediaType: msg.mediaType || null,
215
+ localMediaPath: msg.localMediaPath || null,
216
+ isGroup: msg.isGroup === true,
217
+ timestamp: msg.timestamp || new Date().toISOString(),
218
+ channelContext: Array.isArray(msg.channelContext) ? msg.channelContext.slice(-20) : null,
219
+ channelName: msg.channelName || null,
220
+ groupName: msg.groupName || null,
221
+ guildName: msg.guildName || null,
222
+ roomName: msg.roomName || null,
223
+ groupId: msg.groupId || null,
224
+ channelId: msg.channelId || null,
225
+ serverId: msg.serverId || msg.guildId || null,
226
+ guildId: msg.guildId || msg.serverId || null,
227
+ roomId: msg.roomId || null,
228
+ roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
229
+ replyToMessageId: msg.replyToMessageId || null,
230
+ threadId: msg.threadId || msg.threadTs || null,
231
+ eventType: msg.eventType || 'message',
232
+ metadata: msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : null,
233
+ };
234
+ const queued = enqueueInboundMessage({
235
+ userId,
236
+ agentId,
237
+ platform: platformName,
238
+ platformMessageId: msg.messageId || null,
239
+ chatId: msg.chatId,
240
+ content: normalizedIncomingContent,
241
+ metadata,
242
+ createdAt: enrichedMsg.timestamp,
243
+ payload: enrichedMsg,
244
+ });
245
+ const durableMessage = queued.payload || payloadForInboundJob(queued.job) || enrichedMsg;
246
+
247
+ if (this.isShuttingDown) {
248
+ return durableMessage;
134
249
  }
135
250
 
136
- db.prepare('INSERT INTO messages (user_id, agent_id, role, content, platform, platform_msg_id, platform_chat_id, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
137
- .run(
138
- userId,
139
- agentId,
140
- 'user',
141
- normalizedIncomingContent,
142
- platformName,
143
- msg.messageId,
144
- msg.chatId,
145
- JSON.stringify(metadata),
146
- msg.timestamp,
251
+ if (queued.created) {
252
+ this.io.to(`user:${userId}`).emit('messaging:message', durableMessage);
253
+ } else if (!queued.job) {
254
+ console.warn(
255
+ `[Messaging] Duplicate ${platformName} message for user ${userId} predates durable processing state; skipping replay`,
147
256
  );
148
-
149
- const enrichedMsg = { ...msg, content: normalizedIncomingContent, agentId, platform: platformName };
150
-
151
- if (this.isShuttingDown) {
152
- return enrichedMsg;
257
+ return durableMessage;
258
+ } else if (queued.job.status !== 'pending') {
259
+ console.warn(
260
+ `[Messaging] Duplicate ${platformName} message for user ${userId} has durable status ${queued.job.status}; skipping replay`,
261
+ );
262
+ return durableMessage;
153
263
  }
154
264
 
155
- this.io.to(`user:${userId}`).emit('messaging:message', enrichedMsg);
265
+ await this._processInboundJob(queued.job, durableMessage);
266
+ return durableMessage;
267
+ }
268
+
269
+ async _processInboundJob(job, payload) {
270
+ if (this.isShuttingDown || !job || this.messageHandlers.length === 0) return false;
271
+ if (!claimInboundJob(job.id)) return false;
272
+ let finishTracking;
273
+ const tracked = new Promise((resolve) => {
274
+ finishTracking = resolve;
275
+ });
276
+ this.activeInboundJobs.add(tracked);
156
277
 
157
- for (const handler of this.messageHandlers) {
158
- if (this.isShuttingDown) {
159
- break;
278
+ let status = 'completed';
279
+ let failure = null;
280
+ let runId = null;
281
+ try {
282
+ for (const handler of this.messageHandlers) {
283
+ if (this.isShuttingDown) {
284
+ status = 'pending';
285
+ break;
286
+ }
287
+ let handlerResult = await handler(job.user_id, payload);
288
+ if (handlerResult?.completion) handlerResult = await handlerResult.completion;
289
+ const outcome = handlerResult?.outcome || handlerResult || {};
290
+ runId = outcome.runId || runId;
291
+ if (outcome.cancelled === true) {
292
+ status = runId ? 'failed' : 'pending';
293
+ failure = runId
294
+ ? 'The server stopped after this inbound agent run began; it will not be replayed automatically.'
295
+ : null;
296
+ break;
297
+ }
298
+ if (outcome.error) {
299
+ status = 'failed';
300
+ failure = outcome.error;
301
+ break;
302
+ }
160
303
  }
161
- try {
162
- await handler(userId, enrichedMsg);
163
- } catch (err) {
164
- console.error('Message handler error:', err.message);
304
+ } catch (error) {
305
+ const cancelled = this.isShuttingDown
306
+ || isAbortError(error, this.lifecycleAbortController.signal);
307
+ status = cancelled && !runId ? 'pending' : 'failed';
308
+ failure = status === 'failed' ? error : null;
309
+ if (!cancelled) {
310
+ console.error('[Messaging] Inbound message handler failed:', error?.message || error);
165
311
  }
166
312
  }
167
313
 
168
- return enrichedMsg;
314
+ try {
315
+ settleInboundJob(job.id, status, failure?.message || failure || null);
316
+ return status === 'completed';
317
+ } finally {
318
+ finishTracking();
319
+ this.activeInboundJobs.delete(tracked);
320
+ }
321
+ }
322
+
323
+ _platformReadyForInboundJob(job) {
324
+ const platform = this.platforms.get(this._key(job.user_id, job.agent_id, job.platform));
325
+ if (!platform) return false;
326
+ try {
327
+ return String(platform.getStatus?.() || platform.status || '').toLowerCase() === 'connected';
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ recoverPendingInbound(filters = {}) {
334
+ if (this.isShuttingDown || this.messageHandlers.length === 0) {
335
+ return Promise.resolve({ recovered: 0, skipped: 0 });
336
+ }
337
+ const key = JSON.stringify({
338
+ userId: filters.userId,
339
+ agentId: filters.agentId,
340
+ platform: filters.platform,
341
+ });
342
+ if (this.activeInboundRecoveries.has(key)) return this.activeInboundRecoveries.get(key);
343
+
344
+ const recovery = Promise.resolve().then(async () => {
345
+ if (!this.inboundJobsReconciled) {
346
+ reconcileInterruptedInboundJobs();
347
+ this.inboundJobsReconciled = true;
348
+ }
349
+ let recovered = 0;
350
+ let skipped = 0;
351
+ for (const job of listPendingInboundJobs(filters)) {
352
+ if (this.isShuttingDown) break;
353
+ if (!this._platformReadyForInboundJob(job)) {
354
+ skipped += 1;
355
+ continue;
356
+ }
357
+ const payload = payloadForInboundJob(job);
358
+ if (!payload) {
359
+ settleInboundJob(job.id, 'failed', 'Stored inbound message payload is invalid.');
360
+ continue;
361
+ }
362
+ if (await this._processInboundJob(job, payload)) recovered += 1;
363
+ }
364
+ return { recovered, skipped };
365
+ }).finally(() => {
366
+ if (this.activeInboundRecoveries.get(key) === recovery) {
367
+ this.activeInboundRecoveries.delete(key);
368
+ }
369
+ });
370
+ this.activeInboundRecoveries.set(key, recovery);
371
+ return recovery;
169
372
  }
170
373
 
171
374
  _agentId(userId, options = {}) {
@@ -315,6 +518,8 @@ class MessagingManager extends EventEmitter {
315
518
  }
316
519
 
317
520
  async connectPlatform(userId, platformName, config = {}, options = {}) {
521
+ this._assertRunning();
522
+ throwIfAborted(options.signal, 'Messaging platform connection aborted.');
318
523
  const agentId = this._agentId(userId, options);
319
524
  config = { ...(config || {}) };
320
525
  config.userId = userId;
@@ -410,6 +615,16 @@ class MessagingManager extends EventEmitter {
410
615
  this.io.to(`user:${userId}`).emit('messaging:connected', { agentId, platform: platformName });
411
616
  db.prepare('UPDATE platform_connections SET status = ?, last_connected = datetime(\'now\') WHERE user_id = ? AND agent_id = ? AND platform = ?')
412
617
  .run('connected', userId, agentId, platformName);
618
+ this.emit('platform_connected', { userId, agentId, platform: platformName });
619
+ void this.recoverPendingInbound({
620
+ userId,
621
+ agentId,
622
+ platform: platformName,
623
+ }).catch((error) => {
624
+ if (!this.isShuttingDown) {
625
+ console.error('[Messaging] Inbound recovery failed:', error?.message || error);
626
+ }
627
+ });
413
628
  });
414
629
 
415
630
  platform.on('disconnected', (info) => {
@@ -455,9 +670,13 @@ class MessagingManager extends EventEmitter {
455
670
  });
456
671
  });
457
672
 
458
- platform.on('message', async (msg) => {
673
+ platform.on('message', (msg) => {
459
674
  if (this.isShuttingDown) return;
460
- await this.ingestMessage(userId, platformName, msg, { agentId });
675
+ void this.ingestMessage(userId, platformName, msg, { agentId }).catch((error) => {
676
+ if (!this.isShuttingDown) {
677
+ console.error('[Messaging] Failed to persist or dispatch inbound message:', error?.message || error);
678
+ }
679
+ });
461
680
  });
462
681
 
463
682
  if (!existingConnection) {
@@ -468,7 +687,19 @@ class MessagingManager extends EventEmitter {
468
687
  .run(storedConfig, 'connecting', userId, agentId, platformName);
469
688
  }
470
689
 
471
- await platform.connect();
690
+ try {
691
+ await this._runOperation(
692
+ options,
693
+ `${platformName} connection`,
694
+ () => platform.connect(),
695
+ );
696
+ } catch (error) {
697
+ if (currentPlatform()) {
698
+ this.platforms.delete(key);
699
+ }
700
+ await Promise.resolve(platform.disconnect?.()).catch(() => {});
701
+ throw error;
702
+ }
472
703
  return { status: platform.getStatus() };
473
704
  }
474
705
 
@@ -489,6 +720,7 @@ class MessagingManager extends EventEmitter {
489
720
  }
490
721
 
491
722
  async sendMessage(userId, platformName, to, content, mediaPathOrOptions) {
723
+ this._assertRunning();
492
724
  const agentId = this._agentId(userId, mediaPathOrOptions || {});
493
725
  const key = this._key(userId, agentId, platformName);
494
726
  const platform = this.platforms.get(key);
@@ -505,6 +737,7 @@ class MessagingManager extends EventEmitter {
505
737
  ? sendOptions.metadata
506
738
  : null;
507
739
  const deliveryKind = sendOptions.deliveryKind || 'final';
740
+ throwIfAborted(sendOptions.signal, 'Message delivery aborted.');
508
741
  const normalizedContent = normalizeOutgoingMessageForPlatform(platformName, content, {
509
742
  stripNoResponseMarker: false
510
743
  });
@@ -514,7 +747,13 @@ class MessagingManager extends EventEmitter {
514
747
  return { success: true, suppressed: true };
515
748
  }
516
749
 
517
- const result = await platform.sendMessage(to, normalizedContent, sendOptions);
750
+ const result = await this._runOperation(
751
+ sendOptions,
752
+ `${platformName} message delivery`,
753
+ (signal) => platform.sendMessage(to, normalizedContent, { ...sendOptions, signal }),
754
+ );
755
+ this._assertRunning();
756
+ throwIfAborted(sendOptions.signal, 'Message delivery aborted.');
518
757
  if (result?.success === false) {
519
758
  const reason = result.error || result.reason || 'platform rejected the message';
520
759
  const error = new Error(`Platform ${platformName} delivery failed: ${reason}`);
@@ -676,7 +915,7 @@ class MessagingManager extends EventEmitter {
676
915
  }
677
916
 
678
917
  async restoreConnections() {
679
- this.isShuttingDown = false;
918
+ this._assertRunning();
680
919
  const rows = db.prepare(
681
920
  "SELECT user_id, agent_id, platform, config FROM platform_connections WHERE status IN ('connected', 'awaiting_qr')"
682
921
  ).all();
@@ -715,17 +954,55 @@ class MessagingManager extends EventEmitter {
715
954
  }
716
955
 
717
956
  async shutdown() {
957
+ if (this.shutdownPromise) return this.shutdownPromise;
718
958
  this.isShuttingDown = true;
719
-
720
- const tasks = [];
721
- for (const platform of this.platforms.values()) {
722
- if (typeof platform.disconnect === 'function') {
723
- tasks.push(platform.disconnect().catch(() => {}));
959
+ this.lifecycleAbortController.abort(messagingShutdownError());
960
+
961
+ this.shutdownPromise = (async () => {
962
+ const activeOperationTasks = Array.from(this.activeOperations, (operation) =>
963
+ waitForBoundedResult(operation, {
964
+ serviceName: 'Messaging operation shutdown',
965
+ timeoutMs: 10000,
966
+ }));
967
+ activeOperationTasks.push(...Array.from(this.activeInboundJobs, (job) =>
968
+ waitForBoundedResult(job, {
969
+ serviceName: 'Messaging inbound job shutdown',
970
+ timeoutMs: 10000,
971
+ })));
972
+ activeOperationTasks.push(...Array.from(this.activeInboundRecoveries.values(), (recovery) =>
973
+ waitForBoundedResult(recovery, {
974
+ serviceName: 'Messaging inbound recovery shutdown',
975
+ timeoutMs: 10000,
976
+ })));
977
+ const disconnectTasks = [];
978
+ for (const platform of this.platforms.values()) {
979
+ if (typeof platform.disconnect === 'function') {
980
+ disconnectTasks.push(waitForBoundedResult(
981
+ Promise.resolve().then(() => platform.disconnect()),
982
+ {
983
+ serviceName: `${platform.name || 'Messaging platform'} disconnect`,
984
+ timeoutMs: 10000,
985
+ },
986
+ ));
987
+ }
724
988
  }
725
- }
726
989
 
727
- await Promise.allSettled(tasks);
728
- this.platforms.clear();
990
+ const [operationResults, disconnectResults] = await Promise.all([
991
+ Promise.allSettled(activeOperationTasks),
992
+ Promise.allSettled(disconnectTasks),
993
+ ]);
994
+ this.platforms.clear();
995
+ return {
996
+ state: 'stopped',
997
+ cancelledOperationCount: operationResults.filter(
998
+ (result) => result.status === 'rejected',
999
+ ).length,
1000
+ failedDisconnectCount: disconnectResults.filter(
1001
+ (result) => result.status === 'rejected',
1002
+ ).length,
1003
+ };
1004
+ })();
1005
+ return this.shutdownPromise;
729
1006
  }
730
1007
 
731
1008
  async makeCall(userId, to, greeting, options = {}) {
@@ -733,23 +1010,39 @@ class MessagingManager extends EventEmitter {
733
1010
  const platform = this.platforms.get(key);
734
1011
  if (!platform) throw new Error('Telnyx Voice is not connected');
735
1012
  if (!platform.initiateCall) throw new Error('Telnyx platform does not support outbound calls');
736
- const result = await platform.initiateCall(to, greeting);
1013
+ const result = await this._runOperation(
1014
+ options,
1015
+ 'Telnyx outbound call',
1016
+ (signal) => platform.initiateCall(to, greeting, { ...options, signal }),
1017
+ );
737
1018
  this.io.to(`user:${userId}`).emit('messaging:call_initiated', { platform: 'telnyx', to, callControlId: result.callControlId });
738
1019
  return { success: true, ...result };
739
1020
  }
740
1021
 
741
1022
  async markRead(userId, platformName, chatId, messageId, options = {}) {
1023
+ this._assertRunning();
742
1024
  const key = this._key(userId, this._agentId(userId, options), platformName);
743
1025
  const platform = this.platforms.get(key);
744
1026
  if (!platform?.markRead) return;
745
- return platform.markRead(chatId, messageId);
1027
+ return this._runOperation(
1028
+ options,
1029
+ `${platformName} read receipt`,
1030
+ (signal) => platform.markRead(chatId, messageId, { ...options, signal }),
1031
+ 15000,
1032
+ );
746
1033
  }
747
1034
 
748
1035
  async sendTyping(userId, platformName, chatId, isTyping, options = {}) {
1036
+ this._assertRunning();
749
1037
  const key = this._key(userId, this._agentId(userId, options), platformName);
750
1038
  const platform = this.platforms.get(key);
751
1039
  if (!platform?.sendTyping) return;
752
- return platform.sendTyping(chatId, isTyping);
1040
+ return this._runOperation(
1041
+ options,
1042
+ `${platformName} typing indicator`,
1043
+ (signal) => platform.sendTyping(chatId, isTyping, { ...options, signal }),
1044
+ 15000,
1045
+ );
753
1046
  }
754
1047
 
755
1048
  /**
@@ -223,7 +223,10 @@ class TelegramPlatform extends BasePlatform {
223
223
  ? senderName
224
224
  : `${senderName} in ${msg.chat.title || rawChatId}`;
225
225
 
226
- const channelContext = (!isPrivate && this._isMentioned(msg)) ? this._getContext(rawChatId) : null;
226
+ const channelContext = !isPrivate ? this._getContext(rawChatId) : null;
227
+ const repliedToAgent = !isPrivate
228
+ && msg.reply_to_message?.from?.id
229
+ && String(msg.reply_to_message.from.id) === String(this._botUser?.id || '');
227
230
 
228
231
  this.emit('message', {
229
232
  platform: 'telegram',
@@ -233,6 +236,12 @@ class TelegramPlatform extends BasePlatform {
233
236
  senderDisplayName,
234
237
  senderUsername,
235
238
  senderTag: senderUsername,
239
+ wasMentioned: !isPrivate && this._isMentioned(msg),
240
+ repliedToAgent: Boolean(repliedToAgent),
241
+ replyToMessageId: msg.reply_to_message?.message_id
242
+ ? String(msg.reply_to_message.message_id)
243
+ : null,
244
+ groupId: isPrivate ? null : rawChatId,
236
245
  content,
237
246
  mediaType: null,
238
247
  isGroup: !isPrivate,
@@ -9,6 +9,7 @@ function startTypingKeepalive({
9
9
  runId,
10
10
  platform,
11
11
  chatId,
12
+ signal = null,
12
13
  intervalMs = 4000,
13
14
  onError = null
14
15
  }) {
@@ -65,7 +66,7 @@ function startTypingKeepalive({
65
66
  platform,
66
67
  chatId,
67
68
  isTyping,
68
- { agentId }
69
+ { agentId, signal }
69
70
  );
70
71
  } catch (error) {
71
72
  reportFailure(operation, error);
@@ -97,7 +98,9 @@ function startTypingKeepalive({
97
98
  releaseWait = null;
98
99
  }
99
100
  await loop.catch((error) => reportFailure('typing keepalive loop', error));
100
- await sendTyping(false, 'clear typing indicator');
101
+ if (!signal?.aborted) {
102
+ await sendTyping(false, 'clear typing indicator');
103
+ }
101
104
  })();
102
105
  return stopPromise;
103
106
  };
@@ -54,6 +54,24 @@ class WhatsAppPlatform extends BasePlatform {
54
54
  return [...ownIds].some((id) => text.includes(`@${id}`));
55
55
  }
56
56
 
57
+ _checkMessageAccess(msg, { chatId, isGroup, sender, pushName }) {
58
+ const senderId = normalizeWhatsAppId(sender);
59
+ return this._checkInboundAccess({
60
+ platform: 'whatsapp',
61
+ senderId,
62
+ chatId,
63
+ isDirect: !isGroup,
64
+ isShared: isGroup,
65
+ groupId: isGroup ? chatId : '',
66
+ phoneNumber: senderId,
67
+ wasMentioned: isGroup && this._isGroupAddressedToBot(msg.message || {}),
68
+ }, {
69
+ senderName: pushName || senderId,
70
+ meta: isGroup ? `Group: ${chatId}` : '',
71
+ groupLabel: chatId,
72
+ });
73
+ }
74
+
57
75
  async connect() {
58
76
  this._manualDisconnect = false;
59
77
  if (this._reconnectTimer) {
@@ -154,6 +172,7 @@ class WhatsAppPlatform extends BasePlatform {
154
172
  const isGroup = chatId?.endsWith('@g.us');
155
173
  const sender = isGroup ? msg.key.participant : chatId;
156
174
  const pushName = msg.pushName || '';
175
+ const wasMentioned = isGroup && this._isGroupAddressedToBot(msg.message || {});
157
176
 
158
177
  let content = '';
159
178
  let mediaType = null;
@@ -180,22 +199,12 @@ class WhatsAppPlatform extends BasePlatform {
180
199
  }
181
200
 
182
201
  if (!content && !mediaType) continue;
183
- if (isGroup && !this._isGroupAddressedToBot(msg.message || {})) continue;
184
202
 
185
- const senderId = normalizeWhatsAppId(sender);
186
- const access = this._checkInboundAccess({
187
- platform: 'whatsapp',
188
- senderId,
203
+ const access = this._checkMessageAccess(msg, {
189
204
  chatId,
190
- isDirect: !isGroup,
191
- isShared: isGroup,
192
- groupId: isGroup ? chatId : '',
193
- phoneNumber: senderId,
194
- wasMentioned: isGroup,
195
- }, {
196
- senderName: pushName || senderId,
197
- meta: isGroup ? `Group: ${chatId}` : '',
198
- groupLabel: chatId,
205
+ isGroup,
206
+ sender,
207
+ pushName,
199
208
  });
200
209
 
201
210
  if (!access.allowed) continue;
@@ -250,6 +259,16 @@ class WhatsAppPlatform extends BasePlatform {
250
259
  senderName: pushName,
251
260
  senderDisplayName: pushName || null,
252
261
  senderTag: normalizeWhatsAppId(sender) || sender,
262
+ wasMentioned,
263
+ repliedToAgent: Boolean(
264
+ msg.message?.extendedTextMessage?.contextInfo?.stanzaId
265
+ && msg.message?.extendedTextMessage?.contextInfo?.participant
266
+ && this._ownIds().has(normalizeWhatsAppId(
267
+ msg.message.extendedTextMessage.contextInfo.participant
268
+ ))
269
+ ),
270
+ replyToMessageId: msg.message?.extendedTextMessage?.contextInfo?.stanzaId || null,
271
+ groupId: isGroup ? chatId : null,
253
272
  content,
254
273
  mediaType,
255
274
  localMediaPath,