neoagent 3.2.1-beta.2 → 3.2.1-beta.4

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 (145) 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 +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/messaging/whatsapp.js +22 -14
  116. package/server/services/network/http.js +210 -0
  117. package/server/services/network/safe_request.js +307 -0
  118. package/server/services/runtime/backends/local-vm.js +214 -66
  119. package/server/services/runtime/manager.js +17 -12
  120. package/server/services/social_reach/channels/github.js +10 -4
  121. package/server/services/social_reach/channels/reddit.js +4 -4
  122. package/server/services/social_reach/channels/rss.js +2 -2
  123. package/server/services/social_reach/channels/social_video.js +12 -7
  124. package/server/services/social_reach/channels/v2ex.js +21 -8
  125. package/server/services/social_reach/channels/x.js +2 -2
  126. package/server/services/social_reach/channels/xueqiu.js +5 -5
  127. package/server/services/social_reach/service.js +9 -6
  128. package/server/services/social_reach/utils.js +65 -14
  129. package/server/services/social_video/service.js +160 -50
  130. package/server/services/tasks/integration_runtime.js +18 -8
  131. package/server/services/tasks/runtime.js +39 -4
  132. package/server/services/voice/agentBridge.js +17 -4
  133. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  134. package/server/services/voice/liveSession.js +31 -0
  135. package/server/services/voice/openaiSpeech.js +33 -8
  136. package/server/services/voice/providers.js +233 -151
  137. package/server/services/voice/runtimeManager.js +118 -20
  138. package/server/services/voice/turnRunner.js +6 -0
  139. package/server/services/wearable/firmware_manifest.js +51 -13
  140. package/server/services/wearable/service.js +1 -0
  141. package/server/utils/abort.js +96 -0
  142. package/server/utils/cloud-security.js +110 -3
  143. package/server/utils/files.js +31 -0
  144. package/server/utils/image_payload.js +95 -0
  145. package/server/utils/retry.js +107 -0
@@ -12,8 +12,11 @@ const {
12
12
  safeTrim,
13
13
  } = require('./ingestion_support');
14
14
  const { chunkDocument, overlapWindowChunks } = require('./ingestion_chunking');
15
+ const { isAbortError, throwIfAborted } = require('../../utils/abort');
15
16
 
