@rudderhq/server 0.2.10-canary.21 → 0.2.10-canary.23

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 (90) hide show
  1. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +2 -2
  2. package/dist/home-paths.d.ts.map +1 -1
  3. package/dist/home-paths.js +0 -2
  4. package/dist/home-paths.js.map +1 -1
  5. package/dist/services/automations.d.ts +16 -8
  6. package/dist/services/automations.d.ts.map +1 -1
  7. package/dist/services/automations.js +467 -0
  8. package/dist/services/automations.js.map +1 -1
  9. package/dist/services/automations.scheduler.d.ts.map +1 -1
  10. package/dist/services/automations.scheduler.js +5 -3
  11. package/dist/services/automations.scheduler.js.map +1 -1
  12. package/package.json +13 -13
  13. package/ui-dist/assets/{_basePickBy-B84mG_C-.js → _basePickBy-BkpCtA9I.js} +1 -1
  14. package/ui-dist/assets/{_baseUniq-C1wQfJWU.js → _baseUniq-Cqfp0Y05.js} +1 -1
  15. package/ui-dist/assets/{arc-kUpBd_xW.js → arc-CAvRWR2g.js} +1 -1
  16. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-DfZu-Jcn.js → architectureDiagram-2XIMDMQ5-BqApb8Dt.js} +1 -1
  17. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-ZYrIA2K3.js → blockDiagram-WCTKOSBZ-L4LKDnt4.js} +1 -1
  18. package/ui-dist/assets/{c4Diagram-IC4MRINW-BIFD9mx4.js → c4Diagram-IC4MRINW-BxelIf5q.js} +1 -1
  19. package/ui-dist/assets/channel-CFScBwqB.js +1 -0
  20. package/ui-dist/assets/{chunk-4BX2VUAB-BviJ7Prt.js → chunk-4BX2VUAB-Bo9koCOV.js} +1 -1
  21. package/ui-dist/assets/{chunk-55IACEB6-BZFEE6wA.js → chunk-55IACEB6-Ciu-E4bV.js} +1 -1
  22. package/ui-dist/assets/{chunk-FMBD7UC4-BmJPRdR-.js → chunk-FMBD7UC4-m_K7lHj7.js} +1 -1
  23. package/ui-dist/assets/{chunk-JSJVCQXG-A1WByaW-.js → chunk-JSJVCQXG-BMAoID-3.js} +1 -1
  24. package/ui-dist/assets/{chunk-KX2RTZJC-CIGYPhxY.js → chunk-KX2RTZJC-Bnzj9vMV.js} +1 -1
  25. package/ui-dist/assets/{chunk-NQ4KR5QH-Cok5N0qO.js → chunk-NQ4KR5QH-e0ztxumF.js} +1 -1
  26. package/ui-dist/assets/{chunk-QZHKN3VN-dDMpPYnn.js → chunk-QZHKN3VN-zT92DIr7.js} +1 -1
  27. package/ui-dist/assets/{chunk-WL4C6EOR-DNuPtZhH.js → chunk-WL4C6EOR-E0uvfCO_.js} +1 -1
  28. package/ui-dist/assets/classDiagram-VBA2DB6C-C3hMVnWD.js +1 -0
  29. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-C3hMVnWD.js +1 -0
  30. package/ui-dist/assets/clone-DYHqvdPg.js +1 -0
  31. package/ui-dist/assets/{cose-bilkent-S5V4N54A-Dw_CJHOQ.js → cose-bilkent-S5V4N54A-GOTYVnnS.js} +1 -1
  32. package/ui-dist/assets/{dagre-KLK3FWXG-Dooz_2G1.js → dagre-KLK3FWXG-ztBK7jay.js} +1 -1
  33. package/ui-dist/assets/{diagram-E7M64L7V-BLKOVAhJ.js → diagram-E7M64L7V-ZAd_Kar3.js} +1 -1
  34. package/ui-dist/assets/{diagram-IFDJBPK2-BF5WZ5g4.js → diagram-IFDJBPK2-B_BNio10.js} +1 -1
  35. package/ui-dist/assets/{diagram-P4PSJMXO-BKrdMkXn.js → diagram-P4PSJMXO-DRq9YLu7.js} +1 -1
  36. package/ui-dist/assets/{erDiagram-INFDFZHY-B__-ibvt.js → erDiagram-INFDFZHY-Dw5GNcjt.js} +1 -1
  37. package/ui-dist/assets/{flowDiagram-PKNHOUZH-C-hjm_OT.js → flowDiagram-PKNHOUZH-2roJUARo.js} +1 -1
  38. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-wxhez1p-.js → ganttDiagram-A5KZAMGK-DJsh7q7S.js} +1 -1
  39. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-Cp8kbn7i.js → gitGraphDiagram-K3NZZRJ6-nkF2y37-.js} +1 -1
  40. package/ui-dist/assets/{graph-DZiyJUXX.js → graph-Cq6d8e8y.js} +1 -1
  41. package/ui-dist/assets/{index-BKgV1Wq7.js → index-Az0vtSl5.js} +1 -1
  42. package/ui-dist/assets/{index-ChDA38Dg.js → index-B7Rs3U9E.js} +1 -1
  43. package/ui-dist/assets/{index-DHQjaSv-.js → index-BGvqZZus.js} +1 -1
  44. package/ui-dist/assets/{index-DyYghCWH.js → index-BOZpQE5r.js} +1 -1
  45. package/ui-dist/assets/{index-DIYJRvXK.js → index-BcbmwxPU.js} +1 -1
  46. package/ui-dist/assets/{index-CVUHbI_k.js → index-BdgGupnZ.js} +1 -1
  47. package/ui-dist/assets/{index-FnV0E58J.js → index-BlAwqa4z.js} +1 -1
  48. package/ui-dist/assets/{index-pL9dqAww.js → index-BnbFTxWW.js} +1 -1
  49. package/ui-dist/assets/{index-DgGp5c8D.js → index-BsxB2GPq.js} +1 -1
  50. package/ui-dist/assets/{index-Dz6brya1.js → index-C4aGjC4F.js} +1 -1
  51. package/ui-dist/assets/{index-C3fAZIeC.js → index-C7uubWUW.js} +1 -1
  52. package/ui-dist/assets/{index-Do-fiFQL.js → index-CD8-TtFO.js} +1 -1
  53. package/ui-dist/assets/{index-B9L9WlXc.js → index-CkhordR-.js} +1 -1
  54. package/ui-dist/assets/{index-C_QG-WJb.js → index-CxLzLREW.js} +1 -1
  55. package/ui-dist/assets/{index-DTWZM5-L.js → index-DVyrsahs.js} +1 -1
  56. package/ui-dist/assets/{index-CybAI1SK.js → index-DlSTwgn9.js} +381 -381
  57. package/ui-dist/assets/{index-Nb4qkCNr.js → index-Dy8jLd16.js} +1 -1
  58. package/ui-dist/assets/{index-MQscBKNJ.js → index-IFLmLdnr.js} +1 -1
  59. package/ui-dist/assets/{index-DH_L_PgP.js → index-N6tkbMmY.js} +1 -1
  60. package/ui-dist/assets/{index-BN8Y-wmy.js → index-Nz-p7NMR.js} +1 -1
  61. package/ui-dist/assets/index-d5KcP24K.css +1 -0
  62. package/ui-dist/assets/{index-DEZd60Ta.js → index-fSXacG5Q.js} +1 -1
  63. package/ui-dist/assets/{index-C8bqPhPM.js → index-gQ_OvebU.js} +1 -1
  64. package/ui-dist/assets/{index-Ch8tmO6B.js → index-wh0Gz0sG.js} +1 -1
  65. package/ui-dist/assets/{infoDiagram-LFFYTUFH-DoMYSWm0.js → infoDiagram-LFFYTUFH-DMUJrATb.js} +1 -1
  66. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-CHNtw8iI.js → ishikawaDiagram-PHBUUO56-vHMGLJ2T.js} +1 -1
  67. package/ui-dist/assets/{journeyDiagram-4ABVD52K-DKQdzarJ.js → journeyDiagram-4ABVD52K-BC_HQ0IG.js} +1 -1
  68. package/ui-dist/assets/{kanban-definition-K7BYSVSG-DBgiLRzL.js → kanban-definition-K7BYSVSG-v273KYAK.js} +1 -1
  69. package/ui-dist/assets/{layout-BRIMvAi6.js → layout-Cgdc8lyg.js} +1 -1
  70. package/ui-dist/assets/{linear-D4eTWwgG.js → linear-Cgxzsjwf.js} +1 -1
  71. package/ui-dist/assets/{mermaid.core-CV74wx1T.js → mermaid.core-XrKHApbf.js} +4 -4
  72. package/ui-dist/assets/{mindmap-definition-YRQLILUH-BupnhMjU.js → mindmap-definition-YRQLILUH-D-yOuH4F.js} +1 -1
  73. package/ui-dist/assets/{pieDiagram-SKSYHLDU-BCBWun2O.js → pieDiagram-SKSYHLDU-GQ_c_z0T.js} +1 -1
  74. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-BpLw3VwI.js → quadrantDiagram-337W2JSQ-B6tfJp-j.js} +1 -1
  75. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-DWbjrtUM.js → requirementDiagram-Z7DCOOCP-B5TqnFi9.js} +1 -1
  76. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-Dbi8ppiF.js → sankeyDiagram-WA2Y5GQK-C8mRofaz.js} +1 -1
  77. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-CMm2yoGY.js → sequenceDiagram-2WXFIKYE-DumD78j2.js} +1 -1
  78. package/ui-dist/assets/{stateDiagram-RAJIS63D-L6YwWQ2H.js → stateDiagram-RAJIS63D-BgvZMCvD.js} +1 -1
  79. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-CzNQLhBR.js +1 -0
  80. package/ui-dist/assets/{timeline-definition-YZTLITO2-C6gUqn7X.js → timeline-definition-YZTLITO2-jIqUNBCm.js} +1 -1
  81. package/ui-dist/assets/{treemap-KZPCXAKY-BwJ0Tkq9.js → treemap-KZPCXAKY-DBfVbJ77.js} +1 -1
  82. package/ui-dist/assets/{vennDiagram-LZ73GAT5-BFN1AzRi.js → vennDiagram-LZ73GAT5-CdDcPBiC.js} +1 -1
  83. package/ui-dist/assets/{xychartDiagram-JWTSCODW-Cj7hspvA.js → xychartDiagram-JWTSCODW-d-lmcTy1.js} +1 -1
  84. package/ui-dist/index.html +2 -2
  85. package/ui-dist/assets/channel-BMg_oQ6z.js +0 -1
  86. package/ui-dist/assets/classDiagram-VBA2DB6C-B46gbxWl.js +0 -1
  87. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-B46gbxWl.js +0 -1
  88. package/ui-dist/assets/clone-8p1TdYAS.js +0 -1
  89. package/ui-dist/assets/index-DW8GcqZn.css +0 -1
  90. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-D3owTYsb.js +0 -1
