neoagent 3.2.1-beta.0 → 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.
- package/docs/agent-run-lifecycle.md +10 -0
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +11 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +496 -166
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +4 -4
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- 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
|
|
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
|
-
|
|
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)
|
|
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] = {
|
|
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
|
|
77
|
+
cancelPending(queue);
|
|
21
78
|
queue.cancelRequested = false;
|
|
22
79
|
}
|
|
23
80
|
|
|
24
81
|
if (queue.running) {
|
|
25
|
-
|
|
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
|
|
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 (
|
|
93
|
+
while (currentItem) {
|
|
48
94
|
let outcome;
|
|
49
95
|
try {
|
|
50
|
-
outcome = await executeMessage(
|
|
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:
|
|
112
|
+
failedMessage: currentItem.message
|
|
63
113
|
});
|
|
64
114
|
}
|
|
65
115
|
|
|
66
116
|
if (queue.cancelRequested) {
|
|
67
|
-
queue
|
|
117
|
+
cancelPending(queue);
|
|
68
118
|
cancelled = true;
|
|
69
119
|
break;
|
|
70
120
|
}
|
|
71
121
|
|
|
72
|
-
|
|
122
|
+
currentItem = queue.pending.shift() || null;
|
|
73
123
|
}
|
|
74
124
|
} finally {
|
|
75
125
|
queue.running = false;
|
|
76
|
-
queue
|
|
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
|
-
|
|
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
|
+
};
|