@odience-network/paperclip-plugin-telegram-enhanced 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/dist/acp-bridge.d.ts +35 -0
  4. package/dist/acp-bridge.js +891 -0
  5. package/dist/acp-bridge.js.map +1 -0
  6. package/dist/adapter.d.ts +35 -0
  7. package/dist/adapter.js +75 -0
  8. package/dist/adapter.js.map +1 -0
  9. package/dist/agent-labels.d.ts +12 -0
  10. package/dist/agent-labels.js +96 -0
  11. package/dist/agent-labels.js.map +1 -0
  12. package/dist/allowlist.d.ts +27 -0
  13. package/dist/allowlist.js +34 -0
  14. package/dist/allowlist.js.map +1 -0
  15. package/dist/approval-routing.d.ts +2 -0
  16. package/dist/approval-routing.js +7 -0
  17. package/dist/approval-routing.js.map +1 -0
  18. package/dist/command-registry.d.ts +3 -0
  19. package/dist/command-registry.js +268 -0
  20. package/dist/command-registry.js.map +1 -0
  21. package/dist/commands.d.ts +11 -0
  22. package/dist/commands.js +516 -0
  23. package/dist/commands.js.map +1 -0
  24. package/dist/constants.d.ts +76 -0
  25. package/dist/constants.js +71 -0
  26. package/dist/constants.js.map +1 -0
  27. package/dist/escalation.d.ts +42 -0
  28. package/dist/escalation.js +252 -0
  29. package/dist/escalation.js.map +1 -0
  30. package/dist/file-routing.d.ts +51 -0
  31. package/dist/file-routing.js +212 -0
  32. package/dist/file-routing.js.map +1 -0
  33. package/dist/formatters.d.ts +31 -0
  34. package/dist/formatters.js +336 -0
  35. package/dist/formatters.js.map +1 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +4 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/interaction-delivery.d.ts +90 -0
  40. package/dist/interaction-delivery.js +142 -0
  41. package/dist/interaction-delivery.js.map +1 -0
  42. package/dist/manifest.d.ts +3 -0
  43. package/dist/manifest.js +111 -0
  44. package/dist/manifest.js.map +1 -0
  45. package/dist/media-pipeline.d.ts +47 -0
  46. package/dist/media-pipeline.js +162 -0
  47. package/dist/media-pipeline.js.map +1 -0
  48. package/dist/notification-filters.d.ts +23 -0
  49. package/dist/notification-filters.js +93 -0
  50. package/dist/notification-filters.js.map +1 -0
  51. package/dist/paperclip-api.d.ts +25 -0
  52. package/dist/paperclip-api.js +69 -0
  53. package/dist/paperclip-api.js.map +1 -0
  54. package/dist/polling-offset.d.ts +22 -0
  55. package/dist/polling-offset.js +68 -0
  56. package/dist/polling-offset.js.map +1 -0
  57. package/dist/secret-ref-validation.d.ts +7 -0
  58. package/dist/secret-ref-validation.js +49 -0
  59. package/dist/secret-ref-validation.js.map +1 -0
  60. package/dist/telegram-api.d.ts +40 -0
  61. package/dist/telegram-api.js +251 -0
  62. package/dist/telegram-api.js.map +1 -0
  63. package/dist/topic-projects.d.ts +2 -0
  64. package/dist/topic-projects.js +45 -0
  65. package/dist/topic-projects.js.map +1 -0
  66. package/dist/ui/index.d.ts +2 -0
  67. package/dist/ui/index.js +1446 -0
  68. package/dist/ui/index.js.map +1 -0
  69. package/dist/watch-registry.d.ts +9 -0
  70. package/dist/watch-registry.js +272 -0
  71. package/dist/watch-registry.js.map +1 -0
  72. package/dist/worker.d.ts +162 -0
  73. package/dist/worker.js +1520 -0
  74. package/dist/worker.js.map +1 -0
  75. package/package.json +59 -0
