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