@@ -1,15 +1,23 @@
1
1
  import crypto from "node:crypto";
2
2
  import { and, asc, desc, eq, inArray, isNotNull, isNull, lte, ne, sql } from "drizzle-orm";
3
3
  import { agents, organizationSecrets, goals, heartbeatRuns, issues, projects, automationRuns, automations, automationTriggers, chatContextLinks, chatConversations, chatMessages, } from "@rudderhq/db";
4
+ import { chatIssueProposalFromStructuredPayload } from "@rudderhq/shared";
4
5
  import { conflict, forbidden, notFound, unauthorized, unprocessable } from "../errors.js";
6
+ import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
5
7
  import { logger } from "../middleware/logger.js";
6
8
  import { issueService } from "./issues.js";
9
+ import { chatService } from "./chats.js";
10
+ import { chatAssistantService, ChatAssistantStreamError } from "./chat-assistant.js";
11
+ import { claimChatGeneration, hasActiveChatGeneration } from "./chat-generation-locks.js";
7
12
  import { secretService } from "./secrets.js";
13
+ import { getStorageService } from "../storage/index.js";
8
14
  import { validateCron } from "./cron.js";
9
15
  import { heartbeatService } from "./heartbeat.js";
10
16
  import { queueIssueAssignmentWakeup } from "./issue-assignment-wakeup.js";
