neoagent 3.2.1-beta.1 → 3.2.1-beta.10

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 (172) 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 +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -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 +11 -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 +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  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/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -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
  }
@@ -2,6 +2,56 @@
2
2
 
3
3
  const { getErrorMessage } = require('../bootstrap_helpers');
4
4
 
5
+ function completionResult(outcome, cancelled = false) {
6
+ return {
7
+ runId: outcome?.runId || null,
8
+ result: outcome?.result || null,
9
+ error: outcome?.error || null,
10
+ cancelled,
11
+ };
12
+ }
13
+
14
+ function settleWaiters(item, result) {
15
+ for (const resolve of item?.waiters || []) resolve(result);
16
+ if (item?.waiters) item.waiters = [];
17
+ }
18
+
19
+ function cancelPending(queue) {
20
+ const result = completionResult(null, true);
21
+ for (const item of queue.pending.splice(0)) settleWaiters(item, result);
22
+ }
23
+
24
+ function queuedResult(queue, msg) {
25
+ let resolveCompletion;
26
+ const completion = new Promise((resolve) => {
27
+ resolveCompletion = resolve;
28
+ });
29
+ const last = queue.pending[queue.pending.length - 1];
30
+ if (
31
+ last
32
+ && last.message.platform === msg.platform
33
+ && last.message.chatId === msg.chatId
34
+ && String(last.message.sender || '') === String(msg.sender || '')
35
+ ) {
36
+ last.message.content += `\n${msg.content}`;
37
+ last.message.messageId = msg.messageId;
38
+ last.message.inboundJobIds = Array.from(new Set([
39
+ ...(last.message.inboundJobIds || []),
40
+ ...(msg.inboundJobIds || []),
41
+ msg.inboundJobId,
42
+ ].filter(Boolean)));
43
+ last.waiters.push(resolveCompletion);
44
+ } else {
45
+ queue.pending.push({
46
+ message: { ...msg },
47
+ waiters: [resolveCompletion],
48
+ });
49
+ }
50
+ const result = { queued: true };
51
+ Object.defineProperty(result, 'completion', { value: completion });
52
+ return result;
53
+ }
54
+
5
55
  async function processInboundQueue({
6
56
  userQueues,
7
57
  userId,
@@ -12,79 +62,83 @@ async function processInboundQueue({
12
62
  const agentId = msg.agentId || null;
13
63
  const queueKey = `${userId}:${agentId || 'main'}`;
14
64
  if (!userQueues[queueKey]) {
15
- userQueues[queueKey] = { running: false, pending: [], cancelRequested: false };
65
+ userQueues[queueKey] = {
66
+ running: false,
67
+ pending: [],
68
+ cancelRequested: false,
69
+ cancelPending() {
70
+ cancelPending(this);
71
+ },
72
+ };
16
73
  }
17
74
  const queue = userQueues[queueKey];
18
75
 
19
76
  if (queue.cancelRequested && !queue.running) {
20
- queue.pending = [];
77
+ cancelPending(queue);
21
78
  queue.cancelRequested = false;
22
79
  }
23
80
 
24
81
  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 };
82
+ return queuedResult(queue, msg);
38
83
  }
39
84
 
40
85
  queue.running = true;
41
- let currentMessage = msg;
86
+ let currentItem = { message: msg, waiters: [] };
42
87
  let processedCount = 0;
43
88
  let failedCount = 0;
44
89
  let cancelled = false;
90
+ let initialOutcome = null;
45
91
 
46
92
  try {
47
- while (currentMessage) {
93
+ while (currentItem) {
48
94
  let outcome;
49
95
  try {
50
- outcome = await executeMessage(currentMessage);
96
+ outcome = await executeMessage(currentItem.message);
51
97
  } catch (error) {
52
98
  outcome = { runId: null, result: null, error };
53
99
  }
54
100
  processedCount += 1;
101
+ const itemCancelled = queue.cancelRequested;
102
+ const itemResult = completionResult(outcome, itemCancelled);
103
+ if (!initialOutcome) initialOutcome = itemResult;
104
+ settleWaiters(currentItem, itemResult);
55
105
 
56
- if (outcome?.error) {
106
+ if (outcome?.error && !queue.cancelRequested) {
57
107
  failedCount += 1;
58
108
  await notifyProcessingError(onProcessingError, {
59
109
  error: outcome.error,
60
110
  runId: outcome.runId,
61
111
  userId,
62
- failedMessage: currentMessage
112
+ failedMessage: currentItem.message
63
113
  });
64
114
  }
65
115
 
66
116
  if (queue.cancelRequested) {
67
- queue.pending = [];
117
+ cancelPending(queue);
68
118
  cancelled = true;
69
119
  break;
70
120
  }
71
121
 
72
- currentMessage = queue.pending.shift() || null;
122
+ currentItem = queue.pending.shift() || null;
73
123
  }
74
124
  } finally {
75
125
  queue.running = false;
76
- queue.pending = [];
126
+ cancelPending(queue);
77
127
  queue.cancelRequested = false;
78
128
  if (userQueues[queueKey] === queue) {
79
129
  delete userQueues[queueKey];
80
130
  }
81
131
  }
82
132
 
83
- return {
133
+ const result = {
84
134
  processedCount,
85
135
  failedCount,
86
136
  cancelled
87
137
  };
138
+ Object.defineProperty(result, 'outcome', {
139
+ value: initialOutcome || completionResult(null, cancelled),
140
+ });
141
+ return result;
88
142
  }
89
143
 
90
144
  async function notifyProcessingError(handler, details) {
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const db = require('../../db/database');
5
+
6
+ const MAX_INBOUND_PAYLOAD_BYTES = 1024 * 1024;
7
+ const JOB_STATUSES = new Set(['pending', 'processing', 'completed', 'failed']);
8
+
9
+ function encodePayload(payload) {
10
+ const encoded = JSON.stringify(payload);
11
+ if (Buffer.byteLength(encoded, 'utf8') > MAX_INBOUND_PAYLOAD_BYTES) {
12
+ const error = new Error('Inbound messaging payload exceeds the 1 MiB durability limit.');
13
+ error.code = 'MESSAGING_INBOUND_PAYLOAD_TOO_LARGE';
14
+ throw error;
15
+ }
16
+ return encoded;
17
+ }
18
+
19
+ function decodePayload(value) {
20
+ try {
21
+ const payload = JSON.parse(String(value || ''));
22
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
23
+ ? payload
24
+ : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function getJobByMessageId(messageId) {
31
+ return db.prepare(
32
+ 'SELECT * FROM messaging_inbound_jobs WHERE message_id = ?',
33
+ ).get(messageId) || null;
34
+ }
35
+
36
+ function enqueueInboundMessage({
37
+ userId,
38
+ agentId,
39
+ platform,
40
+ platformMessageId,
41
+ chatId,
42
+ content,
43
+ metadata,
44
+ createdAt,
45
+ payload,
46
+ }) {
47
+ return db.transaction(() => {
48
+ if (platformMessageId) {
49
+ const existing = db.prepare(
50
+ `SELECT id
51
+ FROM messages
52
+ WHERE user_id = ? AND platform = ? AND platform_msg_id = ? AND role = 'user'
53
+ LIMIT 1`,
54
+ ).get(userId, platform, platformMessageId);
55
+ if (existing) {
56
+ return {
57
+ created: false,
58
+ job: getJobByMessageId(existing.id),
59
+ messageId: existing.id,
60
+ };
61
+ }
62
+ }
63
+
64
+ const jobId = randomUUID();
65
+ const durablePayload = {
66
+ ...payload,
67
+ inboundJobId: jobId,
68
+ inboundJobIds: [jobId],
69
+ };
70
+ const insert = db.prepare(
71
+ `INSERT INTO messages (
72
+ user_id, agent_id, role, content, platform, platform_msg_id,
73
+ platform_chat_id, metadata, created_at
74
+ ) VALUES (?, ?, 'user', ?, ?, ?, ?, ?, ?)`,
75
+ ).run(
76
+ userId,
77
+ agentId,
78
+ content,
79
+ platform,
80
+ platformMessageId || null,
81
+ chatId,
82
+ metadata ? JSON.stringify(metadata) : null,
83
+ createdAt || new Date().toISOString(),
84
+ );
85
+ const messageId = Number(insert.lastInsertRowid);
86
+ db.prepare(
87
+ `INSERT INTO messaging_inbound_jobs (
88
+ id, message_id, user_id, agent_id, platform, platform_msg_id,
89
+ platform_chat_id, payload_json, status
90
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')`,
91
+ ).run(
92
+ jobId,
93
+ messageId,
94
+ userId,
95
+ agentId,
96
+ platform,
97
+ platformMessageId || null,
98
+ chatId,
99
+ encodePayload(durablePayload),
100
+ );
101
+ return {
102
+ created: true,
103
+ job: getJobByMessageId(messageId),
104
+ messageId,
105
+ payload: durablePayload,
106
+ };
107
+ })();
108
+ }
109
+
110
+ function claimInboundJob(jobId) {
111
+ const result = db.prepare(
112
+ `UPDATE messaging_inbound_jobs
113
+ SET status = 'processing',
114
+ attempts = attempts + 1,
115
+ last_error = NULL,
116
+ updated_at = datetime('now')
117
+ WHERE id = ? AND status = 'pending'`,
118
+ ).run(jobId);
119
+ return result.changes === 1;
120
+ }
121
+
122
+ function settleInboundJob(jobId, status, error = null) {
123
+ if (!JOB_STATUSES.has(status) || status === 'processing') {
124
+ throw new Error(`Invalid inbound messaging job status: ${status}`);
125
+ }
126
+ db.prepare(
127
+ `UPDATE messaging_inbound_jobs
128
+ SET status = ?,
129
+ last_error = ?,
130
+ completed_at = CASE WHEN ? IN ('completed', 'failed') THEN datetime('now') ELSE NULL END,
131
+ updated_at = datetime('now')
132
+ WHERE id = ?`,
133
+ ).run(
134
+ status,
135
+ error ? String(error).slice(0, 4000) : null,
136
+ status,
137
+ jobId,
138
+ );
139
+ }
140
+
141
+ function attachRunToInboundJobs(jobIds, runId) {
142
+ const ids = Array.from(new Set(
143
+ (Array.isArray(jobIds) ? jobIds : [jobIds])
144
+ .map((value) => String(value || '').trim())
145
+ .filter(Boolean),
146
+ ));
147
+ if (!ids.length || !runId) return 0;
148
+ const update = db.prepare(
149
+ `UPDATE messaging_inbound_jobs
150
+ SET run_id = ?, updated_at = datetime('now')
151
+ WHERE id = ? AND status = 'processing'`,
152
+ );
153
+ return db.transaction(() => ids.reduce(
154
+ (count, id) => count + update.run(runId, id).changes,
155
+ 0,
156
+ ))();
157
+ }
158
+
159
+ function reconcileInterruptedInboundJobs() {
160
+ return db.transaction(() => {
161
+ const completed = db.prepare(
162
+ `UPDATE messaging_inbound_jobs
163
+ SET status = 'completed', completed_at = datetime('now'), updated_at = datetime('now')
164
+ WHERE status = 'processing'
165
+ AND run_id IN (SELECT id FROM agent_runs WHERE status = 'completed')`,
166
+ ).run().changes;
167
+ const failed = db.prepare(
168
+ `UPDATE messaging_inbound_jobs
169
+ SET status = 'failed',
170
+ last_error = COALESCE(last_error, 'The server restarted after this agent run began; it will not be replayed automatically.'),
171
+ completed_at = datetime('now'),
172
+ updated_at = datetime('now')
173
+ WHERE status = 'processing'
174
+ AND run_id IN (SELECT id FROM agent_runs)`,
175
+ ).run().changes;
176
+ const pending = db.prepare(
177
+ `UPDATE messaging_inbound_jobs
178
+ SET status = 'pending',
179
+ last_error = NULL,
180
+ updated_at = datetime('now')
181
+ WHERE status = 'processing'
182
+ AND (run_id IS NULL OR run_id NOT IN (SELECT id FROM agent_runs))`,
183
+ ).run().changes;
184
+ return { completed, failed, pending };
185
+ })();
186
+ }
187
+
188
+ function listPendingInboundJobs(filters = {}) {
189
+ const clauses = ["status = 'pending'"];
190
+ const values = [];
191
+ for (const [column, value] of [
192
+ ['user_id', filters.userId],
193
+ ['agent_id', filters.agentId],
194
+ ['platform', filters.platform],
195
+ ]) {
196
+ if (value === undefined) continue;
197
+ if (value === null) clauses.push(`${column} IS NULL`);
198
+ else {
199
+ clauses.push(`${column} = ?`);
200
+ values.push(value);
201
+ }
202
+ }
203
+ const limit = Math.max(1, Math.min(500, Number(filters.limit) || 100));
204
+ return db.prepare(
205
+ `SELECT * FROM messaging_inbound_jobs
206
+ WHERE ${clauses.join(' AND ')}
207
+ ORDER BY created_at ASC, id ASC
208
+ LIMIT ?`,
209
+ ).all(...values, limit);
210
+ }
211
+
212
+ function payloadForInboundJob(job) {
213
+ return decodePayload(job?.payload_json);
214
+ }
215
+
216
+ module.exports = {
217
+ attachRunToInboundJobs,
218
+ claimInboundJob,
219
+ enqueueInboundMessage,
220
+ listPendingInboundJobs,
221
+ payloadForInboundJob,
222
+ reconcileInterruptedInboundJobs,
223
+ settleInboundJob,
224
+ };