16
17
  async function ingestDocuments(service, userId, documents = [], options = {}) {
18
+ const signal = options.signal || null;
19
+ throwIfAborted(signal, 'Memory ingestion aborted.');
17
20
  const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
18
21
  const normalizedDocs = (Array.isArray(documents) ? documents : [])
19
22
  .map((document) => normalizeDocument(document, {
@@ -45,6 +48,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
45
48
  const memoryIds = [];
46
49
  try {
47
50
  for (const document of normalizedDocs) {
51
+ throwIfAborted(signal, 'Memory ingestion aborted.');
48
52
  const documentId = service.memoryManager.upsertIngestionDocument(
49
53
  userId,
50
54
  document,
@@ -55,6 +59,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
55
59
  const chunks = chunkDocument(document);
56
60
  const savedChunkMemoryIds = [];
57
61
  for (const chunk of chunks) {
62
+ throwIfAborted(signal, 'Memory ingestion aborted.');
58
63
  const memoryId = await service.memoryManager.saveMemory(
59
64
  userId,
60
65
  `${document.title}\n${chunk.content}`,
@@ -87,6 +92,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
87
92
  charEnd: chunk.charEnd,
88
93
  },
89
94
  },
95
+ signal,
90
96
  },
91
97
  );
92
98
  if (!memoryId) continue;
@@ -110,6 +116,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
110
116
 
111
117
  const overlapChunks = overlapWindowChunks(chunks);
112
118
  for (const window of overlapChunks) {
119
+ throwIfAborted(signal, 'Memory ingestion aborted.');
113
120
  const windowMemoryId = await service.memoryManager.saveMemory(
114
121
  userId,
115
122
  `${document.title}\n${window.content}`,
@@ -143,6 +150,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
143
150
  },
144
151
  isOverlapWindow: true,
145
152
  },
153
+ signal,
146
154
  },
147
155
  );
148
156
  if (windowMemoryId) savedChunkMemoryIds.push(windowMemoryId);
@@ -151,6 +159,7 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
151
159
  service.memoryManager.pruneSourceChunks(documentId, retainedChunkIds);
152
160
  }
153
161
 
162
+ throwIfAborted(signal, 'Memory ingestion aborted.');
154
163
  service.memoryManager.recordIngestionJob(userId, {
155
164
  id: jobId,
156
165
  sourceType,
@@ -174,17 +183,18 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
174
183
  knowledgeViews,
175
184
  };
176
185
  } catch (err) {
186
+ const cancelled = isAbortError(err, signal);
177
187
  service.memoryManager.recordIngestionJob(userId, {
178
188
  id: jobId,
179
189
  sourceType,
180
190
  providerKey: safeTrim(options.providerKey, 80),
181
191
  connectionId,
182
- status: 'failed',
192
+ status: cancelled ? 'cancelled' : 'failed',
183
193
  freshnessPolicy: policy,
184
194
  documentCount: documentIds.length,
185
- error: err.message,
195
+ error: cancelled ? null : err.message,
186
196
  completedAt: new Date().toISOString(),
187
- nextSyncAt: nextSyncFromPolicy(policy),
197
+ nextSyncAt: cancelled ? null : nextSyncFromPolicy(policy),
188
198
  }, { agentId });
189
199
  throw err;
190
200
  }
@@ -3,7 +3,6 @@ const path = require('path');
3
3
  const { v4: uuidv4 } = require('uuid');
4
4
  const db = require('../../db/database');
5
5
  const {
6
- getEmbedding,
7
6
  getEmbeddingWithMetadata,
8
7
  cosineSimilarity,
9
8
  serializeEmbedding,
@@ -41,11 +40,14 @@ const {
41
40
  isLocalEncryptedValue,
42
41
  } = require('../../utils/local_secrets');
43
42
 
44
- async function getActiveProvider(userId, agentId = null) {
43
+ async function getActiveProvider(userId, agentId = null, options = {}) {
45
44
  try {
46
45
  const { getSupportedModels } = require('../ai/models');
46
+ const { resolveModelSelection } = require('../ai/model_identity');
47
47
  const { getAiSettings } = require('../ai/settings');
48
- const models = await getSupportedModels(userId, agentId);
48
+ const models = await getSupportedModels(userId, agentId, {
49
+ signal: options.signal,
50
+ });
49
51
  const aiSettings = getAiSettings(userId, agentId);
50
52
  const defaultChatModel = aiSettings.default_chat_model || null;
51
53
  const enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : null;
@@ -55,7 +57,10 @@ async function getActiveProvider(userId, agentId = null) {
55
57
  : (Array.isArray(enabledIds) && enabledIds.length > 0 ? enabledIds[0] : null);
56
58
 
57
59
  if (modelId) {
58
- const def = models.find(m => m.id === modelId && m.available !== false);
60
+ const def = resolveModelSelection(
61
+ models.filter((model) => model.available !== false),
62
+ modelId,
63
+ );
59
64
  if (def) return def.provider;
60
65
  }
61
66
  } catch { }
@@ -1649,29 +1654,32 @@ class MemoryManager {
1649
1654
  const memoryHash = stableHash(`${category}:${content}`);
1650
1655
  const summary = summarizeForPrompt({ content, entities: extractEntities(content) });
1651
1656
 
1652
- const embeddingResult = await getEmbeddingWithMetadata(
1653
- content,
1654
- await getActiveProvider(userId, agentId),
1655
- );
1656
- const embedding = embeddingResult?.vector || null;
1657
-
1658
1657
  const exact = this._findExactMemory(userId, agentId, memoryHash, scope);
1659
1658
  if (exact?.id) {
1660
1659
  return this._reinforceExactMemory(exact.id, importance);
1661
1660
  }
1662
1661
 
1662
+ const embeddingResult = await getEmbeddingWithMetadata(
1663
+ content,
1664
+ await getActiveProvider(userId, agentId, options),
1665
+ { signal: options.signal, inputType: 'document' },
1666
+ );
1667
+ const embedding = embeddingResult?.vector || null;
1668
+
1663
1669
  const duplicateCandidateIds = embedding
1664
1670
  ? findEmbeddingCandidates(db, {
1665
1671
  userId,
1666
1672
  agentId,
1667
1673
  embedding,
1674
+ embeddingProvider: embeddingResult.provider,
1675
+ embeddingModel: embeddingResult.model,
1668
1676
  limit: 120,
1669
1677
  }).map((candidate) => candidate.memory_id)
1670
1678
  : this._searchMemoryFts(userId, agentId, content, 120)
1671
1679
  .map((candidate) => candidate.memory_id);
1672
1680
  const existing = duplicateCandidateIds.length
1673
1681
  ? db.prepare(
1674
- `SELECT id, content, embedding, metadata_json
1682
+ `SELECT id, content, embedding, embedding_provider, embedding_model, metadata_json
1675
1683
  FROM memories
1676
1684
  WHERE id IN (${duplicateCandidateIds.map(() => '?').join(', ')})
1677
1685
  AND user_id = ? AND agent_id = ? AND archived = 0
@@ -1688,7 +1696,12 @@ class MemoryManager {
1688
1696
 
1689
1697
  for (const mem of structuredFacts.length ? [] : existing) {
1690
1698
  let sim = 0;
1691
- if (embedding && mem.embedding) {
1699
+ if (
1700
+ embedding
1701
+ && mem.embedding
1702
+ && mem.embedding_provider === embeddingResult?.provider
1703
+ && mem.embedding_model === embeddingResult?.model
1704
+ ) {
1692
1705
  const memVec = deserializeEmbedding(mem.embedding);
1693
1706
  if (memVec) sim = cosineSimilarity(embedding, memVec);
1694
1707
  } else {
@@ -1839,6 +1852,7 @@ class MemoryManager {
1839
1852
  conversationId: options.conversationId || null,
1840
1853
  runId: options.runId || null,
1841
1854
  },
1855
+ signal: options.signal,
1842
1856
  },
1843
1857
  );
1844
1858
  if (memoryId) memoryIds.push(memoryId);
@@ -1860,9 +1874,18 @@ class MemoryManager {
1860
1874
  const route = routeMemoryQuery(query);
1861
1875
 
1862
1876
  const suppliedQueryEmbedding = options.queryEmbedding;
1863
- const queryVec = suppliedQueryEmbedding
1864
- ? new Float32Array(Array.from(suppliedQueryEmbedding, Number))
1865
- : await getEmbedding(query, await getActiveProvider(userId, agentId));
1877
+ const queryEmbeddingResult = suppliedQueryEmbedding
1878
+ ? {
1879
+ vector: new Float32Array(Array.from(suppliedQueryEmbedding, Number)),
1880
+ provider: options.queryEmbeddingProvider || null,
1881
+ model: options.queryEmbeddingModel || null,
1882
+ }
1883
+ : await getEmbeddingWithMetadata(
1884
+ query,
1885
+ await getActiveProvider(userId, agentId, options),
1886
+ { signal: options.signal, inputType: 'query' },
1887
+ );
1888
+ const queryVec = queryEmbeddingResult?.vector || null;
1866
1889
  const lexicalHits = this._searchMemoryFts(userId, agentId, query, Math.max(80, limit * 12));
1867
1890
  const entityHits = this._searchEntityMemoryIds(userId, agentId, query, Math.max(80, limit * 12));
1868
1891
  const vectorHits = queryVec
@@ -1870,6 +1893,8 @@ class MemoryManager {
1870
1893
  userId,
1871
1894
  agentId,
1872
1895
  embedding: queryVec,
1896
+ embeddingProvider: queryEmbeddingResult?.provider,
1897
+ embeddingModel: queryEmbeddingResult?.model,
1873
1898
  limit: Math.min(500, Math.max(300, limit * 20)),
1874
1899
  })
1875
1900
  : [];
@@ -1917,7 +1942,8 @@ class MemoryManager {
1917
1942
  let all = db.prepare(
1918
1943
  `SELECT id, category, content, summary, importance, confidence, embedding, access_count,
1919
1944
  memory_strength, last_accessed_at, pinned, created_at, updated_at,
1920
- scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
1945
+ scope_type, scope_id, source_type, source_id, source_label, stale_after_days,
1946
+ embedding_provider, embedding_model, metadata_json
1921
1947
  FROM memories
1922
1948
  WHERE id IN (${candidatePlaceholders})
1923
1949
  AND user_id = ? AND agent_id = ? AND archived = 0
@@ -1997,7 +2023,13 @@ class MemoryManager {
1997
2023
 
1998
2024
  const semanticScored = all.map(mem => {
1999
2025
  let semanticScore = 0;
2000
- if (queryVec && mem.embedding) {
2026
+ const sameEmbeddingSpace = !queryEmbeddingResult?.provider
2027
+ || !queryEmbeddingResult?.model
2028
+ || (
2029
+ mem.embedding_provider === queryEmbeddingResult.provider
2030
+ && mem.embedding_model === queryEmbeddingResult.model
2031
+ );
2032
+ if (queryVec && mem.embedding && sameEmbeddingSpace) {
2001
2033
  const memVec = deserializeEmbedding(mem.embedding);
2002
2034
  if (memVec) {
2003
2035
  semanticScore = cosineSimilarity(queryVec, memVec);
@@ -2126,7 +2158,7 @@ class MemoryManager {
2126
2158
  /**
2127
2159
  * Update a memory's content and/or importance.
2128
2160
  */
2129
- async updateMemory(id, { content, importance, category }) {
2161
+ async updateMemory(id, { content, importance, category, signal = null }) {
2130
2162
  const mem = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id);
2131
2163
  if (!mem) return null;
2132
2164
 
@@ -2141,7 +2173,8 @@ class MemoryManager {
2141
2173
  if (content && content !== mem.content) {
2142
2174
  embeddingResult = await getEmbeddingWithMetadata(
2143
2175
  newContent,
2144
- await getActiveProvider(mem.user_id, mem.agent_id),
2176
+ await getActiveProvider(mem.user_id, mem.agent_id, { signal }),
2177
+ { signal, inputType: 'document' },
2145
2178
  );
2146
2179
  newEmbed = embeddingResult?.vector
2147
2180
  ? serializeEmbedding(embeddingResult.vector)
@@ -20,17 +20,53 @@ const {
20
20
  } = require('../voice/runtime');
21
21
  const { getErrorMessage } = require('../bootstrap_helpers');
22
22
  const { processInboundQueue } = require('./inbound_queue');
23
+ const { attachRunToInboundJobs } = require('./inbound_store');
23
24
  const { startTypingKeepalive } = require('./typing_keepalive');
25
+ const { waitForBoundedResult } = require('../network/http');
26
+ const { createAbortError, throwIfAborted } = require('../../utils/abort');
24
27
 
25
28
  function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
26
29
  const userQueues = Object.create(null);
30
+ const activeHandlers = new Set();
31
+ const abortController = new AbortController();
32
+ const runtime = {
33
+ shuttingDown: false,
34
+ shutdownPromise: null,
35
+ shutdown() {
36
+ if (this.shutdownPromise) return this.shutdownPromise;
37
+ this.shuttingDown = true;
38
+ const error = new Error('Messaging automation is shutting down.');
39
+ error.name = 'AbortError';
40
+ error.code = 'MESSAGING_AUTOMATION_SHUTDOWN';
41
+ abortController.abort(error);
42
+ for (const queue of Object.values(userQueues)) {
43
+ queue.cancelRequested = true;
44
+ queue.cancelPending?.();
45
+ }
46
+ this.shutdownPromise = waitForBoundedResult(
47
+ Promise.allSettled(Array.from(activeHandlers)),
48
+ {
49
+ serviceName: 'Messaging automation',
50
+ timeoutMs: 10000,
51
+ },
52
+ ).then(() => ({ state: 'stopped', timedOut: false })).catch((error) => ({
53
+ state: 'timeout',
54
+ timedOut: error?.code === 'HTTP_TIMEOUT',
55
+ error: error?.message || String(error),
56
+ }));
57
+ return this.shutdownPromise;
58
+ },
59
+ };
27
60
  app.locals.userQueues = userQueues;
61
+ app.locals.messagingAutomationRuntime = runtime;
28
62
 
29
- messagingManager.registerHandler(async (userId, msg) => {
63
+ const handleMessage = async (userId, msg, signal) => {
64
+ throwIfAborted(signal, 'Messaging automation stopped before handling the message.');
30
65
  const agentId = msg.agentId || null;
31
66
  if (!(await isAllowedMessagingSender({ io, userId, msg }))) {
32
67
  return;
33
68
  }
69
+ throwIfAborted(signal, 'Messaging automation stopped before handling the message.');
34
70
 
35
71
  const commandRouter = app?.locals?.commandRouter;
36
72
  if (commandRouter) {
@@ -42,9 +78,11 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
42
78
  source: 'messaging',
43
79
  platform: msg.platform,
44
80
  chatId: msg.chatId,
45
- sender: msg.sender
81
+ sender: msg.sender,
82
+ signal,
46
83
  });
47
84
  } catch (err) {
85
+ if (signal?.aborted) throw createAbortError(signal);
48
86
  console.error(`[Messaging] Command dispatch failed on ${msg.platform}:`, err.message);
49
87
  io.to(`user:${userId}`).emit('messaging:error', {
50
88
  error: `Command dispatch failed on ${msg.platform}: ${err.message}`
@@ -55,9 +93,10 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
55
93
  msg.platform,
56
94
  msg.chatId,
57
95
  `Command handling failed: ${err.message}`,
58
- { runId: null, agentId }
96
+ { runId: null, agentId, signal }
59
97
  );
60
98
  } catch (sendErr) {
99
+ if (signal?.aborted) throw createAbortError(signal);
61
100
  console.error(`[Messaging] Failed to report command dispatch error on ${msg.platform}:`, sendErr.message);
62
101
  io.to(`user:${userId}`).emit('messaging:error', {
63
102
  error: `Command handling failed and the error report could not be sent on ${msg.platform}: ${sendErr.message}`
@@ -78,9 +117,10 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
78
117
  msg.platform,
79
118
  msg.chatId,
80
119
  commandResult.content || 'Done.',
81
- { runId: null, agentId }
120
+ { runId: null, agentId, signal }
82
121
  );
83
122
  } catch (err) {
123
+ if (signal?.aborted) throw createAbortError(signal);
84
124
  console.error(`[Messaging] Failed to send command response on ${msg.platform}:`, err.message);
85
125
  io.to(`user:${userId}`).emit('messaging:error', {
86
126
  error: `Command executed but response could not be sent on ${msg.platform}: ${err.message}`
@@ -98,12 +138,13 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
98
138
  upsertSetting.run(userId, agentId, 'last_platform', msg.platform);
99
139
  upsertSetting.run(userId, agentId, 'last_chat_id', msg.chatId);
100
140
 
101
- await processQueuedMessage({
141
+ return processQueuedMessage({
102
142
  userQueues,
103
143
  messagingManager,
104
144
  agentEngine,
105
145
  userId,
106
146
  msg,
147
+ signal,
107
148
  onProcessingError: ({ error, runId, failedMessage }) => {
108
149
  const errorMessage = getErrorMessage(error);
109
150
  console.error(
@@ -118,7 +159,23 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
118
159
  });
119
160
  }
120
161
  });
162
+ };
163
+ messagingManager.registerHandler((userId, msg) => {
164
+ if (runtime.shuttingDown) return null;
165
+ const promise = handleMessage(userId, msg, abortController.signal);
166
+ activeHandlers.add(promise);
167
+ const cleanup = () => activeHandlers.delete(promise);
168
+ promise.then(cleanup, cleanup);
169
+ return promise;
121
170
  });
171
+ if (typeof messagingManager.recoverPendingInbound === 'function') {
172
+ void messagingManager.recoverPendingInbound().catch((error) => {
173
+ if (!runtime.shuttingDown) {
174
+ console.error('[MessagingAutomation] Inbound recovery failed:', getErrorMessage(error));
175
+ }
176
+ });
177
+ }
178
+ return runtime;
122
179
  }
123
180
 
124
181
  async function processQueuedMessage({
@@ -127,6 +184,7 @@ async function processQueuedMessage({
127
184
  agentEngine,
128
185
  userId,
129
186
  msg,
187
+ signal = null,
130
188
  onProcessingError = null
131
189
  }) {
132
190
  return processInboundQueue({
@@ -138,7 +196,8 @@ async function processQueuedMessage({
138
196
  messagingManager,
139
197
  agentEngine,
140
198
  userId,
141
- msg: queuedMessage
199
+ msg: queuedMessage,
200
+ signal,
142
201
  }),
143
202
  onProcessingError
144
203
  });
@@ -148,10 +207,17 @@ async function executeQueuedMessage({
148
207
  messagingManager,
149
208
  agentEngine,
150
209
  userId,
151
- msg
210
+ msg,
211
+ signal = null,
152
212
  }) {
213
+ throwIfAborted(signal, 'Messaging request aborted before execution.');
153
214
  const agentId = msg.agentId || null;
154
215
  const runId = randomUUID();
216
+ const inboundJobIds = Array.from(new Set([
217
+ ...(Array.isArray(msg.inboundJobIds) ? msg.inboundJobIds : []),
218
+ msg.inboundJobId,
219
+ ].map((value) => String(value || '').trim()).filter(Boolean)));
220
+ if (inboundJobIds.length) attachRunToInboundJobs(inboundJobIds, runId);
155
221
  const reportSideEffectError = (operation, error) => {
156
222
  console.warn(
157
223
  `[MessagingAutomation] ${operation} failed platform=${msg.platform} user=${userId}:`,
@@ -165,7 +231,7 @@ async function executeQueuedMessage({
165
231
  msg.platform,
166
232
  msg.chatId,
167
233
  msg.messageId,
168
- { agentId }
234
+ { agentId, signal }
169
235
  );
170
236
  } catch (error) {
171
237
  reportSideEffectError('mark read', error);
@@ -178,6 +244,7 @@ async function executeQueuedMessage({
178
244
  runId,
179
245
  platform: msg.platform,
180
246
  chatId: msg.chatId,
247
+ signal,
181
248
  onError: reportSideEffectError
182
249
  });
183
250
 
@@ -199,6 +266,7 @@ async function executeQueuedMessage({
199
266
  conversationId,
200
267
  source: msg.platform,
201
268
  chatId: msg.chatId,
269
+ messagingInboundJobId: inboundJobIds[0] || null,
202
270
  context: { rawUserMessage: msg.content }
203
271
  };
204
272
 
@@ -208,10 +276,17 @@ async function executeQueuedMessage({
208
276
  ];
209
277
  }
210
278
 
279
+ runOptions.messagingInboundJobId = inboundJobIds[0] || null;
280
+ runOptions.signal = signal;
281
+
211
282
  const result = await agentEngine.run(userId, prompt, runOptions);
212
283
  return { runId, result, error: null };
213
284
  } catch (error) {
214
- return { runId, result: null, error };
285
+ return {
286
+ runId,
287
+ result: null,
288
+ error: signal?.aborted ? createAbortError(signal) : error,
289
+ };
215
290
  } finally {
216
291
  await stopTypingKeepalive();
217
292
  }
@@ -4,6 +4,7 @@ const crypto = require('crypto');
4
4
  const net = require('net');
5
5
  const tls = require('tls');
6
6
  const { BasePlatform } = require('./base');
7
+ const { fetchResponseText } = require('../network/http');
7
8
 
8
9
  function requireText(value, label) {
9
10
  const text = String(value || '').trim();
@@ -51,14 +52,18 @@ function parseHeaders(value) {
51
52
  }
52
53
 
53
54
  async function fetchJson(url, options = {}, serviceName = 'Messaging platform') {
54
- const response = await fetch(url, {
55
+ const { response, text } = await fetchResponseText(url, {
55
56
  ...options,
56
57
  headers: {
57
58
  ...(options.body == null ? {} : { 'content-type': 'application/json' }),
58
59
  ...(options.headers || {}),
59
60
  },
61
+ maxResponseBytes: Number(options.maxResponseBytes) > 0
62
+ ? Number(options.maxResponseBytes)
63
+ : 2 * 1024 * 1024,
64
+ serviceName,
65
+ timeoutMs: Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 15000,
60
66
  });
61
- const text = await response.text();
62
67
  let body = null;
63
68
  if (text) {
64
69
  try { body = JSON.parse(text); } catch { body = text; }
@@ -71,7 +76,13 @@ async function fetchJson(url, options = {}, serviceName = 'Messaging platform')
71
76
  .replace(/https?:\/\/[^\s"'<>]+/gi, '[redacted-url]')
72
77
  .replace(/\b(token|access_token|refresh_token|authorization)\b\s*[:=]\s*"[^"]*"/gi, '$1=[redacted]')
73
78
  .replace(/\b(token|access_token|refresh_token|authorization)\b\s*[:=]\s*[^,\s;"'}\]]+/gi, '$1=[redacted]');
74
- throw new Error(`${serviceName} request failed (${response.status}): ${detail || response.statusText}`);
79
+ const error = new Error(
80
+ `${serviceName} request failed (${response.status}): ${detail || response.statusText}`,
81
+ );
82
+ error.status = response.status;
83
+ error.headers = response.headers;
84
+ error.safeToRetry = response.status === 429;
85
+ throw error;
75
86
  }
76
87
  return body;
77
88
  }
@@ -173,7 +184,7 @@ class ConfigurableHttpPlatform extends BasePlatform {
173
184
  };
174
185
  }
175
186
 
176
- async sendMessage(to, content) {
187
+ async sendMessage(to, content, options = {}) {
177
188
  const urlTemplate = this.config.outboundUrl || this.config.webhookUrl || this.defaults.outboundUrl || this.defaults.webhookUrl;
178
189
  if (!urlTemplate) throw new Error(`${this.defaults.label || this.name} outbound URL is not configured`);
179
190
 
@@ -209,6 +220,7 @@ class ConfigurableHttpPlatform extends BasePlatform {
209
220
  method: this.config.method || this.defaults.method || 'POST',
210
221
  headers,
211
222
  body: JSON.stringify(body),
223
+ signal: options.signal,
212
224
  }, this.defaults.label || this.name);
213
225
  return { success: true };
214
226
  }
@@ -284,12 +296,13 @@ class SlackPlatform extends BasePlatform {
284
296
  return this._botUserId ? { username: this._botUserId } : null;
285
297
  }
286
298
 
287
- async sendMessage(to, content) {
299
+ async sendMessage(to, content, options = {}) {
288
300
  if (!this.botToken) throw new Error('Slack bot token is required for outbound messages');
289
301
  const result = await fetchJson('https://slack.com/api/chat.postMessage', {
290
302
  method: 'POST',
291
303
  headers: { Authorization: `Bearer ${this.botToken}` },
292
304
  body: JSON.stringify({ channel: to, text: content }),
305
+ signal: options.signal,
293
306
  }, 'Slack');
294
307
  if (result && result.ok === false) throw new Error(`Slack post failed: ${result.error || 'unknown error'}`);
295
308
  return { success: true, ts: result?.ts };
@@ -593,12 +606,13 @@ class MatrixPlatform extends BasePlatform {
593
606
  }
594
607
  }
595
608
 
596
- async sendMessage(to, content) {
609
+ async sendMessage(to, content, options = {}) {
597
610
  if (this.status !== 'connected') throw new Error('Matrix not connected');
598
611
  const txnId = encodeURIComponent(crypto.randomUUID());
599
612
  await this.#matrix(`/rooms/${encodeURIComponent(to)}/send/m.room.message/${txnId}`, {
600
613
  method: 'PUT',
601
614
  body: JSON.stringify({ msgtype: 'm.text', body: content }),
615
+ signal: options.signal,
602
616
  });
603
617
  return { success: true };
604
618
  }
@@ -618,9 +632,9 @@ class BlueBubblesPlatform extends ConfigurableHttpPlatform {
618
632
  return { status: 'connected' };
619
633
  }
620
634
 
621
- async sendMessage(to, content) {
635
+ async sendMessage(to, content, options = {}) {
622
636
  if (this.config.outboundUrl || this.config.webhookUrl) {
623
- return super.sendMessage(to, content);
637
+ return super.sendMessage(to, content, options);
624
638
  }
625
639
  const url = new URL(`${this.serverUrl}${this.config.sendPath || '/api/v1/message/text'}`);
626
640
  url.searchParams.set('guid', to);
@@ -629,6 +643,7 @@ class BlueBubblesPlatform extends ConfigurableHttpPlatform {
629
643
  await fetchJson(url.toString(), {
630
644
  method: 'POST',
631
645
  body: JSON.stringify({ message: content }),
646
+ signal: options.signal,
632
647
  }, this.defaults.label);
633
648
  return { success: true };
634
649
  }
@@ -713,9 +728,9 @@ class SignalPlatform extends ConfigurableHttpPlatform {
713
728
  }
714
729
  }
715
730
 
716
- async sendMessage(to, content) {
731
+ async sendMessage(to, content, options = {}) {
717
732
  if (this.config.outboundUrl || this.config.webhookUrl) {
718
- return super.sendMessage(to, content);
733
+ return super.sendMessage(to, content, options);
719
734
  }
720
735
  await fetchJson(`${this.restUrl}/v2/send`, {
721
736
  method: 'POST',
@@ -724,6 +739,7 @@ class SignalPlatform extends ConfigurableHttpPlatform {
724
739
  number: this.account,
725
740
  recipients: [to],
726
741
  }),
742
+ signal: options.signal,
727
743
  }, 'Signal');
728
744
  return { success: true };
729
745
  }
@@ -743,7 +759,7 @@ class LinePlatform extends ConfigurableHttpPlatform {
743
759
  return { status: 'connected' };
744
760
  }
745
761
 
746
- async sendMessage(to, content) {
762
+ async sendMessage(to, content, options = {}) {
747
763
  const token = requireText(this.config.channelAccessToken || this.config.token, 'LINE channel access token');
748
764
  await fetchJson('https://api.line.me/v2/bot/message/push', {
749
765
  method: 'POST',
@@ -752,6 +768,7 @@ class LinePlatform extends ConfigurableHttpPlatform {
752
768
  to,
753
769
  messages: [{ type: 'text', text: content }],
754
770
  }),
771
+ signal: options.signal,
755
772
  }, 'LINE');
756
773
  return { success: true };
757
774
  }
@@ -815,14 +832,17 @@ class MattermostPlatform extends ConfigurableHttpPlatform {
815
832
  return { status: 'connected' };
816
833
  }
817
834
 
818
- async sendMessage(to, content) {
819
- if (this.config.webhookUrl || this.config.outboundUrl) return super.sendMessage(to, content);
835
+ async sendMessage(to, content, options = {}) {
836
+ if (this.config.webhookUrl || this.config.outboundUrl) {
837
+ return super.sendMessage(to, content, options);
838
+ }
820
839
  const baseUrl = trimTrailingSlash(this.config.baseUrl);
821
840
  const token = requireText(this.config.token, 'Mattermost token');
822
841
  await fetchJson(`${baseUrl}/api/v4/posts`, {
823
842
  method: 'POST',
824
843
  headers: { Authorization: `Bearer ${token}` },
825
844
  body: JSON.stringify({ channel_id: to, message: content }),
845
+ signal: options.signal,
826
846
  }, 'Mattermost');
827
847
  return { success: true };
828
848
  }