neoagent 2.4.1-beta.34 → 2.4.1-beta.35

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.
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const { resolveAgentId } = require('../agents/manager');
4
+ const {
5
+ buildCoverageForConnection,
6
+ sourceTypesForConnection,
7
+ } = require('./ingestion_support');
8
+
9
+ function listConnectionStatuses(service, userId, { agentId = null } = {}) {
10
+ const scopedAgentId = resolveAgentId(userId, agentId);
11
+ const connections = service.db.prepare(
12
+ `SELECT *
13
+ FROM integration_connections
14
+ WHERE user_id = ? AND agent_id = ?
15
+ ORDER BY updated_at DESC, id DESC`,
16
+ ).all(userId, scopedAgentId);
17
+ const jobs = service.memoryManager.listIngestionJobs(
18
+ userId,
19
+ { agentId: scopedAgentId, limit: 100 },
20
+ );
21
+ return connections.map((connection) => {
22
+ const latestJob = jobs.find(
23
+ (job) => Number(job.connectionId || 0) === Number(connection.id),
24
+ );
25
+ return {
26
+ connectionId: connection.id,
27
+ providerKey: connection.provider_key,
28
+ appKey: connection.app_key,
29
+ accountEmail: connection.account_email || null,
30
+ ...buildCoverageForConnection(connection, latestJob),
31
+ };
32
+ });
33
+ }
34
+
35
+ function decorateProviderSnapshot(service, snapshot, userId, agentId = null) {
36
+ if (!snapshot || typeof snapshot !== 'object') return snapshot;
37
+ const scopedAgentId = resolveAgentId(userId, agentId);
38
+ const connections = service.db.prepare(
39
+ `SELECT *
40
+ FROM integration_connections
41
+ WHERE user_id = ? AND agent_id = ? AND provider_key = ?`,
42
+ ).all(userId, scopedAgentId, snapshot.provider);
43
+ const jobs = service.memoryManager.listIngestionJobs(userId, {
44
+ agentId: scopedAgentId,
45
+ providerKey: snapshot.provider,
46
+ limit: 100,
47
+ });
48
+ const latestJobForConnection = (connectionId) =>
49
+ jobs.find((job) => Number(job.connectionId || 0) === Number(connectionId));
50
+
51
+ const connectionCoverage = connections.map((connection) => ({
52
+ connectionId: connection.id,
53
+ appKey: connection.app_key,
54
+ accountEmail: connection.account_email || null,
55
+ ...buildCoverageForConnection(connection, latestJobForConnection(connection.id)),
56
+ }));
57
+ const providerDomains = Array.from(
58
+ new Set(connectionCoverage.flatMap((item) => item.dataDomains || [])),
59
+ );
60
+ const providerJob = jobs[0] || null;
61
+ const decorated = {
62
+ ...snapshot,
63
+ memoryCoverage: {
64
+ supported: providerDomains.length > 0,
65
+ contributesToMemory: providerDomains.length > 0,
66
+ contributesToTaskExecution: Boolean(snapshot.connection?.connected),
67
+ status: providerJob?.status || (providerDomains.length > 0 ? 'ready' : 'not_supported'),
68
+ dataDomains: providerDomains,
69
+ documentCount: connectionCoverage.reduce(
70
+ (sum, item) => sum + Number(item.documentCount || 0),
71
+ 0,
72
+ ),
73
+ lastRefreshAt: providerJob?.completedAt || providerJob?.updatedAt || null,
74
+ nextRefreshAt: providerJob?.nextSyncAt || null,
75
+ error: providerJob?.error || null,
76
+ },
77
+ };
78
+
79
+ decorated.apps = (snapshot.apps || []).map((app) => {
80
+ const appConnections = connectionCoverage.filter((item) => item.appKey === app.id);
81
+ const dataDomains = Array.from(new Set([
82
+ ...sourceTypesForConnection(snapshot.provider, app.id),
83
+ ...appConnections.flatMap((item) => item.dataDomains || []),
84
+ ]));
85
+ const latest = appConnections[0] || null;
86
+ return {
87
+ ...app,
88
+ memoryCoverage: {
89
+ supported: dataDomains.length > 0,
90
+ contributesToMemory: dataDomains.length > 0 && app.connection?.connected === true,
91
+ contributesToTaskExecution: app.connection?.connected === true,
92
+ status: latest?.status || (dataDomains.length > 0 ? 'ready' : 'not_supported'),
93
+ dataDomains,
94
+ documentCount: appConnections.reduce(
95
+ (sum, item) => sum + Number(item.documentCount || 0),
96
+ 0,
97
+ ),
98
+ lastRefreshAt: latest?.lastRefreshAt || null,
99
+ nextRefreshAt: latest?.nextRefreshAt || null,
100
+ error: latest?.error || null,
101
+ },
102
+ accounts: (app.accounts || []).map((account) => {
103
+ const accountCoverage = appConnections.find(
104
+ (item) => Number(item.connectionId) === Number(account.connectionId),
105
+ );
106
+ return {
107
+ ...account,
108
+ memoryCoverage: accountCoverage || {
109
+ supported: dataDomains.length > 0,
110
+ contributesToMemory: false,
111
+ contributesToTaskExecution: app.connection?.connected === true,
112
+ status: dataDomains.length > 0 ? 'ready' : 'not_supported',
113
+ dataDomains,
114
+ documentCount: 0,
115
+ lastRefreshAt: null,
116
+ nextRefreshAt: null,
117
+ error: null,
118
+ },
119
+ };
120
+ }),
121
+ };
122
+ });
123
+ return decorated;
124
+ }
125
+
126
+ module.exports = {
127
+ decorateProviderSnapshot,
128
+ listConnectionStatuses,
129
+ };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const { v4: uuidv4 } = require('uuid');
4
+ const { resolveAgentId } = require('../agents/manager');
5
+ const {
6
+ SOURCE_MEMORY_CATEGORIES,
7
+ getFreshnessPolicy,
8
+ nextSyncFromPolicy,
9
+ normalizeDocument,
10
+ normalizeSourceType,
11
+ parseJsonObject,
12
+ safeTrim,
13
+ } = require('./ingestion_support');
14
+
15
+ async function ingestDocuments(service, userId, documents = [], options = {}) {
16
+ const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
17
+ const normalizedDocs = (Array.isArray(documents) ? documents : [])
18
+ .map((document) => normalizeDocument(document, {
19
+ sourceType: options.sourceType,
20
+ providerKey: options.providerKey,
21
+ connectionId: options.connectionId,
22
+ sourceAccount: options.sourceAccount,
23
+ }));
24
+ const sourceType = normalizeSourceType(options.sourceType || normalizedDocs[0]?.sourceType);
25
+ const policy = getFreshnessPolicy(sourceType);
26
+ const jobId = uuidv4();
27
+ const connectionId = Number.isInteger(Number(options.connectionId))
28
+ ? Number(options.connectionId)
29
+ : 0;
30
+
31
+ service.memoryManager.recordIngestionJob(userId, {
32
+ id: jobId,
33
+ sourceType,
34
+ providerKey: safeTrim(options.providerKey, 80),
35
+ connectionId,
36
+ status: 'running',
37
+ freshnessPolicy: policy,
38
+ metadata: parseJsonObject(options.metadata, {}),
39
+ documentCount: 0,
40
+ nextSyncAt: nextSyncFromPolicy(policy),
41
+ }, { agentId });
42
+
43
+ const documentIds = [];
44
+ const memoryIds = [];
45
+ try {
46
+ for (const document of normalizedDocs) {
47
+ const documentId = service.memoryManager.upsertIngestionDocument(
48
+ userId,
49
+ document,
50
+ { agentId },
51
+ );
52
+ documentIds.push(documentId);
53
+ const memoryId = await service.memoryManager.saveMemory(
54
+ userId,
55
+ `${document.title}: ${document.summary || document.content}`,
56
+ SOURCE_MEMORY_CATEGORIES[document.normalizedType]
57
+ || SOURCE_MEMORY_CATEGORIES[document.sourceType]
58
+ || 'episodic',
59
+ document.salience,
60
+ {
61
+ agentId,
62
+ staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
63
+ sourceRef: {
64
+ sourceType: 'memory_ingestion',
65
+ sourceId: document.externalObjectId,
66
+ sourceLabel: document.title,
67
+ },
68
+ scope: {
69
+ scopeType: 'agent',
70
+ scopeId: agentId,
71
+ },
72
+ metadata: {
73
+ ingestionJobId: jobId,
74
+ ingestionDocumentId: documentId,
75
+ providerKey: document.providerKey || null,
76
+ connectionId: document.connectionId || null,
77
+ sourceType: document.sourceType,
78
+ },
79
+ },
80
+ );
81
+ if (memoryId) memoryIds.push(memoryId);
82
+ }
83
+
84
+ service.memoryManager.recordIngestionJob(userId, {
85
+ id: jobId,
86
+ sourceType,
87
+ providerKey: safeTrim(options.providerKey, 80),
88
+ connectionId,
89
+ status: 'completed',
90
+ freshnessPolicy: policy,
91
+ summary: { documentIds, memoryIds },
92
+ metadata: parseJsonObject(options.metadata, {}),
93
+ documentCount: documentIds.length,
94
+ completedAt: new Date().toISOString(),
95
+ nextSyncAt: nextSyncFromPolicy(policy),
96
+ }, { agentId });
97
+
98
+ const knowledgeViews = service.memoryManager.materializeKnowledgeViews(userId, { agentId });
99
+ return {
100
+ jobId,
101
+ status: 'completed',
102
+ documentIds,
103
+ memoryIds,
104
+ knowledgeViews,
105
+ };
106
+ } catch (err) {
107
+ service.memoryManager.recordIngestionJob(userId, {
108
+ id: jobId,
109
+ sourceType,
110
+ providerKey: safeTrim(options.providerKey, 80),
111
+ connectionId,
112
+ status: 'failed',
113
+ freshnessPolicy: policy,
114
+ documentCount: documentIds.length,
115
+ error: err.message,
116
+ completedAt: new Date().toISOString(),
117
+ nextSyncAt: nextSyncFromPolicy(policy),
118
+ }, { agentId });
119
+ throw err;
120
+ }
121
+ }
122
+
123
+ module.exports = { ingestDocuments };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const { compactTextPayload } = require('../ai/preModelCompaction');
4
+
5
+ const SOURCE_TYPES = Object.freeze([
6
+ 'email',
7
+ 'calendar',
8
+ 'chat',
9
+ 'docs',
10
+ 'tickets',
11
+ 'repos',
12
+ 'files',
13
+ 'crm',
14
+ 'payments',
15
+ 'notes',
16
+ ]);
17
+
18
+ const FRESHNESS_POLICIES = Object.freeze({
19
+ email: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
20
+ calendar: Object.freeze({ intervalMinutes: 180, staleAfterDays: 30 }),
21
+ chat: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
22
+ docs: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
23
+ tickets: Object.freeze({ intervalMinutes: 120, staleAfterDays: 30 }),
24
+ repos: Object.freeze({ intervalMinutes: 180, staleAfterDays: 45 }),
25
+ files: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
26
+ crm: Object.freeze({ intervalMinutes: 240, staleAfterDays: 45 }),
27
+ payments: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
28
+ notes: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
29
+ });
30
+
31
+ const SOURCE_MEMORY_CATEGORIES = Object.freeze({
32
+ email: 'episodic',
33
+ calendar: 'events',
34
+ chat: 'episodic',
35
+ docs: 'projects',
36
+ tickets: 'tasks',
37
+ repos: 'projects',
38
+ files: 'episodic',
39
+ crm: 'contacts',
40
+ payments: 'events',
41
+ notes: 'episodic',
42
+ });
43
+
44
+ const INTEGRATION_SOURCE_TYPES = Object.freeze({
45
+ google_workspace: Object.freeze({
46
+ gmail: Object.freeze(['email']),
47
+ calendar: Object.freeze(['calendar']),
48
+ drive: Object.freeze(['files']),
49
+ docs: Object.freeze(['docs']),
50
+ sheets: Object.freeze(['docs', 'files']),
51
+ }),
52
+ microsoft_365: Object.freeze({
53
+ outlook: Object.freeze(['email']),
54
+ calendar: Object.freeze(['calendar']),
55
+ onedrive: Object.freeze(['files']),
56
+ teams: Object.freeze(['chat']),
57
+ }),
58
+ github: Object.freeze({
59
+ repos: Object.freeze(['repos', 'tickets']),
60
+ }),
61
+ slack: Object.freeze({
62
+ slack: Object.freeze(['chat']),
63
+ }),
64
+ whatsapp: Object.freeze({
65
+ personal: Object.freeze(['chat']),
66
+ }),
67
+ notion: Object.freeze({
68
+ notion: Object.freeze(['notes', 'docs']),
69
+ }),
70
+ trello: Object.freeze({
71
+ trello: Object.freeze(['tickets']),
72
+ }),
73
+ });
74
+
75
+ function safeTrim(value, maxLength = 240) {
76
+ return String(value || '').trim().slice(0, maxLength);
77
+ }
78
+
79
+ function parseJsonObject(value, fallback = {}) {
80
+ if (!value) return { ...fallback };
81
+ if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
82
+ try {
83
+ const parsed = JSON.parse(String(value));
84
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
85
+ ? parsed
86
+ : { ...fallback };
87
+ } catch {
88
+ return { ...fallback };
89
+ }
90
+ }
91
+
92
+ function normalizeSourceType(value) {
93
+ const normalized = String(value || '').trim().toLowerCase();
94
+ return SOURCE_TYPES.includes(normalized) ? normalized : 'notes';
95
+ }
96
+
97
+ function getFreshnessPolicy(sourceType) {
98
+ const normalized = normalizeSourceType(sourceType);
99
+ return { ...FRESHNESS_POLICIES[normalized] };
100
+ }
101
+
102
+ function nextSyncFromPolicy(policy, now = Date.now()) {
103
+ const intervalMinutes = Math.max(15, Number(policy?.intervalMinutes) || 360);
104
+ return new Date(now + intervalMinutes * 60 * 1000).toISOString();
105
+ }
106
+
107
+ function normalizeDocument(raw = {}, defaults = {}) {
108
+ const sourceType = normalizeSourceType(raw.sourceType || defaults.sourceType);
109
+ const externalObjectId = safeTrim(raw.externalObjectId || raw.id || raw.sourceId, 180);
110
+ const contentSource = raw.content ?? raw.text ?? raw.body ?? '';
111
+ const content = safeTrim(contentSource, 12000);
112
+ if (!externalObjectId || !content) {
113
+ throw new Error('Normalized memory documents require externalObjectId and content.');
114
+ }
115
+ const compacted = compactTextPayload(content, { maxChars: 2400, maxLines: 50 });
116
+ return {
117
+ sourceType,
118
+ normalizedType: normalizeSourceType(raw.normalizedType || raw.memoryType || sourceType),
119
+ providerKey: safeTrim(raw.providerKey || defaults.providerKey, 80),
120
+ connectionId: Number.isInteger(Number(raw.connectionId ?? defaults.connectionId))
121
+ ? Number(raw.connectionId ?? defaults.connectionId)
122
+ : 0,
123
+ externalObjectId,
124
+ sourceAccount: safeTrim(raw.sourceAccount || defaults.sourceAccount, 180),
125
+ title: safeTrim(raw.title || raw.subject || raw.name || externalObjectId, 220),
126
+ content,
127
+ summary: safeTrim(raw.summary || compacted.text, 1200),
128
+ salience: Math.max(1, Math.min(10, Number(raw.salience) || 5)),
129
+ sourceTimestamp: safeTrim(raw.sourceTimestamp || raw.updatedAt || raw.createdAt, 80) || null,
130
+ metadata: {
131
+ ...parseJsonObject(raw.metadata, {}),
132
+ compaction: compacted.metrics,
133
+ },
134
+ payload: parseJsonObject(raw.payload, {}),
135
+ };
136
+ }
137
+
138
+ function sourceTypesForConnection(providerKey, appKey) {
139
+ const providerMap = INTEGRATION_SOURCE_TYPES[String(providerKey || '').trim()] || {};
140
+ return Array.from(providerMap[String(appKey || '').trim()] || []);
141
+ }
142
+
143
+ function buildCoverageForConnection(connection, latestJob = null) {
144
+ const dataDomains = sourceTypesForConnection(connection.provider_key, connection.app_key);
145
+ const supported = dataDomains.length > 0;
146
+ return {
147
+ supported,
148
+ contributesToMemory: supported,
149
+ contributesToTaskExecution: String(connection.status || '') === 'connected',
150
+ status: latestJob?.status || (supported ? 'ready' : 'not_supported'),
151
+ dataDomains,
152
+ lastRefreshAt: latestJob?.completedAt || latestJob?.updatedAt || null,
153
+ nextRefreshAt: latestJob?.nextSyncAt || null,
154
+ documentCount: Number(latestJob?.documentCount || 0),
155
+ error: latestJob?.error || null,
156
+ };
157
+ }
158
+
159
+ module.exports = {
160
+ FRESHNESS_POLICIES,
161
+ SOURCE_MEMORY_CATEGORIES,
162
+ SOURCE_TYPES,
163
+ buildCoverageForConnection,
164
+ getFreshnessPolicy,
165
+ nextSyncFromPolicy,
166
+ normalizeDocument,
167
+ normalizeSourceType,
168
+ parseJsonObject,
169
+ safeTrim,
170
+ sourceTypesForConnection,
171
+ };
@@ -18,9 +18,12 @@ const {
18
18
  buildVoiceMessagingRunOptions,
19
19
  isVoiceLikeMessage,
20
20
  } = require('../voice/runtime');