@@ -0,0 +1,891 @@
1
+ import { sendMessage, escapeMarkdownV2, sendChatAction } from "./telegram-api.js";
2
+ import { truncateAtWord } from "./telegram-api.js";
3
+ import { resolveMappedProjectIdForTopic } from "./topic-projects.js";
4
+ import { MAX_AGENTS_PER_THREAD, DEFAULT_CONVERSATION_TURNS, MAX_CONVERSATION_TURNS, ACP_SPAWN_EVENT, ACP_OUTPUT_EVENT, } from "./constants.js";
5
+ // --- Setup: register ACP output listener ---
6
+ export function setupAcpOutputListener(ctx, token) {
7
+ ctx.events.on(ACP_OUTPUT_EVENT, async (event) => {
8
+ const payload = event.payload;
9
+ await handleAcpOutput(ctx, token, payload);
10
+ });
11
+ }
12
+ // --- ACP command handler ---
13
+ export async function handleAcpCommand(ctx, token, chatId, args, messageThreadId, companyId, maxAgentsPerThread = MAX_AGENTS_PER_THREAD) {
14
+ const parts = args.trim().split(/\s+/);
15
+ const subcommand = parts[0]?.toLowerCase() ?? "";
16
+ switch (subcommand) {
17
+ case "spawn":
18
+ await handleAcpSpawn(ctx, token, chatId, parts.slice(1).join(" "), messageThreadId, companyId, maxAgentsPerThread);
19
+ break;
20
+ case "status":
21
+ await handleAcpStatus(ctx, token, chatId, messageThreadId);
22
+ break;
23
+ case "cancel":
24
+ await handleAcpCancel(ctx, token, chatId, messageThreadId, companyId);
25
+ break;
26
+ case "close":
27
+ await handleAcpClose(ctx, token, chatId, parts.slice(1).join(" ").trim(), messageThreadId, companyId);
28
+ break;
29
+ default:
30
+ await sendMessage(ctx, token, chatId, [
31
+ escapeMarkdownV2("\ud83d\udd0c") + " *ACP Commands*",
32
+ "",
33
+ `/acp spawn <agent\\-name> \\- ${escapeMarkdownV2("Start an agent session in this thread")}`,
34
+ `/acp status \\- ${escapeMarkdownV2("Show all agent sessions in this thread")}`,
35
+ `/acp cancel \\- ${escapeMarkdownV2("Cancel the running agent task")}`,
36
+ `/acp close [agent\\-name] \\- ${escapeMarkdownV2("End an agent session (most recent if no name given)")}`,
37
+ ].join("\n"), { parseMode: "MarkdownV2", messageThreadId });
38
+ }
39
+ }
40
+ // --- Agent name resolution ---
41
+ /**
42
+ * Resolve an agent by name/urlKey (case-insensitive).
43
+ * The plugin SDK's `agents.get()` requires a UUID, so we list all agents
44
+ * and match by name or urlKey.
45
+ *
46
+ * The SDK may return the agent UUID in `id`, `agentId`, or `_id` depending
47
+ * on the Paperclip version. We pick the first field that looks like a UUID
48
+ * and fall back to `id` if none do (caller will get a clear error on create).
49
+ */
50
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
51
+ async function resolveAgentByName(ctx, name, companyId) {
52
+ try {
53
+ const allAgents = await ctx.agents.list({ companyId });
54
+ const lower = name.toLowerCase();
55
+ const match = allAgents.find((a) => a.name?.toLowerCase() === lower ||
56
+ a.urlKey?.toLowerCase() === lower);
57
+ if (!match)
58
+ return null;
59
+ // Find the UUID — different SDK versions may use different field names
60
+ const candidateId = match.agentId ?? match._id ?? match.id;
61
+ const resolvedId = UUID_RE.test(String(candidateId)) ? String(candidateId) : String(match.id);
62
+ ctx.logger.info("Resolved agent by name", {
63
+ agentName: name,
64
+ resolvedId,
65
+ rawId: match.id,
66
+ rawAgentId: match.agentId,
67
+ hasUrlKey: !!match.urlKey,
68
+ });
69
+ return { id: resolvedId, name: match.name };
70
+ }
71
+ catch (err) {
72
+ ctx.logger.error("Failed to resolve agent by name", { agentName: name, companyId, error: String(err) });
73
+ return null;
74
+ }
75
+ }
76
+ // --- Native prompt delivery via issue creation ---
77
+ //
78
+ // The Paperclip heartbeat system only delivers taskId/issueId/commentId to
79
+ // agents — freeform prompts passed via sessions.sendMessage({ prompt }) are
80
+ // silently dropped. To work around this, we create a lightweight issue whose
81
+ // title IS the prompt and assign it to the agent. The agent wakes with
82
+ // PAPERCLIP_TASK_ID pointing to that issue and can read the prompt from the
83
+ // issue title + description.
84
+ export async function wakeAgentWithIssue(ctx, agentId, companyId, promptText, reason, projectId) {
85
+ try {
86
+ const title = truncateAtWord(promptText.replace(/\n/g, " "), 200);
87
+ const description = promptText.length > 200 ? promptText : undefined;
88
+ const issue = await ctx.issues.create({
89
+ companyId,
90
+ ...(projectId ? { projectId } : {}),
91
+ title: `[Telegram] ${title}`,
92
+ description,
93
+ assigneeAgentId: agentId,
94
+ });
95
+ await ctx.issues.update(issue.id, { status: "todo" }, companyId);
96
+ ctx.logger.info("Created issue for native agent prompt delivery", {
97
+ issueId: issue.id,
98
+ agentId,
99
+ reason,
100
+ promptLength: promptText.length,
101
+ });
102
+ return issue.id;
103
+ }
104
+ catch (err) {
105
+ ctx.logger.error("Failed to create issue for native prompt delivery", {
106
+ agentId,
107
+ companyId,
108
+ projectId,
109
+ reason,
110
+ error: String(err),
111
+ });
112
+ return null;
113
+ }
114
+ }
115
+ // --- Spawn (multi-agent aware, native-first) ---
116
+ async function handleAcpSpawn(ctx, token, chatId, agentName, messageThreadId, companyId, maxAgentsPerThread = MAX_AGENTS_PER_THREAD) {
117
+ if (!agentName.trim()) {
118
+ await sendMessage(ctx, token, chatId, "Usage: /acp spawn <agent-name>", {
119
+ messageThreadId,
120
+ });
121
+ return;
122
+ }
123
+ if (!messageThreadId) {
124
+ await sendMessage(ctx, token, chatId, "Agent sessions must be started inside a topic thread.", { messageThreadId });
125
+ return;
126
+ }
127
+ const sessions = await getSessions(ctx, chatId, messageThreadId);
128
+ const activeSessions = sessions.filter((s) => s.status === "active");
129
+ if (activeSessions.length >= maxAgentsPerThread) {
130
+ const listing = activeSessions.map((s) => ` - ${s.agentDisplayName} (${s.transport})`).join("\n");
131
+ await sendMessage(ctx, token, chatId, `Thread already has ${maxAgentsPerThread} active agents (max):\n${listing}`, { messageThreadId });
132
+ return;
133
+ }
134
+ await sendChatAction(ctx, token, chatId);
135
+ const trimmedName = agentName.trim();
136
+ const displayName = trimmedName.charAt(0).toUpperCase() + trimmedName.slice(1);
137
+ const resolvedCompanyId = companyId ?? await resolveCompanyIdFromChat(ctx, chatId);
138
+ // Try native session first: resolve agent by name, then create session
139
+ let transport = "acp";
140
+ let sessionId;
141
+ let agentId = "";
142
+ const resolved = await resolveAgentByName(ctx, trimmedName, resolvedCompanyId);
143
+ if (resolved) {
144
+ try {
145
+ agentId = resolved.id;
146
+ const session = await ctx.agents.sessions.create(agentId, resolvedCompanyId, {
147
+ reason: `Telegram thread ${chatId}/${messageThreadId}`,
148
+ });
149
+ sessionId = session.sessionId;
150
+ transport = "native";
151
+ ctx.logger.info("Created native agent session", { agentId, sessionId });
152
+ }
153
+ catch (err) {
154
+ ctx.logger.error("Native session creation failed, falling back to ACP", {
155
+ agentId,
156
+ agentName: trimmedName,
157
+ companyId: resolvedCompanyId,
158
+ error: String(err),
159
+ });
160
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
161
+ }
162
+ }
163
+ else {
164
+ ctx.logger.warn("Agent not found by name, using ACP transport", { agentName: trimmedName, companyId: resolvedCompanyId });
165
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
166
+ }
167
+ const now = new Date().toISOString();
168
+ const newSession = {
169
+ sessionId,
170
+ agentId,
171
+ agentName: trimmedName,
172
+ agentDisplayName: displayName,
173
+ transport,
174
+ spawnedAt: now,
175
+ status: "active",
176
+ lastActivityAt: now,
177
+ };
178
+ sessions.push(newSession);
179
+ await saveSessions(ctx, chatId, messageThreadId, sessions);
180
+ if (transport === "acp") {
181
+ // Emit ACP spawn event - companyId is SECOND arg
182
+ ctx.events.emit(ACP_SPAWN_EVENT, resolvedCompanyId, {
183
+ type: "spawn",
184
+ sessionId,
185
+ agentName: trimmedName,
186
+ chatId,
187
+ threadId: messageThreadId,
188
+ });
189
+ }
190
+ const agentCount = activeSessions.length + 1;
191
+ const transportLabel = transport === "native" ? "Paperclip" : "ACP";
192
+ const agentCountLine = agentCount > 1
193
+ ? `\n${escapeMarkdownV2(`${agentCount} agents now active in this thread. Use @${trimmedName} to address directly.`)}`
194
+ : "";
195
+ await sendMessage(ctx, token, chatId, [
196
+ escapeMarkdownV2("\ud83d\udd0c") + " *Agent Session Started*",
197
+ "",
198
+ `Agent: *${escapeMarkdownV2(displayName)}*`,
199
+ `Transport: *${escapeMarkdownV2(transportLabel)}*`,
200
+ `Session: \`${escapeMarkdownV2(sessionId)}\``,
201
+ "",
202
+ escapeMarkdownV2("Send messages in this thread to interact with the agent."),
203
+ agentCountLine,
204
+ ].join("\n"), { parseMode: "MarkdownV2", messageThreadId });
205
+ ctx.logger.info("Agent session spawned", { sessionId, agentName: trimmedName, transport, chatId, threadId: messageThreadId });
206
+ }
207
+ // --- Status ---
208
+ async function handleAcpStatus(ctx, token, chatId, messageThreadId) {
209
+ if (!messageThreadId) {
210
+ await sendMessage(ctx, token, chatId, "Run /acp status inside a thread with an active session.", {
211
+ messageThreadId,
212
+ });
213
+ return;
214
+ }
215
+ const sessions = await getSessions(ctx, chatId, messageThreadId);
216
+ const activeSessions = sessions.filter((s) => s.status === "active");
217
+ if (activeSessions.length === 0) {
218
+ await sendMessage(ctx, token, chatId, "No agent sessions bound to this thread.", {
219
+ messageThreadId,
220
+ });
221
+ return;
222
+ }
223
+ const lines = [
224
+ escapeMarkdownV2("\ud83d\udd0c") + ` *Agent Sessions \\(${activeSessions.length}\\)*`,
225
+ "",
226
+ ];
227
+ for (const session of activeSessions) {
228
+ lines.push(`${escapeMarkdownV2("\ud83e\udd16")} *${escapeMarkdownV2(session.agentDisplayName)}* \\[${escapeMarkdownV2(session.transport)}\\]`, ` Session: \`${escapeMarkdownV2(session.sessionId)}\``, ` Started: ${escapeMarkdownV2(session.spawnedAt)}`, ` Last active: ${escapeMarkdownV2(session.lastActivityAt)}`, "");
229
+ }
230
+ await sendMessage(ctx, token, chatId, lines.join("\n"), {
231
+ parseMode: "MarkdownV2",
232
+ messageThreadId,
233
+ });
234
+ }
235
+ // --- Cancel ---
236
+ async function handleAcpCancel(ctx, token, chatId, messageThreadId, companyId) {
237
+ if (!messageThreadId) {
238
+ await sendMessage(ctx, token, chatId, "Run /acp cancel inside a thread with an active session.", {
239
+ messageThreadId,
240
+ });
241
+ return;
242
+ }
243
+ const sessions = await getSessions(ctx, chatId, messageThreadId);
244
+ const activeSessions = sessions.filter((s) => s.status === "active");
245
+ if (activeSessions.length === 0) {
246
+ await sendMessage(ctx, token, chatId, "No agent sessions bound to this thread.", {
247
+ messageThreadId,
248
+ });
249
+ return;
250
+ }
251
+ // Cancel the most recently active session
252
+ const target = activeSessions.sort((a, b) => new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime())[0];
253
+ const resolvedCompanyId = companyId ?? await resolveCompanyIdFromChat(ctx, chatId);
254
+ if (target.transport === "native") {
255
+ try {
256
+ await ctx.agents.sessions.close(target.sessionId, resolvedCompanyId);
257
+ }
258
+ catch (err) {
259
+ ctx.logger.error("Failed to close native session", { error: String(err) });
260
+ }
261
+ }
262
+ else {
263
+ ctx.events.emit(ACP_SPAWN_EVENT, resolvedCompanyId, {
264
+ type: "cancel",
265
+ sessionId: target.sessionId,
266
+ chatId,
267
+ threadId: messageThreadId,
268
+ });
269
+ }
270
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\u23f9")} Cancellation requested for *${escapeMarkdownV2(target.agentDisplayName)}* \\(\`${escapeMarkdownV2(target.sessionId)}\`\\)`, { parseMode: "MarkdownV2", messageThreadId });
271
+ ctx.logger.info("Agent cancel requested", { sessionId: target.sessionId, chatId, threadId: messageThreadId });
272
+ }
273
+ // --- Close ---
274
+ async function handleAcpClose(ctx, token, chatId, targetAgentName, messageThreadId, companyId) {
275
+ if (!messageThreadId) {
276
+ await sendMessage(ctx, token, chatId, "Run /acp close inside a thread with an active session.", {
277
+ messageThreadId,
278
+ });
279
+ return;
280
+ }
281
+ const sessions = await getSessions(ctx, chatId, messageThreadId);
282
+ const activeSessions = sessions.filter((s) => s.status === "active");
283
+ if (activeSessions.length === 0) {
284
+ await sendMessage(ctx, token, chatId, "No agent sessions bound to this thread.", {
285
+ messageThreadId,
286
+ });
287
+ return;
288
+ }
289
+ let targetSession;
290
+ if (targetAgentName) {
291
+ const lowerTarget = targetAgentName.toLowerCase();
292
+ targetSession = activeSessions.find((s) => s.agentName.toLowerCase() === lowerTarget);
293
+ if (!targetSession) {
294
+ targetSession = activeSessions.find((s) => s.agentName.toLowerCase().includes(lowerTarget));
295
+ }
296
+ if (!targetSession) {
297
+ const listing = activeSessions.map((s) => ` - ${s.agentDisplayName}`).join("\n");
298
+ await sendMessage(ctx, token, chatId, `No agent named "${targetAgentName}" found. Active agents:\n${listing}`, { messageThreadId });
299
+ return;
300
+ }
301
+ }
302
+ else {
303
+ targetSession = activeSessions.sort((a, b) => new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime())[0];
304
+ }
305
+ const resolvedCompanyId = companyId ?? await resolveCompanyIdFromChat(ctx, chatId);
306
+ // Close via the correct transport
307
+ if (targetSession.transport === "native") {
308
+ try {
309
+ await ctx.agents.sessions.close(targetSession.sessionId, resolvedCompanyId);
310
+ }
311
+ catch (err) {
312
+ ctx.logger.error("Failed to close native session", { error: String(err) });
313
+ }
314
+ }
315
+ else {
316
+ ctx.events.emit(ACP_SPAWN_EVENT, resolvedCompanyId, {
317
+ type: "close",
318
+ sessionId: targetSession.sessionId,
319
+ chatId,
320
+ threadId: messageThreadId,
321
+ });
322
+ }
323
+ // Mark closed
324
+ const idx = sessions.findIndex((s) => s.sessionId === targetSession.sessionId);
325
+ if (idx >= 0) {
326
+ sessions[idx].status = "closed";
327
+ }
328
+ await saveSessions(ctx, chatId, messageThreadId, sessions);
329
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\ud83d\udd0c")} Session for *${escapeMarkdownV2(targetSession.agentDisplayName)}* closed\\.`, { parseMode: "MarkdownV2", messageThreadId });
330
+ ctx.logger.info("Agent session closed", {
331
+ sessionId: targetSession.sessionId,
332
+ agentName: targetSession.agentName,
333
+ transport: targetSession.transport,
334
+ chatId,
335
+ threadId: messageThreadId,
336
+ });
337
+ }
338
+ // --- Multi-agent message routing ---
339
+ export async function routeMessageToAgent(ctx, token, chatId, threadId, text, replyToMessageId, companyId) {
340
+ const sessions = await getSessions(ctx, chatId, threadId);
341
+ const activeSessions = sessions.filter((s) => s.status === "active");
342
+ if (activeSessions.length === 0)
343
+ return false;
344
+ let targetSession;
345
+ // 1) Check for @mention
346
+ const mentionMatch = text.match(/@(\w+)/);
347
+ if (mentionMatch) {
348
+ const mentionName = mentionMatch[1].toLowerCase();
349
+ targetSession = activeSessions.find((s) => s.agentName.toLowerCase() === mentionName || s.agentDisplayName.toLowerCase() === mentionName);
350
+ if (!targetSession) {
351
+ targetSession = activeSessions.find((s) => s.agentName.toLowerCase().includes(mentionName) || s.agentDisplayName.toLowerCase().includes(mentionName));
352
+ }
353
+ }
354
+ // 2) Check reply-to for agent message mapping
355
+ if (!targetSession && replyToMessageId) {
356
+ const agentMapping = await ctx.state.get({
357
+ scopeKind: "instance",
358
+ stateKey: `agent_msg_${chatId}_${replyToMessageId}`,
359
+ });
360
+ if (agentMapping) {
361
+ targetSession = activeSessions.find((s) => s.sessionId === agentMapping.sessionId);
362
+ }
363
+ }
364
+ // 3) Fallback: most recently active agent
365
+ if (!targetSession) {
366
+ targetSession = activeSessions.sort((a, b) => new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime())[0];
367
+ }
368
+ // Update last activity
369
+ targetSession.lastActivityAt = new Date().toISOString();
370
+ const idx = sessions.findIndex((s) => s.sessionId === targetSession.sessionId);
371
+ if (idx >= 0) {
372
+ sessions[idx] = targetSession;
373
+ }
374
+ await saveSessions(ctx, chatId, threadId, sessions);
375
+ const resolvedCompanyId = companyId ?? await resolveCompanyIdFromChat(ctx, chatId);
376
+ const projectId = await resolveMappedProjectIdForTopic(ctx, chatId, resolvedCompanyId, threadId);
377
+ // Route via correct transport
378
+ if (targetSession.transport === "native") {
379
+ const issueId = await wakeAgentWithIssue(ctx, targetSession.agentId, resolvedCompanyId, text, "telegram_message", projectId);
380
+ if (!issueId) {
381
+ ctx.logger.error("Failed to deliver message to native agent — issue creation failed", {
382
+ sessionId: targetSession.sessionId,
383
+ agentId: targetSession.agentId,
384
+ });
385
+ return false;
386
+ }
387
+ }
388
+ else {
389
+ // ACP transport - emit event, companyId is SECOND arg
390
+ ctx.events.emit(ACP_SPAWN_EVENT, resolvedCompanyId, {
391
+ type: "message",
392
+ sessionId: targetSession.sessionId,
393
+ chatId,
394
+ threadId,
395
+ text,
396
+ });
397
+ }
398
+ ctx.logger.info("Routed message to agent session", {
399
+ sessionId: targetSession.sessionId,
400
+ agentName: targetSession.agentName,
401
+ transport: targetSession.transport,
402
+ chatId,
403
+ threadId,
404
+ routingMethod: mentionMatch ? "mention" : replyToMessageId ? "reply" : "fallback",
405
+ });
406
+ return true;
407
+ }
408
+ // --- ACP output handler (sequenced, labeled) ---
409
+ export async function handleAcpOutput(ctx, token, event) {
410
+ const { sessionId, chatId, threadId, text, done } = event;
411
+ const sessions = await getSessions(ctx, chatId, threadId);
412
+ const session = sessions.find((s) => s.sessionId === sessionId);
413
+ const displayName = session?.agentDisplayName ?? "Agent";
414
+ // Update last activity
415
+ if (session) {
416
+ session.lastActivityAt = new Date().toISOString();
417
+ const idx = sessions.findIndex((s) => s.sessionId === sessionId);
418
+ if (idx >= 0) {
419
+ sessions[idx] = session;
420
+ }
421
+ await saveSessions(ctx, chatId, threadId, sessions);
422
+ }
423
+ // Output sequencing for multi-agent threads
424
+ const activeSessions = sessions.filter((s) => s.status === "active");
425
+ if (activeSessions.length > 1) {
426
+ const queued = await handleOutputSequencing(ctx, token, chatId, threadId, {
427
+ sessionId,
428
+ agentDisplayName: displayName,
429
+ text,
430
+ done: done ?? false,
431
+ queuedAt: Date.now(),
432
+ });
433
+ if (queued)
434
+ return;
435
+ }
436
+ await sendLabeledOutput(ctx, token, chatId, threadId, sessionId, displayName, text, done);
437
+ await checkConversationLoopContinuation(ctx, token, chatId, threadId, sessionId, text, done);
438
+ }
439
+ // --- Output sequencing ---
440
+ async function handleOutputSequencing(ctx, token, chatId, threadId, entry) {
441
+ const speakerKey = `output_speaker_${chatId}_${threadId}`;
442
+ const queueKey = `output_queue_${chatId}_${threadId}`;
443
+ const currentSpeaker = await ctx.state.get({
444
+ scopeKind: "instance",
445
+ stateKey: speakerKey,
446
+ });
447
+ if (!currentSpeaker || currentSpeaker === entry.sessionId) {
448
+ await ctx.state.set({ scopeKind: "instance", stateKey: speakerKey }, entry.sessionId);
449
+ if (entry.done) {
450
+ await ctx.state.set({ scopeKind: "instance", stateKey: speakerKey }, null);
451
+ await flushOutputQueue(ctx, token, chatId, threadId);
452
+ }
453
+ return false;
454
+ }
455
+ // Another agent is speaking - queue
456
+ const queue = await ctx.state.get({
457
+ scopeKind: "instance",
458
+ stateKey: queueKey,
459
+ }) ?? [];
460
+ queue.push(entry);
461
+ await ctx.state.set({ scopeKind: "instance", stateKey: queueKey }, queue);
462
+ return true;
463
+ }
464
+ async function flushOutputQueue(ctx, token, chatId, threadId) {
465
+ const queueKey = `output_queue_${chatId}_${threadId}`;
466
+ const speakerKey = `output_speaker_${chatId}_${threadId}`;
467
+ const queue = await ctx.state.get({
468
+ scopeKind: "instance",
469
+ stateKey: queueKey,
470
+ }) ?? [];
471
+ if (queue.length === 0)
472
+ return;
473
+ const firstEntry = queue[0];
474
+ const nextSpeaker = firstEntry.sessionId;
475
+ await ctx.state.set({ scopeKind: "instance", stateKey: speakerKey }, nextSpeaker);
476
+ const toSend = [];
477
+ const remaining = [];
478
+ for (const entry of queue) {
479
+ if (entry.sessionId === nextSpeaker) {
480
+ toSend.push(entry);
481
+ }
482
+ else {
483
+ remaining.push(entry);
484
+ }
485
+ }
486
+ await ctx.state.set({ scopeKind: "instance", stateKey: queueKey }, remaining);
487
+ for (const entry of toSend) {
488
+ await sendLabeledOutput(ctx, token, chatId, threadId, entry.sessionId, entry.agentDisplayName, entry.text, entry.done);
489
+ if (entry.done) {
490
+ await ctx.state.set({ scopeKind: "instance", stateKey: speakerKey }, null);
491
+ await flushOutputQueue(ctx, token, chatId, threadId);
492
+ return;
493
+ }
494
+ }
495
+ }
496
+ // --- Markdown to Telegram HTML ---
497
+ function escapeHtml(text) {
498
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
499
+ }
500
+ function markdownToTelegramHtml(text) {
501
+ let html = escapeHtml(text);
502
+ // Bold: **text** → <b>text</b>
503
+ html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
504
+ // Italic: _text_ (but not in the middle of words)
505
+ html = html.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
506
+ // Inline code: `text` → <code>text</code>
507
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
508
+ // Code blocks: ```text``` → <pre>text</pre>
509
+ html = html.replace(/```(?:\w*\n)?([\s\S]*?)```/g, "<pre>$1</pre>");
510
+ return html;
511
+ }
512
+ // --- Send labeled output ---
513
+ const TELEGRAM_MAX_LENGTH = 4000; // Leave room for prefix/label overhead
514
+ async function sendLabeledOutput(ctx, token, chatId, threadId, sessionId, displayName, text, done) {
515
+ const prefix = done
516
+ ? escapeMarkdownV2("\u2705")
517
+ : escapeMarkdownV2("\ud83e\udd16");
518
+ const label = `*\\[${escapeMarkdownV2(displayName)}\\]*`;
519
+ // Split long text into chunks to stay within Telegram's 4096 char limit
520
+ const chunks = [];
521
+ if (text.length <= TELEGRAM_MAX_LENGTH) {
522
+ chunks.push(text);
523
+ }
524
+ else {
525
+ let remaining = text;
526
+ while (remaining.length > 0) {
527
+ if (remaining.length <= TELEGRAM_MAX_LENGTH) {
528
+ chunks.push(remaining);
529
+ break;
530
+ }
531
+ // Try to split at a newline boundary
532
+ let splitAt = remaining.lastIndexOf("\n", TELEGRAM_MAX_LENGTH);
533
+ if (splitAt <= 0)
534
+ splitAt = TELEGRAM_MAX_LENGTH;
535
+ chunks.push(remaining.slice(0, splitAt));
536
+ remaining = remaining.slice(splitAt).replace(/^\n/, "");
537
+ }
538
+ }
539
+ for (let i = 0; i < chunks.length; i++) {
540
+ const isLast = i === chunks.length - 1;
541
+ // Convert agent Markdown to Telegram HTML for proper rendering
542
+ const doneEmoji = done ? "\u2705" : "\ud83e\udd16";
543
+ const chunkPrefix = `${doneEmoji} <b>[${escapeHtml(displayName)}]</b> `;
544
+ const formatted = `${chunkPrefix}${markdownToTelegramHtml(chunks[i])}`;
545
+ const messageId = await sendMessage(ctx, token, chatId, formatted, {
546
+ parseMode: "HTML",
547
+ messageThreadId: threadId,
548
+ });
549
+ if (messageId && isLast) {
550
+ await ctx.state.set({ scopeKind: "instance", stateKey: `agent_msg_${chatId}_${messageId}` }, { sessionId });
551
+ }
552
+ }
553
+ }
554
+ // --- Handoff tool handler ---
555
+ export async function handleHandoffToolCall(ctx, token, params, companyId, sourceAgentId) {
556
+ const targetAgent = String(params.targetAgent ?? "");
557
+ const reason = String(params.reason ?? "");
558
+ const contextSummary = String(params.contextSummary ?? "");
559
+ const requiresApproval = params.requiresApproval !== false;
560
+ const chatId = String(params.chatId ?? "");
561
+ const threadId = Number(params.threadId ?? 0);
562
+ if (!targetAgent || !chatId || !threadId) {
563
+ return { error: "Missing required fields: targetAgent, chatId, threadId" };
564
+ }
565
+ const sessions = await getSessions(ctx, chatId, threadId);
566
+ const sourceSession = sessions.find((s) => s.agentId === sourceAgentId);
567
+ const sourceAgent = sourceSession?.agentDisplayName ?? "Agent";
568
+ const handoffId = `handoff_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
569
+ const handoffText = [
570
+ `${escapeMarkdownV2("\ud83d\udd04")} *\\[${escapeMarkdownV2(sourceAgent)}\\]* ${escapeMarkdownV2("Handing off to")} *${escapeMarkdownV2(targetAgent)}*`,
571
+ "",
572
+ `${escapeMarkdownV2("Reason:")} ${escapeMarkdownV2(reason)}`,
573
+ ].join("\n");
574
+ if (requiresApproval) {
575
+ await sendMessage(ctx, token, chatId, handoffText, {
576
+ parseMode: "MarkdownV2",
577
+ messageThreadId: threadId,
578
+ inlineKeyboard: [
579
+ [
580
+ { text: "Approve", callback_data: `handoff_approve_${handoffId}` },
581
+ { text: "Reject", callback_data: `handoff_reject_${handoffId}` },
582
+ ],
583
+ ],
584
+ });
585
+ const pending = {
586
+ handoffId,
587
+ sourceSessionId: sourceSession?.sessionId ?? "",
588
+ sourceAgent,
589
+ targetAgent,
590
+ reason,
591
+ contextSummary,
592
+ chatId,
593
+ threadId,
594
+ companyId,
595
+ };
596
+ await ctx.state.set({ scopeKind: "instance", stateKey: `handoff_${handoffId}` }, pending);
597
+ return { content: JSON.stringify({ status: "pending_approval", handoffId }) };
598
+ }
599
+ await sendMessage(ctx, token, chatId, handoffText, {
600
+ parseMode: "MarkdownV2",
601
+ messageThreadId: threadId,
602
+ });
603
+ await executeHandoff(ctx, token, chatId, threadId, targetAgent, contextSummary, sessions, companyId);
604
+ return { content: JSON.stringify({ status: "handed_off", handoffId }) };
605
+ }
606
+ // --- Handoff callback handlers ---
607
+ export async function handleHandoffApproval(ctx, token, handoffId, actor, callbackQueryId, chatId, messageId) {
608
+ const pending = await ctx.state.get({
609
+ scopeKind: "instance",
610
+ stateKey: `handoff_${handoffId}`,
611
+ });
612
+ if (!pending)
613
+ return;
614
+ const sessions = await getSessions(ctx, pending.chatId, pending.threadId);
615
+ await executeHandoff(ctx, token, pending.chatId, pending.threadId, pending.targetAgent, pending.contextSummary, sessions, pending.companyId);
616
+ await ctx.state.set({ scopeKind: "instance", stateKey: `handoff_${handoffId}` }, null);
617
+ ctx.logger.info("Handoff approved", { handoffId, actor, targetAgent: pending.targetAgent });
618
+ }
619
+ export async function handleHandoffRejection(ctx, token, handoffId, actor, callbackQueryId, chatId, messageId) {
620
+ const pending = await ctx.state.get({
621
+ scopeKind: "instance",
622
+ stateKey: `handoff_${handoffId}`,
623
+ });
624
+ if (!pending)
625
+ return;
626
+ await sendMessage(ctx, token, pending.chatId, `${escapeMarkdownV2("\u274c")} Handoff to *${escapeMarkdownV2(pending.targetAgent)}* rejected by ${escapeMarkdownV2(actor)}`, { parseMode: "MarkdownV2", messageThreadId: pending.threadId });
627
+ await ctx.state.set({ scopeKind: "instance", stateKey: `handoff_${handoffId}` }, null);
628
+ ctx.logger.info("Handoff rejected", { handoffId, actor, targetAgent: pending.targetAgent });
629
+ }
630
+ async function executeHandoff(ctx, token, chatId, threadId, targetAgent, contextSummary, sessions, companyId) {
631
+ const activeSessions = sessions.filter((s) => s.status === "active");
632
+ const lowerTarget = targetAgent.toLowerCase();
633
+ let targetSession = activeSessions.find((s) => s.agentName.toLowerCase() === lowerTarget || s.agentDisplayName.toLowerCase() === lowerTarget);
634
+ if (!targetSession) {
635
+ // Auto-spawn the target agent using native-first approach
636
+ let transport = "acp";
637
+ let sessionId;
638
+ let agentId = "";
639
+ const resolved = await resolveAgentByName(ctx, targetAgent, companyId);
640
+ if (resolved) {
641
+ try {
642
+ agentId = resolved.id;
643
+ const session = await ctx.agents.sessions.create(agentId, companyId, {
644
+ reason: `Handoff from Telegram thread ${chatId}/${threadId}`,
645
+ });
646
+ sessionId = session.sessionId;
647
+ transport = "native";
648
+ }
649
+ catch (err) {
650
+ ctx.logger.error("Native session creation failed during handoff, falling back to ACP", {
651
+ agentId, targetAgent, companyId, error: String(err),
652
+ });
653
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
654
+ }
655
+ }
656
+ else {
657
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
658
+ }
659
+ const displayName = targetAgent.charAt(0).toUpperCase() + targetAgent.slice(1);
660
+ const now = new Date().toISOString();
661
+ targetSession = {
662
+ sessionId,
663
+ agentId,
664
+ agentName: targetAgent,
665
+ agentDisplayName: displayName,
666
+ transport,
667
+ spawnedAt: now,
668
+ status: "active",
669
+ lastActivityAt: now,
670
+ };
671
+ sessions.push(targetSession);
672
+ await saveSessions(ctx, chatId, threadId, sessions);
673
+ if (transport === "acp") {
674
+ ctx.events.emit(ACP_SPAWN_EVENT, companyId, {
675
+ type: "spawn",
676
+ sessionId,
677
+ agentName: targetAgent,
678
+ chatId,
679
+ threadId,
680
+ });
681
+ }
682
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\ud83d\udd0c")} Auto\\-spawned *${escapeMarkdownV2(displayName)}* \\[${escapeMarkdownV2(transport)}\\] for handoff`, { parseMode: "MarkdownV2", messageThreadId: threadId });
683
+ }
684
+ // Send context to target agent
685
+ if (targetSession.transport === "native") {
686
+ await wakeAgentWithIssue(ctx, targetSession.agentId, companyId, `[Handoff context] ${contextSummary}`, "handoff");
687
+ }
688
+ else {
689
+ ctx.events.emit(ACP_SPAWN_EVENT, companyId, {
690
+ type: "message",
691
+ sessionId: targetSession.sessionId,
692
+ chatId,
693
+ threadId,
694
+ text: `[Handoff context] ${contextSummary}`,
695
+ });
696
+ }
697
+ }
698
+ // --- Discuss tool handler ---
699
+ export async function handleDiscussToolCall(ctx, token, params, companyId, sourceAgentId) {
700
+ const targetAgent = String(params.targetAgent ?? "");
701
+ const topic = String(params.topic ?? "");
702
+ const initialMessage = String(params.initialMessage ?? "");
703
+ const maxTurns = Math.min(Number(params.maxTurns ?? DEFAULT_CONVERSATION_TURNS), MAX_CONVERSATION_TURNS);
704
+ const humanCheckpointAt = params.humanCheckpointAt != null ? Number(params.humanCheckpointAt) : undefined;
705
+ const chatId = String(params.chatId ?? "");
706
+ const threadId = Number(params.threadId ?? 0);
707
+ if (!targetAgent || !initialMessage || !chatId || !threadId) {
708
+ return { error: "Missing required fields: targetAgent, initialMessage, chatId, threadId" };
709
+ }
710
+ const sessions = await getSessions(ctx, chatId, threadId);
711
+ const activeSessions = sessions.filter((s) => s.status === "active");
712
+ const initiatorSession = sessions.find((s) => s.agentId === sourceAgentId);
713
+ // Find or spawn target
714
+ const lowerTarget = targetAgent.toLowerCase();
715
+ let targetSession = activeSessions.find((s) => s.agentName.toLowerCase() === lowerTarget || s.agentDisplayName.toLowerCase() === lowerTarget);
716
+ if (!targetSession) {
717
+ let transport = "acp";
718
+ let sessionId;
719
+ let agentId = "";
720
+ const resolved = await resolveAgentByName(ctx, targetAgent, companyId);
721
+ if (resolved) {
722
+ try {
723
+ agentId = resolved.id;
724
+ const session = await ctx.agents.sessions.create(agentId, companyId, {
725
+ reason: `Discussion from Telegram thread ${chatId}/${threadId}`,
726
+ });
727
+ sessionId = session.sessionId;
728
+ transport = "native";
729
+ }
730
+ catch (err) {
731
+ ctx.logger.error("Native session creation failed during discussion, falling back to ACP", {
732
+ agentId, targetAgent, companyId, error: String(err),
733
+ });
734
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
735
+ }
736
+ }
737
+ else {
738
+ sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
739
+ }
740
+ const displayName = targetAgent.charAt(0).toUpperCase() + targetAgent.slice(1);
741
+ const now = new Date().toISOString();
742
+ targetSession = {
743
+ sessionId,
744
+ agentId,
745
+ agentName: targetAgent,
746
+ agentDisplayName: displayName,
747
+ transport,
748
+ spawnedAt: now,
749
+ status: "active",
750
+ lastActivityAt: now,
751
+ };
752
+ sessions.push(targetSession);
753
+ await saveSessions(ctx, chatId, threadId, sessions);
754
+ if (transport === "acp") {
755
+ ctx.events.emit(ACP_SPAWN_EVENT, companyId, {
756
+ type: "spawn",
757
+ sessionId,
758
+ agentName: targetAgent,
759
+ chatId,
760
+ threadId,
761
+ });
762
+ }
763
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\ud83d\udd0c")} Auto\\-spawned *${escapeMarkdownV2(displayName)}* for discussion`, { parseMode: "MarkdownV2", messageThreadId: threadId });
764
+ }
765
+ const loopId = `loop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
766
+ const loop = {
767
+ loopId,
768
+ initiatorSessionId: initiatorSession?.sessionId ?? "",
769
+ targetSessionId: targetSession.sessionId,
770
+ initiatorAgent: initiatorSession?.agentDisplayName ?? "Agent",
771
+ targetAgent: targetSession.agentDisplayName,
772
+ topic,
773
+ maxTurns,
774
+ humanCheckpointAt,
775
+ currentTurn: 0,
776
+ lastOutputHash: null,
777
+ previousOutputHash: null,
778
+ status: "active",
779
+ chatId,
780
+ threadId,
781
+ };
782
+ await ctx.state.set({ scopeKind: "instance", stateKey: `loop_${chatId}_${threadId}` }, loop);
783
+ await sendMessage(ctx, token, chatId, [
784
+ `${escapeMarkdownV2("\ud83d\udcac")} *Discussion Started*`,
785
+ "",
786
+ `Topic: ${escapeMarkdownV2(topic)}`,
787
+ `Between: *${escapeMarkdownV2(loop.initiatorAgent)}* and *${escapeMarkdownV2(loop.targetAgent)}*`,
788
+ `Max turns: ${escapeMarkdownV2(String(maxTurns))}`,
789
+ humanCheckpointAt ? `Human checkpoint at turn: ${escapeMarkdownV2(String(humanCheckpointAt))}` : "",
790
+ ].filter(Boolean).join("\n"), { parseMode: "MarkdownV2", messageThreadId: threadId });
791
+ // Send initial message to target via correct transport
792
+ if (targetSession.transport === "native") {
793
+ await wakeAgentWithIssue(ctx, targetSession.agentId, companyId, `[Discussion: ${topic}] ${initialMessage}`, "discussion");
794
+ }
795
+ else {
796
+ ctx.events.emit(ACP_SPAWN_EVENT, companyId, {
797
+ type: "message",
798
+ sessionId: targetSession.sessionId,
799
+ chatId,
800
+ threadId,
801
+ text: `[Discussion: ${topic}] ${initialMessage}`,
802
+ });
803
+ }
804
+ return { content: JSON.stringify({ status: "started", loopId, maxTurns }) };
805
+ }
806
+ // --- Conversation loop continuation ---
807
+ async function checkConversationLoopContinuation(ctx, token, chatId, threadId, sessionId, text, done) {
808
+ const loop = await ctx.state.get({
809
+ scopeKind: "instance",
810
+ stateKey: `loop_${chatId}_${threadId}`,
811
+ });
812
+ if (!loop || loop.status !== "active")
813
+ return;
814
+ const isInitiator = sessionId === loop.initiatorSessionId;
815
+ const isTarget = sessionId === loop.targetSessionId;
816
+ if (!isInitiator && !isTarget)
817
+ return;
818
+ loop.currentTurn += 1;
819
+ // Stale loop detection
820
+ const outputHash = simpleHash(text);
821
+ if (outputHash === loop.lastOutputHash && outputHash === loop.previousOutputHash) {
822
+ loop.status = "paused";
823
+ await ctx.state.set({ scopeKind: "instance", stateKey: `loop_${chatId}_${threadId}` }, loop);
824
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\u26a0\ufe0f")} *Discussion Paused* \\- Stale loop detected \\(same output repeated\\)\\. Send a message to resume\\.`, { parseMode: "MarkdownV2", messageThreadId: threadId });
825
+ return;
826
+ }
827
+ loop.previousOutputHash = loop.lastOutputHash;
828
+ loop.lastOutputHash = outputHash;
829
+ if (loop.currentTurn >= loop.maxTurns) {
830
+ loop.status = "completed";
831
+ await ctx.state.set({ scopeKind: "instance", stateKey: `loop_${chatId}_${threadId}` }, loop);
832
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\u2705")} *Discussion Completed* \\- Reached ${escapeMarkdownV2(String(loop.maxTurns))} turns\\.`, { parseMode: "MarkdownV2", messageThreadId: threadId });
833
+ return;
834
+ }
835
+ if (loop.humanCheckpointAt && loop.currentTurn === loop.humanCheckpointAt) {
836
+ loop.status = "paused";
837
+ await ctx.state.set({ scopeKind: "instance", stateKey: `loop_${chatId}_${threadId}` }, loop);
838
+ await sendMessage(ctx, token, chatId, `${escapeMarkdownV2("\u270b")} *Discussion Paused* at turn ${escapeMarkdownV2(String(loop.currentTurn))} for human review\\. Send a message to resume\\.`, { parseMode: "MarkdownV2", messageThreadId: threadId });
839
+ return;
840
+ }
841
+ await ctx.state.set({ scopeKind: "instance", stateKey: `loop_${chatId}_${threadId}` }, loop);
842
+ // Route to the OTHER participant (only if not done)
843
+ if (!done) {
844
+ const nextSessionId = isInitiator ? loop.targetSessionId : loop.initiatorSessionId;
845
+ const sessions = await getSessions(ctx, chatId, threadId);
846
+ const nextSession = sessions.find((s) => s.sessionId === nextSessionId);
847
+ if (nextSession) {
848
+ const resolvedCompanyId = await resolveCompanyIdFromChat(ctx, chatId);
849
+ if (nextSession.transport === "native") {
850
+ await wakeAgentWithIssue(ctx, nextSession.agentId, resolvedCompanyId, `[Discussion: ${loop.topic}] ${text}`, "discussion_turn");
851
+ }
852
+ else {
853
+ ctx.events.emit(ACP_SPAWN_EVENT, resolvedCompanyId, {
854
+ type: "message",
855
+ sessionId: nextSessionId,
856
+ chatId,
857
+ threadId,
858
+ text: `[Discussion: ${loop.topic}] ${text}`,
859
+ });
860
+ }
861
+ }
862
+ }
863
+ }
864
+ // --- Session state helpers ---
865
+ export async function getSessions(ctx, chatId, threadId) {
866
+ const sessions = await ctx.state.get({
867
+ scopeKind: "instance",
868
+ stateKey: `sessions_${chatId}_${threadId}`,
869
+ });
870
+ return sessions ?? [];
871
+ }
872
+ async function saveSessions(ctx, chatId, threadId, sessions) {
873
+ await ctx.state.set({ scopeKind: "instance", stateKey: `sessions_${chatId}_${threadId}` }, sessions);
874
+ }
875
+ async function resolveCompanyIdFromChat(ctx, chatId) {
876
+ const mapping = await ctx.state.get({
877
+ scopeKind: "instance",
878
+ stateKey: `chat_${chatId}`,
879
+ });
880
+ return mapping?.companyId ?? mapping?.companyName ?? chatId;
881
+ }
882
+ function simpleHash(text) {
883
+ let hash = 0;
884
+ for (let i = 0; i < text.length; i++) {
885
+ const char = text.charCodeAt(i);
886
+ hash = ((hash << 5) - hash) + char;
887
+ hash = hash & hash;
888
+ }
889
+ return String(hash);
890
+ }
891
+ //# sourceMappingURL=acp-bridge.js.map