neoagent 3.2.1-beta.2 → 3.2.1-beta.3
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 +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- 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/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -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 +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- 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 +5 -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/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +347 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- 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 +8 -41
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- 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/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -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 +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- 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/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 +158 -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/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 +87 -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 +84 -9
- 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/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- 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/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- 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
|
@@ -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 = {}) {
|
|
@@ -119,53 +188,169 @@ class MessagingManager extends EventEmitter {
|
|
|
119
188
|
senderTag: msg.senderTag,
|
|
120
189
|
isGroup: msg.isGroup,
|
|
121
190
|
mediaType: msg.mediaType,
|
|
191
|
+
localMediaPath: msg.localMediaPath || null,
|
|
122
192
|
...(msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : {}),
|
|
123
193
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
194
|
+
const enrichedMsg = {
|
|
195
|
+
agentId,
|
|
196
|
+
platform: platformName,
|
|
197
|
+
chatId: msg.chatId,
|
|
198
|
+
messageId: msg.messageId || null,
|
|
199
|
+
sender: msg.sender,
|
|
200
|
+
senderName: msg.senderName || null,
|
|
201
|
+
senderDisplayName: msg.senderDisplayName || null,
|
|
202
|
+
senderUsername: msg.senderUsername || null,
|
|
203
|
+
senderTag: msg.senderTag || null,
|
|
204
|
+
content: normalizedIncomingContent,
|
|
205
|
+
mediaType: msg.mediaType || null,
|
|
206
|
+
localMediaPath: msg.localMediaPath || null,
|
|
207
|
+
isGroup: msg.isGroup === true,
|
|
208
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
209
|
+
channelContext: Array.isArray(msg.channelContext) ? msg.channelContext.slice(-20) : null,
|
|
210
|
+
channelName: msg.channelName || null,
|
|
211
|
+
groupName: msg.groupName || null,
|
|
212
|
+
guildName: msg.guildName || null,
|
|
213
|
+
roomName: msg.roomName || null,
|
|
214
|
+
metadata: msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : null,
|
|
215
|
+
};
|
|
216
|
+
const queued = enqueueInboundMessage({
|
|
217
|
+
userId,
|
|
218
|
+
agentId,
|
|
219
|
+
platform: platformName,
|
|
220
|
+
platformMessageId: msg.messageId || null,
|
|
221
|
+
chatId: msg.chatId,
|
|
222
|
+
content: normalizedIncomingContent,
|
|
223
|
+
metadata,
|
|
224
|
+
createdAt: enrichedMsg.timestamp,
|
|
225
|
+
payload: enrichedMsg,
|
|
226
|
+
});
|
|
227
|
+
const durableMessage = queued.payload || payloadForInboundJob(queued.job) || enrichedMsg;
|
|
228
|
+
|
|
229
|
+
if (this.isShuttingDown) {
|
|
230
|
+
return durableMessage;
|
|
134
231
|
}
|
|
135
232
|
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
normalizedIncomingContent,
|
|
142
|
-
platformName,
|
|
143
|
-
msg.messageId,
|
|
144
|
-
msg.chatId,
|
|
145
|
-
JSON.stringify(metadata),
|
|
146
|
-
msg.timestamp,
|
|
233
|
+
if (queued.created) {
|
|
234
|
+
this.io.to(`user:${userId}`).emit('messaging:message', durableMessage);
|
|
235
|
+
} else if (!queued.job) {
|
|
236
|
+
console.warn(
|
|
237
|
+
`[Messaging] Duplicate ${platformName} message for user ${userId} predates durable processing state; skipping replay`,
|
|
147
238
|
);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
239
|
+
return durableMessage;
|
|
240
|
+
} else if (queued.job.status !== 'pending') {
|
|
241
|
+
console.warn(
|
|
242
|
+
`[Messaging] Duplicate ${platformName} message for user ${userId} has durable status ${queued.job.status}; skipping replay`,
|
|
243
|
+
);
|
|
244
|
+
return durableMessage;
|
|
153
245
|
}
|
|
154
246
|
|
|
155
|
-
this.
|
|
247
|
+
await this._processInboundJob(queued.job, durableMessage);
|
|
248
|
+
return durableMessage;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async _processInboundJob(job, payload) {
|
|
252
|
+
if (this.isShuttingDown || !job || this.messageHandlers.length === 0) return false;
|
|
253
|
+
if (!claimInboundJob(job.id)) return false;
|
|
254
|
+
let finishTracking;
|
|
255
|
+
const tracked = new Promise((resolve) => {
|
|
256
|
+
finishTracking = resolve;
|
|
257
|
+
});
|
|
258
|
+
this.activeInboundJobs.add(tracked);
|
|
156
259
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
260
|
+
let status = 'completed';
|
|
261
|
+
let failure = null;
|
|
262
|
+
let runId = null;
|
|
263
|
+
try {
|
|
264
|
+
for (const handler of this.messageHandlers) {
|
|
265
|
+
if (this.isShuttingDown) {
|
|
266
|
+
status = 'pending';
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
let handlerResult = await handler(job.user_id, payload);
|
|
270
|
+
if (handlerResult?.completion) handlerResult = await handlerResult.completion;
|
|
271
|
+
const outcome = handlerResult?.outcome || handlerResult || {};
|
|
272
|
+
runId = outcome.runId || runId;
|
|
273
|
+
if (outcome.cancelled === true) {
|
|
274
|
+
status = runId ? 'failed' : 'pending';
|
|
275
|
+
failure = runId
|
|
276
|
+
? 'The server stopped after this inbound agent run began; it will not be replayed automatically.'
|
|
277
|
+
: null;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (outcome.error) {
|
|
281
|
+
status = 'failed';
|
|
282
|
+
failure = outcome.error;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
160
285
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const cancelled = this.isShuttingDown
|
|
288
|
+
|| isAbortError(error, this.lifecycleAbortController.signal);
|
|
289
|
+
status = cancelled && !runId ? 'pending' : 'failed';
|
|
290
|
+
failure = status === 'failed' ? error : null;
|
|
291
|
+
if (!cancelled) {
|
|
292
|
+
console.error('[Messaging] Inbound message handler failed:', error?.message || error);
|
|
165
293
|
}
|
|
166
294
|
}
|
|
167
295
|
|
|
168
|
-
|
|
296
|
+
try {
|
|
297
|
+
settleInboundJob(job.id, status, failure?.message || failure || null);
|
|
298
|
+
return status === 'completed';
|
|
299
|
+
} finally {
|
|
300
|
+
finishTracking();
|
|
301
|
+
this.activeInboundJobs.delete(tracked);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_platformReadyForInboundJob(job) {
|
|
306
|
+
const platform = this.platforms.get(this._key(job.user_id, job.agent_id, job.platform));
|
|
307
|
+
if (!platform) return false;
|
|
308
|
+
try {
|
|
309
|
+
return String(platform.getStatus?.() || platform.status || '').toLowerCase() === 'connected';
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
recoverPendingInbound(filters = {}) {
|
|
316
|
+
if (this.isShuttingDown || this.messageHandlers.length === 0) {
|
|
317
|
+
return Promise.resolve({ recovered: 0, skipped: 0 });
|
|
318
|
+
}
|
|
319
|
+
const key = JSON.stringify({
|
|
320
|
+
userId: filters.userId,
|
|
321
|
+
agentId: filters.agentId,
|
|
322
|
+
platform: filters.platform,
|
|
323
|
+
});
|
|
324
|
+
if (this.activeInboundRecoveries.has(key)) return this.activeInboundRecoveries.get(key);
|
|
325
|
+
|
|
326
|
+
const recovery = Promise.resolve().then(async () => {
|
|
327
|
+
if (!this.inboundJobsReconciled) {
|
|
328
|
+
reconcileInterruptedInboundJobs();
|
|
329
|
+
this.inboundJobsReconciled = true;
|
|
330
|
+
}
|
|
331
|
+
let recovered = 0;
|
|
332
|
+
let skipped = 0;
|
|
333
|
+
for (const job of listPendingInboundJobs(filters)) {
|
|
334
|
+
if (this.isShuttingDown) break;
|
|
335
|
+
if (!this._platformReadyForInboundJob(job)) {
|
|
336
|
+
skipped += 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const payload = payloadForInboundJob(job);
|
|
340
|
+
if (!payload) {
|
|
341
|
+
settleInboundJob(job.id, 'failed', 'Stored inbound message payload is invalid.');
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (await this._processInboundJob(job, payload)) recovered += 1;
|
|
345
|
+
}
|
|
346
|
+
return { recovered, skipped };
|
|
347
|
+
}).finally(() => {
|
|
348
|
+
if (this.activeInboundRecoveries.get(key) === recovery) {
|
|
349
|
+
this.activeInboundRecoveries.delete(key);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
this.activeInboundRecoveries.set(key, recovery);
|
|
353
|
+
return recovery;
|
|
169
354
|
}
|
|
170
355
|
|
|
171
356
|
_agentId(userId, options = {}) {
|
|
@@ -315,6 +500,8 @@ class MessagingManager extends EventEmitter {
|
|
|
315
500
|
}
|
|
316
501
|
|
|
317
502
|
async connectPlatform(userId, platformName, config = {}, options = {}) {
|
|
503
|
+
this._assertRunning();
|
|
504
|
+
throwIfAborted(options.signal, 'Messaging platform connection aborted.');
|
|
318
505
|
const agentId = this._agentId(userId, options);
|
|
319
506
|
config = { ...(config || {}) };
|
|
320
507
|
config.userId = userId;
|
|
@@ -410,6 +597,16 @@ class MessagingManager extends EventEmitter {
|
|
|
410
597
|
this.io.to(`user:${userId}`).emit('messaging:connected', { agentId, platform: platformName });
|
|
411
598
|
db.prepare('UPDATE platform_connections SET status = ?, last_connected = datetime(\'now\') WHERE user_id = ? AND agent_id = ? AND platform = ?')
|
|
412
599
|
.run('connected', userId, agentId, platformName);
|
|
600
|
+
this.emit('platform_connected', { userId, agentId, platform: platformName });
|
|
601
|
+
void this.recoverPendingInbound({
|
|
602
|
+
userId,
|
|
603
|
+
agentId,
|
|
604
|
+
platform: platformName,
|
|
605
|
+
}).catch((error) => {
|
|
606
|
+
if (!this.isShuttingDown) {
|
|
607
|
+
console.error('[Messaging] Inbound recovery failed:', error?.message || error);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
413
610
|
});
|
|
414
611
|
|
|
415
612
|
platform.on('disconnected', (info) => {
|
|
@@ -455,9 +652,13 @@ class MessagingManager extends EventEmitter {
|
|
|
455
652
|
});
|
|
456
653
|
});
|
|
457
654
|
|
|
458
|
-
platform.on('message',
|
|
655
|
+
platform.on('message', (msg) => {
|
|
459
656
|
if (this.isShuttingDown) return;
|
|
460
|
-
|
|
657
|
+
void this.ingestMessage(userId, platformName, msg, { agentId }).catch((error) => {
|
|
658
|
+
if (!this.isShuttingDown) {
|
|
659
|
+
console.error('[Messaging] Failed to persist or dispatch inbound message:', error?.message || error);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
461
662
|
});
|
|
462
663
|
|
|
463
664
|
if (!existingConnection) {
|
|
@@ -468,7 +669,19 @@ class MessagingManager extends EventEmitter {
|
|
|
468
669
|
.run(storedConfig, 'connecting', userId, agentId, platformName);
|
|
469
670
|
}
|
|
470
671
|
|
|
471
|
-
|
|
672
|
+
try {
|
|
673
|
+
await this._runOperation(
|
|
674
|
+
options,
|
|
675
|
+
`${platformName} connection`,
|
|
676
|
+
() => platform.connect(),
|
|
677
|
+
);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (currentPlatform()) {
|
|
680
|
+
this.platforms.delete(key);
|
|
681
|
+
}
|
|
682
|
+
await Promise.resolve(platform.disconnect?.()).catch(() => {});
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
472
685
|
return { status: platform.getStatus() };
|
|
473
686
|
}
|
|
474
687
|
|
|
@@ -489,6 +702,7 @@ class MessagingManager extends EventEmitter {
|
|
|
489
702
|
}
|
|
490
703
|
|
|
491
704
|
async sendMessage(userId, platformName, to, content, mediaPathOrOptions) {
|
|
705
|
+
this._assertRunning();
|
|
492
706
|
const agentId = this._agentId(userId, mediaPathOrOptions || {});
|
|
493
707
|
const key = this._key(userId, agentId, platformName);
|
|
494
708
|
const platform = this.platforms.get(key);
|
|
@@ -505,6 +719,7 @@ class MessagingManager extends EventEmitter {
|
|
|
505
719
|
? sendOptions.metadata
|
|
506
720
|
: null;
|
|
507
721
|
const deliveryKind = sendOptions.deliveryKind || 'final';
|
|
722
|
+
throwIfAborted(sendOptions.signal, 'Message delivery aborted.');
|
|
508
723
|
const normalizedContent = normalizeOutgoingMessageForPlatform(platformName, content, {
|
|
509
724
|
stripNoResponseMarker: false
|
|
510
725
|
});
|
|
@@ -514,7 +729,13 @@ class MessagingManager extends EventEmitter {
|
|
|
514
729
|
return { success: true, suppressed: true };
|
|
515
730
|
}
|
|
516
731
|
|
|
517
|
-
const result = await
|
|
732
|
+
const result = await this._runOperation(
|
|
733
|
+
sendOptions,
|
|
734
|
+
`${platformName} message delivery`,
|
|
735
|
+
(signal) => platform.sendMessage(to, normalizedContent, { ...sendOptions, signal }),
|
|
736
|
+
);
|
|
737
|
+
this._assertRunning();
|
|
738
|
+
throwIfAborted(sendOptions.signal, 'Message delivery aborted.');
|
|
518
739
|
if (result?.success === false) {
|
|
519
740
|
const reason = result.error || result.reason || 'platform rejected the message';
|
|
520
741
|
const error = new Error(`Platform ${platformName} delivery failed: ${reason}`);
|
|
@@ -676,7 +897,7 @@ class MessagingManager extends EventEmitter {
|
|
|
676
897
|
}
|
|
677
898
|
|
|
678
899
|
async restoreConnections() {
|
|
679
|
-
this.
|
|
900
|
+
this._assertRunning();
|
|
680
901
|
const rows = db.prepare(
|
|
681
902
|
"SELECT user_id, agent_id, platform, config FROM platform_connections WHERE status IN ('connected', 'awaiting_qr')"
|
|
682
903
|
).all();
|
|
@@ -715,17 +936,55 @@ class MessagingManager extends EventEmitter {
|
|
|
715
936
|
}
|
|
716
937
|
|
|
717
938
|
async shutdown() {
|
|
939
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
718
940
|
this.isShuttingDown = true;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
941
|
+
this.lifecycleAbortController.abort(messagingShutdownError());
|
|
942
|
+
|
|
943
|
+
this.shutdownPromise = (async () => {
|
|
944
|
+
const activeOperationTasks = Array.from(this.activeOperations, (operation) =>
|
|
945
|
+
waitForBoundedResult(operation, {
|
|
946
|
+
serviceName: 'Messaging operation shutdown',
|
|
947
|
+
timeoutMs: 10000,
|
|
948
|
+
}));
|
|
949
|
+
activeOperationTasks.push(...Array.from(this.activeInboundJobs, (job) =>
|
|
950
|
+
waitForBoundedResult(job, {
|
|
951
|
+
serviceName: 'Messaging inbound job shutdown',
|
|
952
|
+
timeoutMs: 10000,
|
|
953
|
+
})));
|
|
954
|
+
activeOperationTasks.push(...Array.from(this.activeInboundRecoveries.values(), (recovery) =>
|
|
955
|
+
waitForBoundedResult(recovery, {
|
|
956
|
+
serviceName: 'Messaging inbound recovery shutdown',
|
|
957
|
+
timeoutMs: 10000,
|
|
958
|
+
})));
|
|
959
|
+
const disconnectTasks = [];
|
|
960
|
+
for (const platform of this.platforms.values()) {
|
|
961
|
+
if (typeof platform.disconnect === 'function') {
|
|
962
|
+
disconnectTasks.push(waitForBoundedResult(
|
|
963
|
+
Promise.resolve().then(() => platform.disconnect()),
|
|
964
|
+
{
|
|
965
|
+
serviceName: `${platform.name || 'Messaging platform'} disconnect`,
|
|
966
|
+
timeoutMs: 10000,
|
|
967
|
+
},
|
|
968
|
+
));
|
|
969
|
+
}
|
|
724
970
|
}
|
|
725
|
-
}
|
|
726
971
|
|
|
727
|
-
|
|
728
|
-
|
|
972
|
+
const [operationResults, disconnectResults] = await Promise.all([
|
|
973
|
+
Promise.allSettled(activeOperationTasks),
|
|
974
|
+
Promise.allSettled(disconnectTasks),
|
|
975
|
+
]);
|
|
976
|
+
this.platforms.clear();
|
|
977
|
+
return {
|
|
978
|
+
state: 'stopped',
|
|
979
|
+
cancelledOperationCount: operationResults.filter(
|
|
980
|
+
(result) => result.status === 'rejected',
|
|
981
|
+
).length,
|
|
982
|
+
failedDisconnectCount: disconnectResults.filter(
|
|
983
|
+
(result) => result.status === 'rejected',
|
|
984
|
+
).length,
|
|
985
|
+
};
|
|
986
|
+
})();
|
|
987
|
+
return this.shutdownPromise;
|
|
729
988
|
}
|
|
730
989
|
|
|
731
990
|
async makeCall(userId, to, greeting, options = {}) {
|
|
@@ -733,23 +992,39 @@ class MessagingManager extends EventEmitter {
|
|
|
733
992
|
const platform = this.platforms.get(key);
|
|
734
993
|
if (!platform) throw new Error('Telnyx Voice is not connected');
|
|
735
994
|
if (!platform.initiateCall) throw new Error('Telnyx platform does not support outbound calls');
|
|
736
|
-
const result = await
|
|
995
|
+
const result = await this._runOperation(
|
|
996
|
+
options,
|
|
997
|
+
'Telnyx outbound call',
|
|
998
|
+
(signal) => platform.initiateCall(to, greeting, { ...options, signal }),
|
|
999
|
+
);
|
|
737
1000
|
this.io.to(`user:${userId}`).emit('messaging:call_initiated', { platform: 'telnyx', to, callControlId: result.callControlId });
|
|
738
1001
|
return { success: true, ...result };
|
|
739
1002
|
}
|
|
740
1003
|
|
|
741
1004
|
async markRead(userId, platformName, chatId, messageId, options = {}) {
|
|
1005
|
+
this._assertRunning();
|
|
742
1006
|
const key = this._key(userId, this._agentId(userId, options), platformName);
|
|
743
1007
|
const platform = this.platforms.get(key);
|
|
744
1008
|
if (!platform?.markRead) return;
|
|
745
|
-
return
|
|
1009
|
+
return this._runOperation(
|
|
1010
|
+
options,
|
|
1011
|
+
`${platformName} read receipt`,
|
|
1012
|
+
(signal) => platform.markRead(chatId, messageId, { ...options, signal }),
|
|
1013
|
+
15000,
|
|
1014
|
+
);
|
|
746
1015
|
}
|
|
747
1016
|
|
|
748
1017
|
async sendTyping(userId, platformName, chatId, isTyping, options = {}) {
|
|
1018
|
+
this._assertRunning();
|
|
749
1019
|
const key = this._key(userId, this._agentId(userId, options), platformName);
|
|
750
1020
|
const platform = this.platforms.get(key);
|
|
751
1021
|
if (!platform?.sendTyping) return;
|
|
752
|
-
return
|
|
1022
|
+
return this._runOperation(
|
|
1023
|
+
options,
|
|
1024
|
+
`${platformName} typing indicator`,
|
|
1025
|
+
(signal) => platform.sendTyping(chatId, isTyping, { ...options, signal }),
|
|
1026
|
+
15000,
|
|
1027
|
+
);
|
|
753
1028
|
}
|
|
754
1029
|
|
|
755
1030
|
/**
|
|
@@ -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
|
};
|