21
+ const { getErrorMessage } = require('../bootstrap_helpers');
22
+ const { processInboundQueue } = require('./inbound_queue');
23
+ const { startTypingKeepalive } = require('./typing_keepalive');
21
24
 
22
25
  function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
23
- const userQueues = {};
26
+ const userQueues = Object.create(null);
24
27
  app.locals.userQueues = userQueues;
25
28
 
26
29
  messagingManager.registerHandler(async (userId, msg) => {
@@ -100,62 +103,85 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
100
103
  messagingManager,
101
104
  agentEngine,
102
105
  userId,
103
- msg
106
+ msg,
107
+ onProcessingError: ({ error, runId, failedMessage }) => {
108
+ const errorMessage = getErrorMessage(error);
109
+ console.error(
110
+ `[MessagingAutomation] Agent run failed platform=${failedMessage.platform} user=${userId}:`,
111
+ errorMessage
112
+ );
113
+ io.to(`user:${userId}`).emit('messaging:error', {
114
+ error: `NeoAgent could not finish the ${failedMessage.platform} request. Check the run history for details.`,
115
+ platform: failedMessage.platform,
116
+ chatId: failedMessage.chatId,
117
+ runId
118
+ });
119
+ }
104
120
  });
105
121
  });
106
122
  }
107
123
 
