neoagent 2.4.1-beta.8 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -18,9 +18,12 @@ const {
18
18
  buildVoiceMessagingRunOptions,
19
19
  isVoiceLikeMessage,
20
20
  } = require('../voice/runtime');
21
+ const { getErrorMessage } = require('../bootstrap_helpers');
22
+ const { processInboundQueue } = require('./inbound_queue');
23
+ const { startTypingKeepalive } = require('./typing_keepalive');
21
24
 
22
25
  function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
23
- const userQueues = {};
26
+ const userQueues = Object.create(null);
24
27
  app.locals.userQueues = userQueues;
25
28
 
26
29
  messagingManager.registerHandler(async (userId, msg) => {
@@ -100,62 +103,85 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
100
103
  messagingManager,
101
104
  agentEngine,
102
105
  userId,
103
- msg
106
+ msg,
107
+ onProcessingError: ({ error, runId, failedMessage }) => {
108
+ const errorMessage = getErrorMessage(error);
109
+ console.error(
110
+ `[MessagingAutomation] Agent run failed platform=${failedMessage.platform} user=${userId}:`,
111
+ errorMessage
112
+ );
113
+ io.to(`user:${userId}`).emit('messaging:error', {
114
+ error: `NeoAgent could not finish the ${failedMessage.platform} request. Check the run history for details.`,
115
+ platform: failedMessage.platform,
116
+ chatId: failedMessage.chatId,
117
+ runId
118
+ });
119
+ }
104
120
  });
105
121
  });
106
122
  }
107
123
 