11
17
  import { logActivity } from "./activity-log.js";
18
+ import { publishLiveEvent } from "./live-events.js";
12
19
  import { assertTimeZone, LIVE_HEARTBEAT_RUN_STATUSES, MAX_CATCH_UP_RUNS, nextCronTickInTimeZone, nextResultText, normalizeWebhookTimestampMs, OPEN_ISSUE_STATUSES, } from "./automations.scheduler.js";
20
+ const CHAT_OUTPUT_STALE_RUN_MS = 30 * 60 * 1000;
13
21
  function toAutomation(row) {
14
22
  return {
15
23
  ...row,
@@ -18,8 +26,12 @@ function toAutomation(row) {
18
26
  }
19
27
  export function automationService(db, deps = {}) {
20
28
  const issueSvc = issueService(db);
29
+ const chatSvc = chatService(db);
30
+ const storageSvc = deps.storage ?? (deps.chatAssistant ? null : getStorageService());
31
+ const assistantSvc = deps.chatAssistant ?? chatAssistantService(db, storageSvc ?? undefined);
21
32
  const secretsSvc = secretService(db);
22
33
  const heartbeat = deps.heartbeat ?? heartbeatService(db);
34
+ const autoStartChatOutputRuns = deps.autoStartChatOutputRuns ?? true;
23
35
  async function getAutomationById(id) {
24
36
  return db
25
37
  .select()
@@ -142,6 +154,414 @@ export function automationService(db, deps = {}) {
142
154
  .where(eq(automationRuns.id, input.runId));
143
155
  return conversation.id;
144
156
  }
157
+ function automationChatPrompt(input) {
158
+ const base = input.automation.description?.trim() || input.automation.title.trim();
159
+ const context = [
160
+ `Automation: ${input.automation.title}`,
161
+ `Trigger source: ${input.source}`,
162
+ ];
163
+ if (input.payload && Object.keys(input.payload).length > 0) {
164
+ context.push(`Trigger payload: ${JSON.stringify(input.payload)}`);
165
+ }
166
+ return `${base}\n\n${context.join("\n")}`.trim();
167
+ }
168
+ function notifyChatChanged(orgId, conversationId, details) {
169
+ publishLiveEvent({
170
+ orgId,
171
+ type: "activity.logged",
172
+ payload: {
173
+ actorType: "system",
174
+ actorId: "automation-chat-output",
175
+ action: "chat.message_updated",
176
+ entityType: "chat",
177
+ entityId: conversationId,
178
+ agentId: null,
179
+ runId: null,
180
+ details: details ?? null,
181
+ },
182
+ });
183
+ }
184
+ async function logChatMessageAdded(input) {
185
+ await logActivity(db, {
186
+ orgId: input.orgId,
187
+ actorType: "system",
188
+ actorId: "automation-chat-output",
189
+ agentId: input.agentId ?? null,
190
+ action: "chat.message_added",
191
+ entityType: "chat",
192
+ entityId: input.conversationId,
193
+ details: {
194
+ messageId: input.message.id,
195
+ role: input.message.role,
196
+ kind: input.message.kind,
197
+ status: input.message.status,
198
+ source: "automation_chat_output",
199
+ },
200
+ });
201
+ }
202
+ function automationChatRunMetadata(automation, run, conversationId) {
203
+ return {
204
+ automationId: automation.id,
205
+ automationTitle: automation.title,
206
+ runId: run.id,
207
+ links: {
208
+ automation: `/automations/${automation.id}`,
209
+ chat: `/messenger/chat/${conversationId}`,
210
+ },
211
+ };
212
+ }
213
+ function proposedPlanDocumentPayload(structuredPayload) {
214
+ if (!structuredPayload)
215
+ return null;
216
+ const rawDocument = structuredPayload.planDocument
217
+ && typeof structuredPayload.planDocument === "object"
218
+ && !Array.isArray(structuredPayload.planDocument)
219
+ ? structuredPayload.planDocument
220
+ : structuredPayload.plan && typeof structuredPayload.plan === "object" && !Array.isArray(structuredPayload.plan)
221
+ ? structuredPayload.plan
222
+ : null;
223
+ return rawDocument ? rawDocument : null;
224
+ }
225
+ async function attachGeneratedFiles(input) {
226
+ if (!input.message || !input.generatedAttachments || input.generatedAttachments.length === 0) {
227
+ return input.message;
228
+ }
229
+ const attachments = [];
230
+ for (const generated of input.generatedAttachments) {
231
+ if (generated.body.length > MAX_ATTACHMENT_BYTES) {
232
+ throw new ChatAssistantStreamError(`Generated attachment exceeds ${MAX_ATTACHMENT_BYTES} bytes`, input.message.body, input.generatedAttachments);
233
+ }
234
+ const stored = await (storageSvc ?? getStorageService()).putFile({
235
+ orgId: input.conversation.orgId,
236
+ namespace: `chats/${input.conversation.id}/generated`,
237
+ originalFilename: generated.originalFilename,
238
+ contentType: generated.contentType,
239
+ body: generated.body,
240
+ });
241
+ const attachment = await chatSvc.createAttachment({
242
+ orgId: input.conversation.orgId,
243
+ conversationId: input.conversation.id,
244
+ messageId: input.message.id,
245
+ provider: stored.provider,
246
+ objectKey: stored.objectKey,
247
+ contentType: stored.contentType,
248
+ byteSize: stored.byteSize,
249
+ sha256: stored.sha256,
250
+ originalFilename: stored.originalFilename,
251
+ createdByAgentId: input.replyingAgentId ?? null,
252
+ createdByUserId: null,
253
+ });
254
+ attachments.push(attachment);
255
+ }
256
+ return {
257
+ ...input.message,
258
+ attachments: [...(input.message.attachments ?? []), ...attachments],
259
+ };
260
+ }
261
+ async function automationAssistantReplyPersistence(input) {
262
+ if (input.reply.kind === "issue_proposal") {
263
+ const proposal = chatIssueProposalFromStructuredPayload(input.reply.structuredPayload);
264
+ const planDocument = proposedPlanDocumentPayload(input.reply.structuredPayload);
265
+ const approval = await chatSvc.createProposalApproval(input.conversation.orgId, {
266
+ type: "chat_issue_creation",
267
+ requestedByUserId: null,
268
+ payload: {
269
+ chatConversationId: input.conversation.id,
270
+ proposedByAgentId: input.reply.replyingAgentId ?? input.conversation.preferredAgentId ?? null,
271
+ proposedIssue: proposal,
272
+ ...(planDocument ? { planDocument } : {}),
273
+ },
274
+ });
275
+ return {
276
+ kind: "issue_proposal",
277
+ approvalId: approval.id,
278
+ structuredPayload: input.reply.structuredPayload,
279
+ };
280
+ }
281
+ if (input.reply.kind === "operation_proposal") {
282
+ const approval = await chatSvc.createProposalApproval(input.conversation.orgId, {
283
+ type: "chat_operation",
284
+ requestedByUserId: null,
285
+ payload: {
286
+ chatConversationId: input.conversation.id,
287
+ operationProposal: input.reply.structuredPayload &&
288
+ typeof input.reply.structuredPayload.operationProposal === "object" &&
289
+ input.reply.structuredPayload.operationProposal !== null
290
+ ? input.reply.structuredPayload.operationProposal
291
+ : input.reply.structuredPayload ?? {},
292
+ },
293
+ });
294
+ return {
295
+ kind: "operation_proposal",
296
+ approvalId: approval.id,
297
+ structuredPayload: {
298
+ ...(input.reply.structuredPayload ?? {}),
299
+ operationProposalState: {
300
+ status: "pending",
301
+ decisionNote: null,
302
+ decidedByUserId: null,
303
+ decidedAt: null,
304
+ },
305
+ },
306
+ };
307
+ }
308
+ if (input.reply.kind === "ask_user") {
309
+ return {
310
+ kind: "ask_user",
311
+ approvalId: null,
312
+ structuredPayload: input.reply.structuredPayload,
313
+ };
314
+ }
315
+ return {
316
+ kind: "message",
317
+ approvalId: null,
318
+ structuredPayload: input.reply.structuredPayload,
319
+ };
320
+ }
321
+ async function expireStaleChatOutputRuns(automation, runId, executor = db) {
322
+ const staleBefore = new Date(Date.now() - CHAT_OUTPUT_STALE_RUN_MS);
323
+ const staleRuns = await executor
324
+ .select({
325
+ id: automationRuns.id,
326
+ linkedChatConversationId: automationRuns.linkedChatConversationId,
327
+ })
328
+ .from(automationRuns)
329
+ .where(and(eq(automationRuns.orgId, automation.orgId), eq(automationRuns.automationId, automation.id), ne(automationRuns.id, runId), inArray(automationRuns.status, ["received", "running"]), lte(automationRuns.updatedAt, staleBefore)));
330
+ for (const staleRun of staleRuns) {
331
+ if (staleRun.linkedChatConversationId && hasActiveChatGeneration(staleRun.linkedChatConversationId)) {
332
+ continue;
333
+ }
334
+ await executor
335
+ .update(automationRuns)
336
+ .set({
337
+ status: "failed",
338
+ failureReason: "Automation chat run was interrupted before completion",
339
+ completedAt: new Date(),
340
+ updatedAt: new Date(),
341
+ })
342
+ .where(eq(automationRuns.id, staleRun.id));
343
+ }
344
+ }
345
+ async function findLiveChatOutputRun(automation, runId, executor = db) {
346
+ await expireStaleChatOutputRuns(automation, runId, executor);
347
+ return executor
348
+ .select()
349
+ .from(automationRuns)
350
+ .where(and(eq(automationRuns.orgId, automation.orgId), eq(automationRuns.automationId, automation.id), ne(automationRuns.id, runId), inArray(automationRuns.status, ["received", "running"])))
351
+ .orderBy(desc(automationRuns.updatedAt), desc(automationRuns.createdAt))
352
+ .limit(1)
353
+ .then((rows) => rows[0] ?? null);
354
+ }
355
+ async function loadAutomationChatAssistantInput(conversationId) {
356
+ const conversation = await chatSvc.getById(conversationId);
357
+ if (!conversation)
358
+ throw notFound("Automation chat conversation not found");
359
+ const enriched = await assistantSvc.enrichConversation(conversation);
360
+ const messages = (await chatSvc.listMessages(conversationId)).filter((message) => !message.supersededAt);
361
+ const issueLabels = await issueSvc.listLabels(conversation.orgId);
362
+ return {
363
+ conversation: enriched,
364
+ messages,
365
+ contextLinks: (enriched.contextLinks ?? []),
366
+ issueLabels,
367
+ operatorProfile: null,
368
+ };
369
+ }
370
+ async function executeChatOutputAutomationRun(runId) {
371
+ const run = await getAutomationRunById(runId);
372
+ if (!run || run.status !== "running" || !run.linkedChatConversationId)
373
+ return;
374
+ const automation = await getAutomationById(run.automationId);
375
+ if (!automation || automation.outputMode !== "chat_output")
376
+ return;
377
+ const conversation = await chatSvc.getById(run.linkedChatConversationId);
378
+ if (!conversation) {
379
+ await finalizeRun(run.id, {
380
+ status: "failed",
381
+ failureReason: "Automation chat conversation not found",
382
+ completedAt: new Date(),
383
+ });
384
+ return;
385
+ }
386
+ const abortController = new AbortController();
387
+ const releaseGeneration = claimChatGeneration(conversation.id, abortController);
388
+ if (!releaseGeneration) {
389
+ await finalizeRun(run.id, {
390
+ status: "failed",
391
+ failureReason: "A chat reply is already being generated for this conversation",
392
+ completedAt: new Date(),
393
+ });
394
+ return;
395
+ }
396
+ const transcript = [];
397
+ let assistantDraftBody = "";
398
+ let assistantProgressMessage = null;
399
+ let assistantProgressMessageId = null;
400
+ let userMessage = null;
401
+ let lastRunProgressTouchMs = 0;
402
+ const touchRunChatProgress = async (messageId) => {
403
+ const nowMs = Date.now();
404
+ if (nowMs - lastRunProgressTouchMs < 15_000)
405
+ return;
406
+ lastRunProgressTouchMs = nowMs;
407
+ await db
408
+ .update(automationRuns)
409
+ .set({
410
+ lastChatMessageId: messageId,
411
+ updatedAt: new Date(nowMs),
412
+ })
413
+ .where(eq(automationRuns.id, run.id));
414
+ };
415
+ const persistProgress = async (progressConversation, status = "streaming", body = assistantDraftBody, replyingAgentId = progressConversation.chatRuntime?.runtimeAgentId ?? progressConversation.preferredAgentId ?? null, structuredPayload = null, kind = "message", approvalId = null) => {
416
+ if (!userMessage?.chatTurnId)
417
+ return null;
418
+ const input = {
419
+ kind,
420
+ status,
421
+ body,
422
+ transcript,
423
+ structuredPayload,
424
+ approvalId,
425
+ replyingAgentId,
426
+ };
427
+ if (assistantProgressMessage) {
428
+ const updated = await chatSvc.updateMessage(progressConversation.id, assistantProgressMessage.id, input);
429
+ if (updated) {
430
+ assistantProgressMessage = updated;
431
+ assistantProgressMessageId = assistantProgressMessage.id;
432
+ notifyChatChanged(progressConversation.orgId, progressConversation.id, {
433
+ messageId: assistantProgressMessage.id,
434
+ status,
435
+ });
436
+ await touchRunChatProgress(assistantProgressMessage.id);
437
+ return assistantProgressMessage;
438
+ }
439
+ }
440
+ assistantProgressMessage = await chatSvc.addMessage(progressConversation.id, {
441
+ orgId: progressConversation.orgId,
442
+ role: "assistant",
443
+ kind,
444
+ status,
445
+ body,
446
+ transcript,
447
+ structuredPayload,
448
+ approvalId,
449
+ replyingAgentId,
450
+ chatTurnId: userMessage.chatTurnId,
451
+ turnVariant: userMessage.turnVariant,
452
+ });
453
+ assistantProgressMessageId = assistantProgressMessage.id;
454
+ await logChatMessageAdded({
455
+ orgId: progressConversation.orgId,
456
+ conversationId: progressConversation.id,
457
+ message: assistantProgressMessage,
458
+ agentId: replyingAgentId,
459
+ });
460
+ await touchRunChatProgress(assistantProgressMessage.id);
461
+ return assistantProgressMessage;
462
+ };
463
+ try {
464
+ const prompt = automationChatPrompt({
465
+ automation,
466
+ source: run.source,
467
+ payload: run.triggerPayload,
468
+ });
469
+ userMessage = await chatSvc.addUserChatMessage(conversation.id, conversation.orgId, prompt);
470
+ await logChatMessageAdded({
471
+ orgId: conversation.orgId,
472
+ conversationId: conversation.id,
473
+ message: userMessage,
474
+ });
475
+ await finalizeRun(run.id, {
476
+ startedChatMessageId: userMessage.id,
477
+ lastChatMessageId: userMessage.id,
478
+ });
479
+ const assistantInput = await loadAutomationChatAssistantInput(conversation.id);
480
+ const streamed = await assistantSvc.streamChatAssistantReply({
481
+ ...assistantInput,
482
+ abortSignal: abortController.signal,
483
+ onAssistantDelta: async (delta) => {
484
+ assistantDraftBody = `${assistantDraftBody}${delta}`;
485
+ await persistProgress(assistantInput.conversation);
486
+ },
487
+ onAssistantState: async () => {
488
+ await persistProgress(assistantInput.conversation);
489
+ },
490
+ onTranscriptEntry: async (entry) => {
491
+ transcript.push(entry);
492
+ await persistProgress(assistantInput.conversation);
493
+ },
494
+ });
495
+ const finalStatus = streamed.outcome === "stopped" ? "stopped" : "completed";
496
+ const finalBody = streamed.outcome === "stopped" ? streamed.partialBody : streamed.reply.body;
497
+ const finalReplyPersistence = streamed.outcome === "completed"
498
+ ? await automationAssistantReplyPersistence({
499
+ conversation: assistantInput.conversation,
500
+ automation,
501
+ run,
502
+ reply: streamed.reply,
503
+ })
504
+ : {
505
+ kind: "message",
506
+ approvalId: null,
507
+ structuredPayload: null,
508
+ };
509
+ const finalMessage = await persistProgress(assistantInput.conversation, finalStatus, finalBody, streamed.replyingAgentId, {
510
+ ...(finalReplyPersistence.structuredPayload ?? {}),
511
+ automationChatRun: {
512
+ ...automationChatRunMetadata(automation, run, conversation.id),
513
+ status: streamed.outcome,
514
+ },
515
+ }, finalReplyPersistence.kind, finalReplyPersistence.approvalId);
516
+ const finalMessageWithAttachments = streamed.outcome === "completed"
517
+ ? await attachGeneratedFiles({
518
+ conversation: assistantInput.conversation,
519
+ message: finalMessage,
520
+ generatedAttachments: streamed.reply.generatedAttachments,
521
+ replyingAgentId: streamed.replyingAgentId,
522
+ })
523
+ : finalMessage;
524
+ await finalizeRun(run.id, {
525
+ status: finalStatus === "completed" ? "completed" : "failed",
526
+ terminalChatMessageId: finalMessageWithAttachments?.id ?? assistantProgressMessageId,
527
+ lastChatMessageId: finalMessageWithAttachments?.id ?? assistantProgressMessageId ?? userMessage.id,
528
+ completedAt: new Date(),
529
+ });
530
+ }
531
+ catch (error) {
532
+ const partialBody = error instanceof ChatAssistantStreamError ? error.partialBody : "";
533
+ const failureReason = error instanceof Error ? error.message : String(error);
534
+ const fallbackBody = partialBody.trim() || "Automation chat run failed before it produced a final response.";
535
+ const latestConversation = await chatSvc.getById(conversation.id);
536
+ const failedMessage = await persistProgress((latestConversation ?? conversation), "failed", fallbackBody, automation.assigneeAgentId, {
537
+ eventType: "automation_chat_run_result",
538
+ automationId: automation.id,
539
+ automationTitle: automation.title,
540
+ runId: run.id,
541
+ status: "failed",
542
+ failureReason,
543
+ links: {
544
+ automation: `/automations/${automation.id}`,
545
+ chat: `/messenger/chat/${conversation.id}`,
546
+ },
547
+ });
548
+ await finalizeRun(run.id, {
549
+ status: "failed",
550
+ failureReason,
551
+ terminalChatMessageId: failedMessage?.id ?? assistantProgressMessageId,
552
+ lastChatMessageId: failedMessage?.id ?? assistantProgressMessageId ?? userMessage?.id ?? null,
553
+ completedAt: new Date(),
554
+ });
555
+ logger.warn({ err: error, automationId: automation.id, runId: run.id }, "automation chat output run failed");
556
+ }
557
+ finally {
558
+ releaseGeneration();
559
+ notifyChatChanged(conversation.orgId, conversation.id, {
560
+ automationId: automation.id,
561
+ runId: run.id,
562
+ });
563
+ }
564
+ }
145
565
  async function listTriggersForAutomationIds(orgId, automationIds) {
146
566
  if (automationIds.length === 0)
147
567
  return new Map();
@@ -525,6 +945,46 @@ export function automationService(db, deps = {}) {
525
945
  : undefined;
526
946
  let createdIssue = null;
527
947
  try {
948
+ if (input.automation.outputMode === "chat_output") {
949
+ const activeRun = await findLiveChatOutputRun(input.automation, createdRun.id, txDb);
950
+ if (activeRun && input.automation.concurrencyPolicy !== "always_enqueue") {
951
+ const status = input.automation.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
952
+ const updated = await finalizeRun(createdRun.id, {
953
+ status,
954
+ linkedChatConversationId: activeRun.linkedChatConversationId,
955
+ coalescedIntoRunId: activeRun.id,
956
+ completedAt: triggeredAt,
957
+ }, txDb);
958
+ await updateAutomationTouchedState({
959
+ automationId: input.automation.id,
960
+ triggerId: input.trigger?.id ?? null,
961
+ triggeredAt,
962
+ status,
963
+ nextRunAt,
964
+ }, txDb);
965
+ return await getAutomationRunById(createdRun.id, txDb) ?? updated ?? createdRun;
966
+ }
967
+ const conversationId = await resolveAutomationRunChatConversationId({
968
+ automation: input.automation,
969
+ runId: createdRun.id,
970
+ executor: txDb,
971
+ });
972
+ if (!conversationId) {
973
+ throw new Error("Failed to create automation chat conversation");
974
+ }
975
+ const updated = await finalizeRun(createdRun.id, {
976
+ status: "running",
977
+ linkedChatConversationId: conversationId,
978
+ }, txDb);
979
+ await updateAutomationTouchedState({
980
+ automationId: input.automation.id,
981
+ triggerId: input.trigger?.id ?? null,
982
+ triggeredAt,
983
+ status: "running",
984
+ nextRunAt,
985
+ }, txDb);
986
+ return await getAutomationRunById(createdRun.id, txDb) ?? updated ?? createdRun;
987
+ }
528
988
  const activeIssue = await findLiveExecutionIssue(input.automation, txDb);
529
989
  if (activeIssue && input.automation.concurrencyPolicy !== "always_enqueue") {
530
990
  const status = input.automation.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
@@ -687,6 +1147,7 @@ export function automationService(db, deps = {}) {
687
1147
  triggerId: input.trigger?.id ?? null,
688
1148
  source: run.source,
689
1149
  status: run.status,
1150
+ outputMode: input.automation.outputMode,
690
1151
  },
691
1152
  });
692
1153
  }
@@ -694,11 +1155,17 @@ export function automationService(db, deps = {}) {
694
1155
  logger.warn({ err, automationId: input.automation.id, runId: run.id }, "failed to log automated run");
695
1156
  }
696
1157
  }
1158
+ if (autoStartChatOutputRuns && input.automation.outputMode === "chat_output" && run.status === "running") {
1159
+ void executeChatOutputAutomationRun(run.id).catch((err) => {
1160
+ logger.warn({ err, automationId: input.automation.id, runId: run.id }, "automation chat output worker failed");
1161
+ });
1162
+ }
697
1163
  return run;
698
1164
  }
699
1165
  return {
700
1166
  get: getAutomationById,
701
1167
  getTrigger: getTriggerById,
1168
+ executeChatOutputAutomationRun,
702
1169
  list: async (orgId) => {
703
1170
  const rows = await db
704
1171
  .select()