108
124
  async function processQueuedMessage({
109
125
  userQueues,
126
+ messagingManager,
127
+ agentEngine,
128
+ userId,
129
+ msg,
130
+ onProcessingError = null
131
+ }) {
132
+ return processInboundQueue({
133
+ userQueues,
134
+ userId,
135
+ msg,
136
+ executeMessage: (queuedMessage) =>
137
+ executeQueuedMessage({
138
+ messagingManager,
139
+ agentEngine,
140
+ userId,
141
+ msg: queuedMessage
142
+ }),
143
+ onProcessingError
144
+ });
145
+ }
146
+
147
+ async function executeQueuedMessage({
110
148
  messagingManager,
111
149
  agentEngine,
112
150
  userId,
113
151
  msg
114
152
  }) {
115
153
  const agentId = msg.agentId || null;
116
- const queueKey = `${userId}:${agentId || 'main'}`;
117
- if (!userQueues[queueKey]) {
118
- userQueues[queueKey] = { running: false, pending: [], cancelRequested: false };
119
- }
120
- const queue = userQueues[queueKey];
154
+ const runId = randomUUID();
155
+ const reportSideEffectError = (operation, error) => {
156
+ console.warn(
157
+ `[MessagingAutomation] ${operation} failed platform=${msg.platform} user=${userId}:`,
158
+ getErrorMessage(error)
159
+ );
160
+ };
121
161
 
122
- if (queue.cancelRequested && !queue.running) {
123
- queue.pending = [];
124
- queue.cancelRequested = false;
162
+ try {
163
+ await messagingManager.markRead(
164
+ userId,
165
+ msg.platform,
166
+ msg.chatId,
167
+ msg.messageId,
168
+ { agentId }
169
+ );
170
+ } catch (error) {
171
+ reportSideEffectError('mark read', error);
125
172
  }
126
173
 
127
- if (queue.running) {
128
- const last = queue.pending[queue.pending.length - 1];
129
- if (
130
- last
131
- && last.platform === msg.platform
132
- && last.chatId === msg.chatId
133
- && String(last.sender || '') === String(msg.sender || '')
134
- ) {
135
- last.content += `\n${msg.content}`;
136
- last.messageId = msg.messageId;
137
- } else {
138
- queue.pending.push({ ...msg });
139
- }
140
- return;
141
- }
174
+ const stopTypingKeepalive = startTypingKeepalive({
175
+ messagingManager,
176
+ userId,
177
+ agentId,
178
+ runId,
179
+ platform: msg.platform,
180
+ chatId: msg.chatId,
181
+ onError: reportSideEffectError
182
+ });
142
183
 
143
- queue.running = true;
144
- let stopTypingKeepalive = async () => {};
145
184
  try {
146
- const runId = randomUUID();
147
- await messagingManager
148
- .markRead(userId, msg.platform, msg.chatId, msg.messageId, { agentId })
149
- .catch(() => {});
150
- stopTypingKeepalive = startTypingKeepalive({
151
- messagingManager,
152
- userId,
153
- agentId,
154
- runId,
155
- platform: msg.platform,
156
- chatId: msg.chatId
157
- });
158
-
159
185
  const prompt = buildIncomingPrompt(msg);
160
186
  const conversationId = ensureConversation(userId, msg);
161
187
  const runOptions = isVoiceLikeMessage(msg)
@@ -182,105 +208,15 @@ async function processQueuedMessage({
182
208
  ];
183
209
  }
184
210
 
185
- await agentEngine.run(userId, prompt, runOptions);
211
+ const result = await agentEngine.run(userId, prompt, runOptions);
212
+ return { runId, result, error: null };
213
+ } catch (error) {
214
+ return { runId, result: null, error };
186
215
  } finally {
187
216
  await stopTypingKeepalive();
188
- if (queue.cancelRequested) {
189
- queue.pending = [];
190
- queue.running = false;
191
- queue.cancelRequested = false;
192
- return;
193
- }
194
- queue.running = false;
195
- if (queue.pending.length > 0) {
196
- const next = queue.pending.shift();
197
- await processQueuedMessage({
198
- userQueues,
199
- messagingManager,
200
- agentEngine,
201
- userId,
202
- msg: next
203
- });
204
- }
205
217
  }
206
218
  }
207
219
 
208
- function startTypingKeepalive({
209
- messagingManager,
210
- userId,
211
- agentId,
212
- runId,
213
- platform,
214
- chatId,
215
- intervalMs = 4000
216
- }) {
217
- let stopped = false;
218
- let timer = null;
219
- let releaseWait = null;
220
- let stopPromise = null;
221
-
222
- const matchesRunDelivery = (event) => (
223
- event?.runId
224
- && runId
225
- && event.runId === runId
226
- && event.userId === userId
227
- && event.platform === platform
228
- && event.to === chatId
229
- );
230
-
231
- const onMessageSent = (event) => {
232
- if (matchesRunDelivery(event) && event.deliveryKind !== 'interim') {
233
- stop().catch(() => {});
234
- }
235
- };
236
-
237
- if (typeof messagingManager?.on === 'function' && typeof messagingManager?.off === 'function') {
238
- messagingManager.on('message_sent', onMessageSent);
239
- }
240
-
241
- const wait = () =>
242
- new Promise((resolve) => {
243
- releaseWait = resolve;
244
- timer = setTimeout(resolve, intervalMs);
245
- });
246
-
247
- const loop = (async () => {
248
- while (!stopped) {
249
- await messagingManager
250
- .sendTyping(userId, platform, chatId, true, { agentId })
251
- .catch(() => {});
252
-
253
- if (stopped) break;
254
- await wait();
255
- }
256
- })();
257
-
258
- const stop = async () => {
259
- if (stopPromise) return stopPromise;
260
- stopPromise = (async () => {
261
- if (typeof messagingManager?.off === 'function') {
262
- messagingManager.off('message_sent', onMessageSent);
263
- }
264
- stopped = true;
265
- if (timer) {
266
- clearTimeout(timer);
267
- timer = null;
268
- }
269
- if (releaseWait) {
270
- releaseWait();
271
- releaseWait = null;
272
- }
273
- await loop.catch(() => {});
274
- await messagingManager
275
- .sendTyping(userId, platform, chatId, false, { agentId })
276
- .catch(() => {});
277
- })();
278
- return stopPromise;
279
- };
280
-
281
- return stop;
282
- }
283
-
284
220
  function ensureConversation(userId, msg) {
285
221
  const agentId = msg.agentId || null;
286
222
  let conversation = db
@@ -419,6 +355,7 @@ function emitBlockedSenderSuggestion({ io, userId, msg }) {
419
355
  module.exports = {
420
356
  buildIncomingPrompt,
421
357
  buildSenderIdentityBlock,
358
+ executeQueuedMessage,
422
359
  isAllowedMessagingSender,
423
360
  processQueuedMessage,
424
361
  registerMessagingAutomation,