108
124
  async function processQueuedMessage({
109
125
  userQueues,
126
+ messagingManager,
127
+ agentEngine,
128
+ userId,
129
+ msg,
130
+ onProcessingError = null
131
+ }) {
132
+ return processInboundQueue({
133
+ userQueues,
134
+ userId,
135
+ msg,
136
+ executeMessage: (queuedMessage) =>
137
+ executeQueuedMessage({
138
+ messagingManager,
139
+ agentEngine,
140
+ userId,
141
+ msg: queuedMessage
142
+ }),
143
+ onProcessingError
144
+ });
145
+ }
146
+
147
+ async function executeQueuedMessage({
110
148
  messagingManager,
111
149
  agentEngine,
112
150
  userId,
113
151
  msg
114
152
  }) {
115
153
  const agentId = msg.agentId || null;
116
- const queueKey = `${userId}:${agentId || 'main'}`;
117
- if (!userQueues[queueKey]) {
118
- userQueues[queueKey] = { running: false, pending: [], cancelRequested: false };
119
- }
120
- const queue = userQueues[queueKey];
154
+ const runId = randomUUID();
155
+ const reportSideEffectError = (operation, error) => {
156
+ console.warn(
157
+ `[MessagingAutomation] ${operation} failed platform=${msg.platform} user=${userId}:`,
158
+ getErrorMessage(error)
159
+ );
160
+ };
121
161
 
122
- if (queue.cancelRequested && !queue.running) {
123
- queue.pending = [];
124
- queue.cancelRequested = false;
162
+ try {
163
+ await messagingManager.markRead(
164
+ userId,
165
+ msg.platform,
166
+ msg.chatId,
167
+ msg.messageId,
168
+ { agentId }
169
+ );
170
+ } catch (error) {
171
+ reportSideEffectError('mark read', error);
125
172
  }
126
173
 
127
- if (queue.running) {
128
- const last = queue.pending[queue.pending.length - 1];
129
- if (
130
- last
131
- && last.platform === msg.platform
132
- && last.chatId === msg.chatId
133
- && String(last.sender || '') === String(msg.sender || '')
134
- ) {
135
- last.content += `\n${msg.content}`;
136
- last.messageId = msg.messageId;
137
- } else {
138
- queue.pending.push({ ...msg });
139
- }
140
- return;
141
- }
174
+ const stopTypingKeepalive = startTypingKeepalive({
175
+ messagingManager,
176
+ userId,
177
+ agentId,
178
+ runId,
179
+ platform: msg.platform,
180
+ chatId: msg.chatId,
181
+ onError: reportSideEffectError
182
+ });
142
183
 
143
- queue.running = true;
144
- let stopTypingKeepalive = async () => {};
145
184
  try {
146
- const runId = randomUUID();
147
- await messagingManager
148
- .markRead(userId, msg.platform, msg.chatId, msg.messageId, { agentId })
149
- .catch(() => {});
150
- stopTypingKeepalive = startTypingKeepalive({
151
- messagingManager,
152
- userId,
153
- agentId,
154
- runId,
155
- platform: msg.platform,
156
- chatId: msg.chatId
157
- });
158
-
159
185
  const prompt = buildIncomingPrompt(msg);
160
186
  const conversationId = ensureConversation(userId, msg);
161
187
  const runOptions = isVoiceLikeMessage(msg)
@@ -182,105 +208,15 @@ async function processQueuedMessage({
182
208
  ];
183
209
  }
184
210
 
185
- await agentEngine.run(userId, prompt, runOptions);
211
+ const result = await agentEngine.run(userId, prompt, runOptions);
212
+ return { runId, result, error: null };
213
+ } catch (error) {
214
+ return { runId, result: null, error };
186
215
  } finally {
187
216
  await stopTypingKeepalive();
188
- if (queue.cancelRequested) {
189
- queue.pending = [];
190
- queue.running = false;
191
- queue.cancelRequested = false;
192
- return;
193
- }
194
- queue.running = false;
195
- if (queue.pending.length > 0) {
196
- const next = queue.pending.shift();
197
- await processQueuedMessage({
198
- userQueues,
199
- messagingManager,
200
- agentEngine,
201
- userId,
202
- msg: next
203
- });
204
- }
205
217
  }
206
218
  }
207
219
 
208
- function startTypingKeepalive({
209
- messagingManager,
210
- userId,
211
- agentId,
212
- runId,
213
- platform,
214
- chatId,
215
- intervalMs = 4000
216
- }) {
217
- let stopped = false;
218
- let timer = null;
219
- let releaseWait = null;
220
- let stopPromise = null;
221
-
222
- const matchesRunDelivery = (event) => (
223
- event?.runId
224
- && runId
225
- && event.runId === runId
226
- && event.userId === userId
227
- && event.platform === platform
228
- && event.to === chatId
229
- );
230
-
231
- const onMessageSent = (event) => {
232
- if (matchesRunDelivery(event) && event.deliveryKind !== 'interim') {
233
- stop().catch(() => {});
234
- }
235
- };
236
-
237
- if (typeof messagingManager?.on === 'function' && typeof messagingManager?.off === 'function') {
238
- messagingManager.on('message_sent', onMessageSent);
239
- }
240
-
241
- const wait = () =>
242
- new Promise((resolve) => {
243
- releaseWait = resolve;
244
- timer = setTimeout(resolve, intervalMs);
245
- });
246
-
247
- const loop = (async () => {
248
- while (!stopped) {
249
- await messagingManager
250
- .sendTyping(userId, platform, chatId, true, { agentId })
251
- .catch(() => {});
252
-
253
- if (stopped) break;
254
- await wait();
255
- }
256
- })();
257
-
258
- const stop = async () => {
259
- if (stopPromise) return stopPromise;
260
- stopPromise = (async () => {
261
- if (typeof messagingManager?.off === 'function') {
262
- messagingManager.off('message_sent', onMessageSent);
263
- }
264
- stopped = true;
265
- if (timer) {
266
- clearTimeout(timer);
267
- timer = null;
268
- }
269
- if (releaseWait) {
270
- releaseWait();
271
- releaseWait = null;
272
- }
273
- await loop.catch(() => {});
274
- await messagingManager
275
- .sendTyping(userId, platform, chatId, false, { agentId })
276
- .catch(() => {});
277
- })();
278
- return stopPromise;
279
- };
280
-
281
- return stop;
282
- }
283
-
284
220
  function ensureConversation(userId, msg) {
285
221
  const agentId = msg.agentId || null;
286
222
  let conversation = db
@@ -419,6 +355,7 @@ function emitBlockedSenderSuggestion({ io, userId, msg }) {
419
355
  module.exports = {
420
356
  buildIncomingPrompt,
421
357
  buildSenderIdentityBlock,
358
+ executeQueuedMessage,
422
359
  isAllowedMessagingSender,
423
360
  processQueuedMessage,
424
361
  registerMessagingAutomation,
@@ -99,9 +99,34 @@ function adaptSpokenFormatting(text) {
99
99
  .trim();
100
100
  }
101
101
 
102
+ function coerceMessageContent(content) {
103
+ if (typeof content === 'string') return content;
104
+ if (content == null) return '';
105
+ if (typeof content === 'number' || typeof content === 'boolean' || typeof content === 'bigint') {
106
+ return String(content);
107
+ }
108
+ if (typeof content !== 'object') return String(content || '');
109
+
110
+ for (const key of ['text', 'content', 'message', 'summary']) {
111
+ if (typeof content[key] === 'string' && content[key].trim()) {
112
+ return content[key];
113
+ }
114
+ }
115
+
116
+ if (content.result != null && content.result !== content) {
117
+ return coerceMessageContent(content.result);
118
+ }
119
+
120
+ try {
121
+ return JSON.stringify(content, null, 2);
122
+ } catch {
123
+ return String(content || '');
124
+ }
125
+ }
126
+
102
127
  function normalizeOutgoingMessageForPlatform(platform, content, options = {}) {
103
128
  const profile = getPlatformFormattingProfile(platform);
104
- let text = String(content || '');
129
+ let text = coerceMessageContent(content);
105
130
 
106
131
  if (options.stripNoResponseMarker !== false) {
107
132
  text = text.replace(/\[NO RESPONSE\]/gi, '');
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const { getErrorMessage } = require('../bootstrap_helpers');
4
+
5
+ async function processInboundQueue({
6
+ userQueues,
7
+ userId,
8
+ msg,
9
+ executeMessage,
10
+ onProcessingError = null
11
+ }) {
12
+ const agentId = msg.agentId || null;
13
+ const queueKey = `${userId}:${agentId || 'main'}`;
14
+ if (!userQueues[queueKey]) {
15
+ userQueues[queueKey] = { running: false, pending: [], cancelRequested: false };
16
+ }
17
+ const queue = userQueues[queueKey];
18
+
19
+ if (queue.cancelRequested && !queue.running) {
20
+ queue.pending = [];
21
+ queue.cancelRequested = false;
22
+ }
23
+
24
+ if (queue.running) {
25
+ const last = queue.pending[queue.pending.length - 1];
26
+ if (
27
+ last
28
+ && last.platform === msg.platform
29
+ && last.chatId === msg.chatId
30
+ && String(last.sender || '') === String(msg.sender || '')
31
+ ) {
32
+ last.content += `\n${msg.content}`;
33
+ last.messageId = msg.messageId;
34
+ } else {
35
+ queue.pending.push({ ...msg });
36
+ }
37
+ return { queued: true };
38
+ }
39
+
40
+ queue.running = true;
41
+ let currentMessage = msg;
42
+ let processedCount = 0;
43
+ let failedCount = 0;
44
+ let cancelled = false;
45
+
46
+ try {
47
+ while (currentMessage) {
48
+ let outcome;
49
+ try {
50
+ outcome = await executeMessage(currentMessage);
51
+ } catch (error) {
52
+ outcome = { runId: null, result: null, error };
53
+ }
54
+ processedCount += 1;
55
+
56
+ if (outcome?.error) {
57
+ failedCount += 1;
58
+ await notifyProcessingError(onProcessingError, {
59
+ error: outcome.error,
60
+ runId: outcome.runId,
61
+ userId,
62
+ failedMessage: currentMessage
63
+ });
64
+ }
65
+
66
+ if (queue.cancelRequested) {
67
+ queue.pending = [];
68
+ cancelled = true;
69
+ break;
70
+ }
71
+
72
+ currentMessage = queue.pending.shift() || null;
73
+ }
74
+ } finally {
75
+ queue.running = false;
76
+ queue.pending = [];
77
+ queue.cancelRequested = false;
78
+ if (userQueues[queueKey] === queue) {
79
+ delete userQueues[queueKey];
80
+ }
81
+ }
82
+
83
+ return {
84
+ processedCount,
85
+ failedCount,
86
+ cancelled
87
+ };
88
+ }
89
+
90
+ async function notifyProcessingError(handler, details) {
91
+ if (typeof handler !== 'function') {
92
+ console.error(
93
+ `[MessagingAutomation] Agent run failed platform=${details.failedMessage.platform} user=${details.userId}:`,
94
+ getErrorMessage(details.error)
95
+ );
96
+ return;
97
+ }
98
+
99
+ try {
100
+ await handler(details);
101
+ } catch (error) {
102
+ console.error(
103
+ '[MessagingAutomation] Failed to report an agent run error:',
104
+ getErrorMessage(error)
105
+ );
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ processInboundQueue
111
+ };
@@ -104,6 +104,9 @@ class MessagingManager extends EventEmitter {
104
104
  return null;
105
105
  }
106
106
 
107
+ const normalizedIncomingContent = normalizeOutgoingMessageForPlatform(platformName, msg?.content, {
108
+ stripNoResponseMarker: false
109
+ });
107
110
  const agentId = this._agentId(userId, {
108
111
  ...options,
109
112
  agentId: options?.agentId ?? msg?.agentId ?? null,
@@ -135,7 +138,7 @@ class MessagingManager extends EventEmitter {
135
138
  userId,
136
139
  agentId,
137
140
  'user',
138
- msg.content,
141
+ normalizedIncomingContent,
139
142
  platformName,
140
143
  msg.messageId,
141
144
  msg.chatId,
@@ -143,7 +146,7 @@ class MessagingManager extends EventEmitter {
143
146
  msg.timestamp,
144
147
  );
145
148
 
146
- const enrichedMsg = { ...msg, agentId, platform: platformName };
149
+ const enrichedMsg = { ...msg, content: normalizedIncomingContent, agentId, platform: platformName };
147
150
 
148
151
  if (this.isShuttingDown) {
149
152
  return enrichedMsg;
@@ -317,6 +320,9 @@ class MessagingManager extends EventEmitter {
317
320
  config.userId = userId;
318
321
  config.agentId = agentId;
319
322
  config.accessPolicy = this._loadAccessPolicy(userId, agentId, platformName);
323
+ const existingConnection = db
324
+ .prepare('SELECT id, status FROM platform_connections WHERE user_id = ? AND agent_id = ? AND platform = ?')
325
+ .get(userId, agentId, platformName);
320
326
  const PlatformClass = this.platformTypes[platformName];
321
327
  if (!PlatformClass) throw new Error(`Unknown platform: ${platformName}`);
322
328
  if (platformName === 'meshtastic' && !readMeshtasticEnabled()) {
@@ -325,7 +331,18 @@ class MessagingManager extends EventEmitter {
325
331
 
326
332
  if (platformName === 'whatsapp' && !config.authDir) {
327
333
  config.authDir = this._scopedPlatformAuthDir(userId, agentId, platformName);
328
- this._maybeMigrateLegacyWhatsAppAuth(config.authDir);
334
+ let shouldMigrateLegacyAuth = true;
335
+ if (
336
+ existingConnection &&
337
+ existingConnection.status !== 'connected' &&
338
+ existingConnection.status !== 'awaiting_qr'
339
+ ) {
340
+ fs.rmSync(config.authDir, { recursive: true, force: true });
341
+ shouldMigrateLegacyAuth = false;
342
+ }
343
+ if (shouldMigrateLegacyAuth) {
344
+ this._maybeMigrateLegacyWhatsAppAuth(config.authDir);
345
+ }
329
346
  }
330
347
 
331
348
  // For Telnyx, inject saved whitelist and voice secret into config before constructing
@@ -443,8 +460,7 @@ class MessagingManager extends EventEmitter {
443
460
  await this.ingestMessage(userId, platformName, msg, { agentId });
444
461
  });
445
462
 
446
- const existing = db.prepare('SELECT id FROM platform_connections WHERE user_id = ? AND agent_id = ? AND platform = ?').get(userId, agentId, platformName);
447
- if (!existing) {
463
+ if (!existingConnection) {
448
464
  db.prepare('INSERT INTO platform_connections (user_id, agent_id, platform, config, status) VALUES (?, ?, ?, ?, ?)')
449
465
  .run(userId, agentId, platformName, storedConfig, 'connecting');
450
466
  } else {
@@ -460,10 +476,11 @@ class MessagingManager extends EventEmitter {
460
476
  const agentId = this._agentId(userId, options);
461
477
  const key = this._key(userId, agentId, platformName);
462
478
  const platform = this.platforms.get(key);
463
- if (!platform) return { status: 'not_connected' };
464
479
 
465
- await platform.disconnect();
466
- this.platforms.delete(key);
480
+ if (platform) {
481
+ await platform.disconnect();
482
+ this.platforms.delete(key);
483
+ }
467
484
 
468
485
  db.prepare('UPDATE platform_connections SET status = ? WHERE user_id = ? AND agent_id = ? AND platform = ?')
469
486
  .run('disconnected', userId, agentId, platformName);
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const { getErrorMessage } = require('../bootstrap_helpers');
4
+
5
+ function startTypingKeepalive({
6
+ messagingManager,
7
+ userId,
8
+ agentId,
9
+ runId,
10
+ platform,
11
+ chatId,
12
+ intervalMs = 4000,
13
+ onError = null
14
+ }) {
15
+ let stopped = false;
16
+ let timer = null;
17
+ let releaseWait = null;
18
+ let stopPromise = null;
19
+ const reportedFailures = new Set();
20
+
21
+ const reportFailure = (operation, error) => {
22
+ if (reportedFailures.has(operation)) return;
23
+ reportedFailures.add(operation);
24
+ if (typeof onError === 'function') {
25
+ try {
26
+ onError(operation, error);
27
+ } catch (reportError) {
28
+ console.error(
29
+ '[MessagingAutomation] Typing failure reporter failed:',
30
+ getErrorMessage(reportError)
31
+ );
32
+ }
33
+ }
34
+ };
35
+
36
+ const matchesRunDelivery = (event) => (
37
+ event?.runId
38
+ && runId
39
+ && event.runId === runId
40
+ && event.userId === userId
41
+ && event.platform === platform
42
+ && event.to === chatId
43
+ );
44
+
45
+ const onMessageSent = (event) => {
46
+ if (matchesRunDelivery(event) && event.deliveryKind !== 'interim') {
47
+ stop().catch((error) => reportFailure('stop typing keepalive', error));
48
+ }
49
+ };
50
+
51
+ if (typeof messagingManager?.on === 'function' && typeof messagingManager?.off === 'function') {
52
+ messagingManager.on('message_sent', onMessageSent);
53
+ }
54
+
55
+ const wait = () =>
56
+ new Promise((resolve) => {
57
+ releaseWait = resolve;
58
+ timer = setTimeout(resolve, intervalMs);
59
+ });
60
+
61
+ const sendTyping = async (isTyping, operation) => {
62
+ try {
63
+ await messagingManager.sendTyping(
64
+ userId,
65
+ platform,
66
+ chatId,
67
+ isTyping,
68
+ { agentId }
69
+ );
70
+ } catch (error) {
71
+ reportFailure(operation, error);
72
+ }
73
+ };
74
+
75
+ const loop = (async () => {
76
+ while (!stopped) {
77
+ await sendTyping(true, 'send typing indicator');
78
+
79
+ if (stopped) break;
80
+ await wait();
81
+ }
82
+ })();
83
+
84
+ const stop = async () => {
85
+ if (stopPromise) return stopPromise;
86
+ stopPromise = (async () => {
87
+ if (typeof messagingManager?.off === 'function') {
88
+ messagingManager.off('message_sent', onMessageSent);
89
+ }
90
+ stopped = true;
91
+ if (timer) {
92
+ clearTimeout(timer);
93
+ timer = null;
94
+ }
95
+ if (releaseWait) {
96
+ releaseWait();
97
+ releaseWait = null;
98
+ }
99
+ await loop.catch((error) => reportFailure('typing keepalive loop', error));
100
+ await sendTyping(false, 'clear typing indicator');
101
+ })();
102
+ return stopPromise;
103
+ };
104
+
105
+ return stop;
106
+ }
107
+
108
+ module.exports = {
109
+ startTypingKeepalive
110
+ };
@@ -31,10 +31,11 @@ class RuntimeManager {
31
31
  this.artifactStore = options.artifactStore || null;
32
32
 
33
33
  this.getExtensionBrowserProvider = options.getExtensionBrowserProvider
34
- || ((userId) => new ExtensionBrowserProvider({
34
+ || ((userId, providerOptions = {}) => new ExtensionBrowserProvider({
35
35
  registry: options.browserExtensionRegistry,
36
36
  artifactStore: options.artifactStore,
37
37
  userId,
38
+ tokenId: providerOptions.tokenId || null,
38
39
  }));
39
40
 
40
41
  this.getDesktopCliProvider = options.getDesktopCliProvider
@@ -51,10 +52,11 @@ class RuntimeManager {
51
52
  }
52
53
 
53
54
  hasActiveExtensionBrowser(userId) {
55
+ const settings = this.getSettings(userId);
54
56
  return Boolean(
55
57
  this.browserExtensionRegistry
56
58
  && typeof this.browserExtensionRegistry.isConnected === 'function'
57
- && this.browserExtensionRegistry.isConnected(userId)
59
+ && this.browserExtensionRegistry.isConnected(userId, settings.browser_extension_token_id)
58
60
  );
59
61
  }
60
62
 
@@ -92,7 +94,9 @@ class RuntimeManager {
92
94
  async getBrowserProviderForUser(userId) {
93
95
  const settings = this.getSettings(userId);
94
96
  if (settings.browser_backend === 'extension' && this.hasActiveExtensionBrowser(userId)) {
95
- return this.getExtensionBrowserProvider(userId);
97
+ return this.getExtensionBrowserProvider(userId, {
98
+ tokenId: settings.browser_extension_token_id,
99
+ });
96
100
  }
97
101
  return this.browserBackend.getBrowserProviderForUser(userId);
98
102
  }
@@ -10,6 +10,9 @@ function createDefaultRuntimeSettings() {
10
10
  android_backend: policy.runtimeDefaults.android_backend,
11
11
  mcp_backend: policy.runtimeDefaults.mcp_backend,
12
12
  cli_backend: policy.runtimeDefaults.cli_backend ?? 'vm',
13
+ browser_extension_token_id: null,
14
+ selected_browser_extension_token_id: null,
15
+ cli_desktop_device_id: null,
13
16
  };
14
17
  }
15
18
 
@@ -26,6 +29,9 @@ const BASE_FALLBACK_SETTINGS = Object.freeze({
26
29
  android_backend: 'host',
27
30
  mcp_backend: 'host-remote',
28
31
  cli_backend: 'vm',
32
+ browser_extension_token_id: null,
33
+ selected_browser_extension_token_id: null,
34
+ cli_desktop_device_id: null,
29
35
  });
30
36
 
31
37
  const RUNTIME_SETTING_KEYS = Object.freeze(Object.keys(DEFAULT_RUNTIME_SETTINGS));
@@ -35,6 +41,11 @@ function normalizeChoice(value, allowed, fallback) {
35
41
  return allowed.includes(normalized) ? normalized : fallback;
36
42
  }
37
43
 
44
+ function normalizeOptionalString(value) {
45
+ const normalized = String(value || '').trim();
46
+ return normalized || null;
47
+ }
48
+
38
49
  function deriveDefaultsForProfile(profile) {
39
50
  switch (profile) {
40
51
  case 'secure-vm':
@@ -57,6 +68,9 @@ function normalizeRuntimeSettings(raw = {}) {
57
68
  const runtimeBackend = normalizeChoice(raw.runtime_backend, ['vm'], derived.runtime_backend);
58
69
  const browserBackend = normalizeChoice(raw.browser_backend, ['vm', 'extension'], derived.browser_backend);
59
70
  const cliBackend = normalizeChoice(raw.cli_backend, ['vm', 'desktop'], derived.cli_backend ?? 'vm');
71
+ const selectedExtensionTokenId = normalizeOptionalString(
72
+ raw.browser_extension_token_id || raw.selected_browser_extension_token_id,
73
+ );
60
74
  const androidBackend = 'host';
61
75
  return {
62
76
  runtime_profile: profile === 'trusted-host' ? 'secure-vm' : profile,
@@ -65,6 +79,9 @@ function normalizeRuntimeSettings(raw = {}) {
65
79
  android_backend: androidBackend,
66
80
  mcp_backend: 'host-remote',
67
81
  cli_backend: cliBackend === 'desktop' ? 'desktop' : 'vm',
82
+ browser_extension_token_id: selectedExtensionTokenId,
83
+ selected_browser_extension_token_id: selectedExtensionTokenId,
84
+ cli_desktop_device_id: normalizeOptionalString(raw.cli_desktop_device_id),
68
85
  };
69
86
  }
70
87