@rudderhq/server 0.2.0-canary.9 → 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 (192) hide show
  1. package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
  2. package/dist/bootstrap/register-api-routes.js +2 -0
  3. package/dist/bootstrap/register-api-routes.js.map +1 -1
  4. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js +8 -1
  5. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js.map +2 -2
  6. package/dist/bundled-plugins/plugin-linear/dist/worker.js +17 -3
  7. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +2 -2
  8. package/dist/home-paths.d.ts +2 -0
  9. package/dist/home-paths.d.ts.map +1 -1
  10. package/dist/home-paths.js +6 -1
  11. package/dist/home-paths.js.map +1 -1
  12. package/dist/index.d.ts +11 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +55 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/langfuse-transcript.d.ts.map +1 -1
  17. package/dist/langfuse-transcript.js +16 -2
  18. package/dist/langfuse-transcript.js.map +1 -1
  19. package/dist/middleware/auth.d.ts.map +1 -1
  20. package/dist/middleware/auth.js +54 -1
  21. package/dist/middleware/auth.js.map +1 -1
  22. package/dist/onboarding-assets/ceo/HEARTBEAT.md +8 -4
  23. package/dist/onboarding-assets/default/HEARTBEAT.md +7 -4
  24. package/dist/routes/agents.d.ts.map +1 -1
  25. package/dist/routes/agents.js +62 -3
  26. package/dist/routes/agents.js.map +1 -1
  27. package/dist/routes/approvals.d.ts.map +1 -1
  28. package/dist/routes/approvals.js +30 -1
  29. package/dist/routes/approvals.js.map +1 -1
  30. package/dist/routes/chats.d.ts.map +1 -1
  31. package/dist/routes/chats.js +285 -46
  32. package/dist/routes/chats.js.map +1 -1
  33. package/dist/routes/costs.d.ts.map +1 -1
  34. package/dist/routes/costs.js +20 -0
  35. package/dist/routes/costs.js.map +1 -1
  36. package/dist/routes/issues.d.ts.map +1 -1
  37. package/dist/routes/issues.js +229 -19
  38. package/dist/routes/issues.js.map +1 -1
  39. package/dist/routes/onboarding.d.ts +3 -0
  40. package/dist/routes/onboarding.d.ts.map +1 -0
  41. package/dist/routes/onboarding.js +545 -0
  42. package/dist/routes/onboarding.js.map +1 -0
  43. package/dist/routes/orgs.d.ts.map +1 -1
  44. package/dist/routes/orgs.js +22 -0
  45. package/dist/routes/orgs.js.map +1 -1
  46. package/dist/services/activity.d.ts.map +1 -1
  47. package/dist/services/activity.js +32 -1
  48. package/dist/services/activity.js.map +1 -1
  49. package/dist/services/agent-run-context.d.ts +1 -0
  50. package/dist/services/agent-run-context.d.ts.map +1 -1
  51. package/dist/services/agent-run-context.js +1 -0
  52. package/dist/services/agent-run-context.js.map +1 -1
  53. package/dist/services/agents.d.ts +13 -13
  54. package/dist/services/automations.d.ts +2 -2
  55. package/dist/services/calendar.d.ts +4 -4
  56. package/dist/services/chat-assistant.d.ts +10 -1
  57. package/dist/services/chat-assistant.d.ts.map +1 -1
  58. package/dist/services/chat-assistant.js +102 -3
  59. package/dist/services/chat-assistant.js.map +1 -1
  60. package/dist/services/chats.d.ts +87 -12
  61. package/dist/services/chats.d.ts.map +1 -1
  62. package/dist/services/chats.js +185 -10
  63. package/dist/services/chats.js.map +1 -1
  64. package/dist/services/costs.d.ts +21 -0
  65. package/dist/services/costs.d.ts.map +1 -1
  66. package/dist/services/costs.js +76 -2
  67. package/dist/services/costs.js.map +1 -1
  68. package/dist/services/finance.d.ts +2 -2
  69. package/dist/services/goals.d.ts +12 -12
  70. package/dist/services/instance-settings.d.ts.map +1 -1
  71. package/dist/services/instance-settings.js +25 -16
  72. package/dist/services/instance-settings.js.map +1 -1
  73. package/dist/services/issue-review-wakeup.d.ts +49 -1
  74. package/dist/services/issue-review-wakeup.d.ts.map +1 -1
  75. package/dist/services/issue-review-wakeup.js +39 -2
  76. package/dist/services/issue-review-wakeup.js.map +1 -1
  77. package/dist/services/issues.d.ts +2 -1
  78. package/dist/services/issues.d.ts.map +1 -1
  79. package/dist/services/issues.js +126 -5
  80. package/dist/services/issues.js.map +1 -1
  81. package/dist/services/knowledge-portability/organization-skills.d.ts +1 -0
  82. package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
  83. package/dist/services/knowledge-portability/organization-skills.js +3 -2
  84. package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
  85. package/dist/services/messenger.d.ts +5 -0
  86. package/dist/services/messenger.d.ts.map +1 -1
  87. package/dist/services/messenger.js +147 -9
  88. package/dist/services/messenger.js.map +1 -1
  89. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  90. package/dist/services/organization-workspace-browser.js +64 -9
  91. package/dist/services/organization-workspace-browser.js.map +1 -1
  92. package/dist/services/orgs.d.ts +1 -1
  93. package/dist/services/plugin-registry.d.ts +4 -4
  94. package/dist/services/projects.d.ts +1 -1
  95. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  96. package/dist/services/runtime-kernel/heartbeat.js +567 -29
  97. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  98. package/dist/services/secrets.d.ts +5 -5
  99. package/dist/services/workspace-backups.d.ts.map +1 -1
  100. package/dist/services/workspace-backups.js +6 -0
  101. package/dist/services/workspace-backups.js.map +1 -1
  102. package/dist/services/workspace-runtime.d.ts.map +1 -1
  103. package/dist/services/workspace-runtime.js +2 -0
  104. package/dist/services/workspace-runtime.js.map +1 -1
  105. package/package.json +13 -13
  106. package/resources/bundled-skills/rudder/SKILL.md +72 -7
  107. package/resources/bundled-skills/rudder/references/cli-reference.md +34 -9
  108. package/resources/bundled-skills/rudder/references/organization-skills.md +12 -7
  109. package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +1 -0
  110. package/skills/rudder/SKILL.md +72 -7
  111. package/skills/rudder/references/cli-reference.md +34 -9
  112. package/skills/rudder/references/organization-skills.md +12 -7
  113. package/skills/rudder-create-agent/references/cli-reference.md +1 -0
  114. package/ui-dist/assets/{_basePickBy-aX2f6dVl.js → _basePickBy-3Hg7N37c.js} +1 -1
  115. package/ui-dist/assets/{_baseUniq-BYwL7heN.js → _baseUniq-Bvy8WJh0.js} +1 -1
  116. package/ui-dist/assets/{arc-BG9f5pwY.js → arc-DrmvGX4U.js} +1 -1
  117. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-BFFQoJJ1.js → architectureDiagram-2XIMDMQ5-vbevcV-8.js} +1 -1
  118. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-Bvx1IB1z.js → blockDiagram-WCTKOSBZ-DvupMRN9.js} +1 -1
  119. package/ui-dist/assets/{c4Diagram-IC4MRINW-DJbCE4sh.js → c4Diagram-IC4MRINW-CbsNVA8e.js} +1 -1
  120. package/ui-dist/assets/channel-DhW0A-FV.js +1 -0
  121. package/ui-dist/assets/{chunk-4BX2VUAB-BOVbLFsN.js → chunk-4BX2VUAB-BL4OUqNV.js} +1 -1
  122. package/ui-dist/assets/{chunk-55IACEB6-D5pKj6S9.js → chunk-55IACEB6-DFwq2ebc.js} +1 -1
  123. package/ui-dist/assets/{chunk-FMBD7UC4-OY5xuJeR.js → chunk-FMBD7UC4-Cyl6kF9G.js} +1 -1
  124. package/ui-dist/assets/{chunk-JSJVCQXG-C5X67KZg.js → chunk-JSJVCQXG-v4mfLtsY.js} +1 -1
  125. package/ui-dist/assets/{chunk-KX2RTZJC-C-4PZ9Q_.js → chunk-KX2RTZJC-Bfg48g5k.js} +1 -1
  126. package/ui-dist/assets/{chunk-NQ4KR5QH-XysPlqPj.js → chunk-NQ4KR5QH-BcSdbequ.js} +1 -1
  127. package/ui-dist/assets/{chunk-QZHKN3VN-B5wEbFHo.js → chunk-QZHKN3VN-BT8QI712.js} +1 -1
  128. package/ui-dist/assets/{chunk-WL4C6EOR-BanwYFa2.js → chunk-WL4C6EOR-CqH2or9g.js} +1 -1
  129. package/ui-dist/assets/classDiagram-VBA2DB6C-Bw6kzUsz.js +1 -0
  130. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-Bw6kzUsz.js +1 -0
  131. package/ui-dist/assets/clone-Luak8Fsn.js +1 -0
  132. package/ui-dist/assets/{cose-bilkent-S5V4N54A-Cd4q2swD.js → cose-bilkent-S5V4N54A-CLH06Lnz.js} +1 -1
  133. package/ui-dist/assets/{dagre-KLK3FWXG-B_VyOhf3.js → dagre-KLK3FWXG-DxNQPDBj.js} +1 -1
  134. package/ui-dist/assets/{diagram-E7M64L7V-BRoG4Mz6.js → diagram-E7M64L7V-BOcSeWh0.js} +1 -1
  135. package/ui-dist/assets/{diagram-IFDJBPK2-CRU_A9p9.js → diagram-IFDJBPK2-DXyaFKVr.js} +1 -1
  136. package/ui-dist/assets/{diagram-P4PSJMXO-BYSQDbfb.js → diagram-P4PSJMXO-DhY_ls3C.js} +1 -1
  137. package/ui-dist/assets/{erDiagram-INFDFZHY-v5j1kyWr.js → erDiagram-INFDFZHY-QtL5Yt_b.js} +1 -1
  138. package/ui-dist/assets/{flowDiagram-PKNHOUZH-C06ZQgTj.js → flowDiagram-PKNHOUZH-BYqyaowc.js} +1 -1
  139. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-Dw9p5nQ1.js → ganttDiagram-A5KZAMGK-D4xd7J_z.js} +1 -1
  140. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-CrpXRIaP.js → gitGraphDiagram-K3NZZRJ6-Co9xqKNH.js} +1 -1
  141. package/ui-dist/assets/{graph-ClTUmULf.js → graph-DEC7S98H.js} +1 -1
  142. package/ui-dist/assets/{index-sLGLHxIu.js → index-4_gJOU3u.js} +1 -1
  143. package/ui-dist/assets/{index-Cr7n11UG.js → index-B8QjK4Xd.js} +1 -1
  144. package/ui-dist/assets/index-BLDnKx7N.js +1478 -0
  145. package/ui-dist/assets/{index-DxzAgTWd.js → index-BX6QyxsL.js} +1 -1
  146. package/ui-dist/assets/{index-CIr7H9OI.js → index-BZGiyL9p.js} +1 -1
  147. package/ui-dist/assets/{index-DK13xhRv.js → index-BelfAyHh.js} +1 -1
  148. package/ui-dist/assets/index-BisI78wU.css +1 -0
  149. package/ui-dist/assets/{index-D-6z8wxx.js → index-Bm86s0IY.js} +1 -1
  150. package/ui-dist/assets/{index-T81awgzh.js → index-Bz0jEwWG.js} +1 -1
  151. package/ui-dist/assets/{index-CqYInp-c.js → index-CFANc8oH.js} +1 -1
  152. package/ui-dist/assets/{index-Btwy7Cp-.js → index-CIAMqUzr.js} +1 -1
  153. package/ui-dist/assets/{index-L6M3nVxh.js → index-ClrueuiI.js} +1 -1
  154. package/ui-dist/assets/{index-C_BTFRTX.js → index-CpxwEuIg.js} +1 -1
  155. package/ui-dist/assets/{index-CQWmziMF.js → index-D1ZkASZY.js} +1 -1
  156. package/ui-dist/assets/{index-DWFMs9uk.js → index-DUP0i_Iv.js} +1 -1
  157. package/ui-dist/assets/{index-BVfM9ax8.js → index-DawkXomB.js} +1 -1
  158. package/ui-dist/assets/{index-DNlWBtHa.js → index-DxchV0Z7.js} +1 -1
  159. package/ui-dist/assets/{index-DkDkjZ-D.js → index-Dzd88G_H.js} +1 -1
  160. package/ui-dist/assets/{index-BvGogi9q.js → index-SklGX83C.js} +1 -1
  161. package/ui-dist/assets/{index-Bpc2gRVo.js → index-_xX3B4n0.js} +1 -1
  162. package/ui-dist/assets/{index-_x9smX4T.js → index-bVqVfFu5.js} +1 -1
  163. package/ui-dist/assets/{index-DAhPD1Ss.js → index-eIjkqSkc.js} +1 -1
  164. package/ui-dist/assets/{index-4uxadHo5.js → index-mIrYeZR2.js} +1 -1
  165. package/ui-dist/assets/{index-BYC_xlrx.js → index-xg2FQeSA.js} +1 -1
  166. package/ui-dist/assets/{infoDiagram-LFFYTUFH-BA3VxOIU.js → infoDiagram-LFFYTUFH-BQ0qsBJ6.js} +1 -1
  167. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-DGrizi0S.js → ishikawaDiagram-PHBUUO56-B1u2RAnY.js} +1 -1
  168. package/ui-dist/assets/{journeyDiagram-4ABVD52K-6ey34a7e.js → journeyDiagram-4ABVD52K-Dv5wJGwT.js} +1 -1
  169. package/ui-dist/assets/{kanban-definition-K7BYSVSG-CwNnmsam.js → kanban-definition-K7BYSVSG-CJOykCsT.js} +1 -1
  170. package/ui-dist/assets/{layout-buNxvllr.js → layout-BDcM6t-f.js} +1 -1
  171. package/ui-dist/assets/{linear-BPWhxaRl.js → linear-B9Sm5Y96.js} +1 -1
  172. package/ui-dist/assets/{mermaid.core-Cajx0s-z.js → mermaid.core-lZPaf_Ix.js} +4 -4
  173. package/ui-dist/assets/{mindmap-definition-YRQLILUH-Bf5InEp-.js → mindmap-definition-YRQLILUH-Cu4HfP8K.js} +1 -1
  174. package/ui-dist/assets/{pieDiagram-SKSYHLDU-CZFz7NWC.js → pieDiagram-SKSYHLDU-B_v-Vluc.js} +1 -1
  175. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-XBmKVoc9.js → quadrantDiagram-337W2JSQ-BU1ZwGcS.js} +1 -1
  176. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-BkgdDv0H.js → requirementDiagram-Z7DCOOCP-DBOqB50G.js} +1 -1
  177. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-CASFR28i.js → sankeyDiagram-WA2Y5GQK-CsXDIOlq.js} +1 -1
  178. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-BzY7LMRv.js → sequenceDiagram-2WXFIKYE-Cmgr7vKy.js} +1 -1
  179. package/ui-dist/assets/{stateDiagram-RAJIS63D-C9UMSk36.js → stateDiagram-RAJIS63D-Bd0uRbWd.js} +1 -1
  180. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-qGaY7iN1.js +1 -0
  181. package/ui-dist/assets/{timeline-definition-YZTLITO2-D6m4R4xe.js → timeline-definition-YZTLITO2-B9OfCgYQ.js} +1 -1
  182. package/ui-dist/assets/{treemap-KZPCXAKY-7V9mnT8T.js → treemap-KZPCXAKY-FWWMNo03.js} +1 -1
  183. package/ui-dist/assets/{vennDiagram-LZ73GAT5-Ci-sfAyq.js → vennDiagram-LZ73GAT5-CGs3T7cn.js} +1 -1
  184. package/ui-dist/assets/{xychartDiagram-JWTSCODW-BayXhRSu.js → xychartDiagram-JWTSCODW-BJ6DrP1k.js} +1 -1
  185. package/ui-dist/index.html +2 -2
  186. package/ui-dist/assets/channel-ClX7n84B.js +0 -1
  187. package/ui-dist/assets/classDiagram-VBA2DB6C-DvWbsnVz.js +0 -1
  188. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-DvWbsnVz.js +0 -1
  189. package/ui-dist/assets/clone-Dla3A8ZA.js +0 -1
  190. package/ui-dist/assets/index-CSANx6ee.css +0 -1
  191. package/ui-dist/assets/index-DCa9-Sy-.js +0 -1439
  192. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DWVhbAdj.js +0 -1
@@ -3,14 +3,14 @@ import { Router } from "express";
3
3
  import multer from "multer";
4
4
  import { addChatMessageSchema, updateChatConversationUserStateSchema, convertChatToIssueSchema, createChatAttachmentMetadataSchema, createChatContextLinkSchema, createChatConversationSchema, resolveChatOperationProposalSchema, setChatProjectContextSchema, updateChatConversationSchema, } from "@rudderhq/shared";
5
5
  import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
6
- import { HttpError } from "../errors.js";
6
+ import { forbidden, HttpError, unauthorized } from "../errors.js";
7
7
  import { observeExecutionEvent, updateExecutionObservation, updateExecutionTraceIO, withExecutionObservation, } from "../langfuse.js";
8
8
  import { emitExecutionTranscriptTree } from "../langfuse-transcript.js";
9
9
  import { validate } from "../middleware/validate.js";
10
10
  import { logger } from "../middleware/logger.js";
11
11
  import { ChatAssistantStreamError, chatAssistantService, } from "../services/chat-assistant.js";
12
- import { cancelActiveChatGeneration, claimChatGeneration } from "../services/chat-generation-locks.js";
13
- import { agentService, chatService, operatorProfileService, organizationService, goalService, issueService, logActivity, projectService, } from "../services/index.js";
12
+ import { cancelActiveChatGeneration, claimChatGeneration, hasActiveChatGeneration } from "../services/chat-generation-locks.js";
13
+ import { accessService, agentService, chatService, operatorProfileService, organizationService, goalService, issueService, logActivity, projectService, } from "../services/index.js";
14
14
  import { summarizeRuntimeSkillsForTrace } from "../services/runtime-trace-metadata.js";
15
15
  import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
16
16
  export function chatRoutes(db, storage) {
@@ -21,6 +21,7 @@ export function chatRoutes(db, storage) {
21
21
  const projectsSvc = projectService(db);
22
22
  const agentsSvc = agentService(db);
23
23
  const goalsSvc = goalService(db);
24
+ const access = accessService(db);
24
25
  const assistantSvc = chatAssistantService(db);
25
26
  const operatorProfiles = operatorProfileService(db);
26
27
  const upload = multer({
@@ -84,6 +85,36 @@ export function chatRoutes(db, storage) {
84
85
  assertBoard(req);
85
86
  return req.actor.userId ?? "local-board";
86
87
  }
88
+ function canCreateAgentsLegacy(agent) {
89
+ if (agent.role === "ceo")
90
+ return true;
91
+ if (!agent.permissions || typeof agent.permissions !== "object")
92
+ return false;
93
+ return Boolean(agent.permissions.canCreateAgents);
94
+ }
95
+ async function assertCanAssignTasks(req, orgId) {
96
+ assertCompanyAccess(req, orgId);
97
+ if (req.actor.type === "board") {
98
+ if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
99
+ return;
100
+ const allowed = await access.canUser(orgId, req.actor.userId, "tasks:assign");
101
+ if (!allowed)
102
+ throw forbidden("Missing permission: tasks:assign");
103
+ return;
104
+ }
105
+ if (req.actor.type === "agent") {
106
+ if (!req.actor.agentId)
107
+ throw forbidden("Agent authentication required");
108
+ const allowedByGrant = await access.hasPermission(orgId, "agent", req.actor.agentId, "tasks:assign");
109
+ if (allowedByGrant)
110
+ return;
111
+ const actorAgent = await agentsSvc.getById(req.actor.agentId);
112
+ if (actorAgent && actorAgent.orgId === orgId && canCreateAgentsLegacy(actorAgent))
113
+ return;
114
+ throw forbidden("Missing permission: tasks:assign");
115
+ }
116
+ throw unauthorized();
117
+ }
87
118
  function buildChatObservabilityContext(conversation, input) {
88
119
  return {
89
120
  surface: input.surface ?? "chat_action",
@@ -153,6 +184,11 @@ export function chatRoutes(db, storage) {
153
184
  eventType: typeof systemPayload?.eventType === "string" ? systemPayload.eventType : null,
154
185
  };
155
186
  }
187
+ function modelTurnInputFromInvocationMeta(invocationMeta) {
188
+ return typeof invocationMeta.prompt === "string" && invocationMeta.prompt.trim().length > 0
189
+ ? invocationMeta.prompt
190
+ : undefined;
191
+ }
156
192
  function buildChatTraceInput(input, invocationMeta) {
157
193
  return {
158
194
  conversationId: input.conversationId,
@@ -367,25 +403,121 @@ export function chatRoutes(db, storage) {
367
403
  ? structuredPayload.issueProposal
368
404
  : structuredPayload;
369
405
  }
370
- async function persistAssistantReply(conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation)) {
406
+ function proposalAssignsOrReviewsIssue(proposal) {
407
+ if (!proposal)
408
+ return false;
409
+ return Boolean((typeof proposal.assigneeAgentId === "string" && proposal.assigneeAgentId.trim().length > 0)
410
+ || (typeof proposal.assigneeUserId === "string" && proposal.assigneeUserId.trim().length > 0)
411
+ || (typeof proposal.reviewerAgentId === "string" && proposal.reviewerAgentId.trim().length > 0)
412
+ || (typeof proposal.reviewerUserId === "string" && proposal.reviewerUserId.trim().length > 0));
413
+ }
414
+ async function proposedIssuePayloadForConversion(conversationId, input) {
415
+ if (input.proposal)
416
+ return proposedIssuePayload(input.proposal);
417
+ if (input.messageId) {
418
+ const message = await svc.getMessage(conversationId, input.messageId);
419
+ return proposedIssuePayload(message?.structuredPayload ?? null);
420
+ }
421
+ const messages = await svc.listMessages(conversationId);
422
+ const message = [...messages].reverse().find((entry) => entry.kind === "issue_proposal");
423
+ return proposedIssuePayload(message?.structuredPayload ?? null);
424
+ }
425
+ async function assertCanConvertIssueProposal(req, conversation, input) {
426
+ const proposal = await proposedIssuePayloadForConversion(conversation.id, input);
427
+ if (proposalAssignsOrReviewsIssue(proposal)) {
428
+ await assertCanAssignTasks(req, conversation.orgId);
429
+ }
430
+ }
431
+ function proposedPlanDocumentPayload(structuredPayload) {
432
+ if (!structuredPayload)
433
+ return null;
434
+ const rawDocument = structuredPayload.planDocument
435
+ && typeof structuredPayload.planDocument === "object"
436
+ && !Array.isArray(structuredPayload.planDocument)
437
+ ? structuredPayload.planDocument
438
+ : structuredPayload.plan && typeof structuredPayload.plan === "object" && !Array.isArray(structuredPayload.plan)
439
+ ? structuredPayload.plan
440
+ : null;
441
+ return rawDocument ? rawDocument : null;
442
+ }
443
+ async function persistAssistantReply(req, conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation), existingMessageId) {
371
444
  const createdMessages = [];
372
445
  const { chatTurnId, turnVariant } = turnContext;
446
+ const attachGeneratedFiles = async (message, generatedAttachments) => {
447
+ if (!generatedAttachments || generatedAttachments.length === 0)
448
+ return message;
449
+ const attachments = [];
450
+ for (const generated of generatedAttachments) {
451
+ if (generated.body.length > MAX_ATTACHMENT_BYTES) {
452
+ throw new ChatAssistantStreamError(`Generated attachment exceeds ${MAX_ATTACHMENT_BYTES} bytes`, assistantReply.body, generatedAttachments);
453
+ }
454
+ const stored = await storage.putFile({
455
+ orgId: conversation.orgId,
456
+ namespace: `chats/${conversation.id}/generated`,
457
+ originalFilename: generated.originalFilename,
458
+ contentType: generated.contentType,
459
+ body: generated.body,
460
+ });
461
+ const attachment = await svc.createAttachment({
462
+ orgId: conversation.orgId,
463
+ conversationId: conversation.id,
464
+ messageId: message.id,
465
+ provider: stored.provider,
466
+ objectKey: stored.objectKey,
467
+ contentType: stored.contentType,
468
+ byteSize: stored.byteSize,
469
+ sha256: stored.sha256,
470
+ originalFilename: stored.originalFilename,
471
+ createdByAgentId: replyingAgentId,
472
+ createdByUserId: null,
473
+ });
474
+ attachments.push(attachment);
475
+ }
476
+ return {
477
+ ...message,
478
+ attachments: [...(message.attachments ?? []), ...attachments],
479
+ };
480
+ };
481
+ const saveAssistantMessage = async (input) => {
482
+ if (existingMessageId) {
483
+ const updated = await svc.updateMessage(conversation.id, existingMessageId, {
484
+ kind: input.kind,
485
+ status: "completed",
486
+ body: input.body,
487
+ structuredPayload: input.structuredPayload ?? null,
488
+ transcript,
489
+ approvalId: input.approvalId ?? null,
490
+ replyingAgentId,
491
+ });
492
+ if (updated)
493
+ return updated;
494
+ }
495
+ return svc.addMessage(conversation.id, {
496
+ orgId: conversation.orgId,
497
+ role: "assistant",
498
+ kind: input.kind,
499
+ body: input.body,
500
+ structuredPayload: input.structuredPayload ?? null,
501
+ transcript,
502
+ approvalId: input.approvalId ?? null,
503
+ replyingAgentId,
504
+ chatTurnId,
505
+ turnVariant,
506
+ });
507
+ };
373
508
  if (assistantReply.kind === "issue_proposal") {
374
509
  const issueProposalStructuredPayload = withDefaultIssueProposalAssignee(assistantReply.structuredPayload, await defaultIssueAssigneeAgentId(conversation));
375
- const shouldAutoCreateIssue = conversation.planMode || conversation.issueCreationMode === "auto_create";
510
+ const shouldAutoCreateIssue = !conversation.planMode && conversation.issueCreationMode === "auto_create";
376
511
  if (shouldAutoCreateIssue) {
377
- const proposalMessage = await svc.addMessage(conversation.id, {
378
- orgId: conversation.orgId,
379
- role: "assistant",
512
+ const proposalMessage = await saveAssistantMessage({
380
513
  kind: "issue_proposal",
381
514
  body: assistantReply.body,
382
515
  structuredPayload: issueProposalStructuredPayload,
383
- transcript,
384
- replyingAgentId,
385
- chatTurnId,
386
- turnVariant,
387
516
  });
388
- createdMessages.push(proposalMessage);
517
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
518
+ await assertCanConvertIssueProposal(req, conversation, {
519
+ proposal: issueProposalStructuredPayload,
520
+ });
389
521
  const issue = await svc.convertToIssue(conversation.id, {
390
522
  actorUserId: actor.actorType === "user" ? actor.actorId : null,
391
523
  messageId: proposalMessage.id,
@@ -414,32 +546,28 @@ export function chatRoutes(db, storage) {
414
546
  details: {
415
547
  issueId: issue.id,
416
548
  issueIdentifier: issue.identifier,
417
- source: conversation.planMode ? "plan_mode" : "auto_create",
549
+ source: "auto_create",
418
550
  },
419
551
  });
420
552
  return createdMessages;
421
553
  }
554
+ const planDocument = proposedPlanDocumentPayload(issueProposalStructuredPayload);
422
555
  const approval = await svc.createProposalApproval(conversation.orgId, {
423
556
  type: "chat_issue_creation",
424
557
  requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
425
558
  payload: {
426
559
  chatConversationId: conversation.id,
427
560
  proposedIssue: proposedIssuePayload(issueProposalStructuredPayload),
561
+ ...(planDocument ? { planDocument } : {}),
428
562
  },
429
563
  });
430
- const proposalMessage = await svc.addMessage(conversation.id, {
431
- orgId: conversation.orgId,
432
- role: "assistant",
564
+ const proposalMessage = await saveAssistantMessage({
433
565
  kind: "issue_proposal",
434
566
  body: assistantReply.body,
435
567
  structuredPayload: issueProposalStructuredPayload,
436
- transcript,
437
568
  approvalId: approval.id,
438
- replyingAgentId,
439
- chatTurnId,
440
- turnVariant,
441
569
  });
442
- createdMessages.push(proposalMessage);
570
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
443
571
  return createdMessages;
444
572
  }
445
573
  if (assistantReply.kind === "operation_proposal") {
@@ -455,9 +583,7 @@ export function chatRoutes(db, storage) {
455
583
  : assistantReply.structuredPayload,
456
584
  },
457
585
  });
458
- const proposalMessage = await svc.addMessage(conversation.id, {
459
- orgId: conversation.orgId,
460
- role: "assistant",
586
+ const proposalMessage = await saveAssistantMessage({
461
587
  kind: "operation_proposal",
462
588
  body: assistantReply.body,
463
589
  structuredPayload: {
@@ -469,41 +595,80 @@ export function chatRoutes(db, storage) {
469
595
  decidedAt: null,
470
596
  },
471
597
  },
472
- transcript,
473
598
  approvalId: approval.id,
474
- replyingAgentId,
475
- chatTurnId,
476
- turnVariant,
477
599
  });
478
- createdMessages.push(proposalMessage);
600
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
479
601
  return createdMessages;
480
602
  }
481
- const assistantMessage = await svc.addMessage(conversation.id, {
482
- orgId: conversation.orgId,
483
- role: "assistant",
603
+ const assistantMessage = await saveAssistantMessage({
484
604
  kind: assistantReply.kind === "routing_suggestion" ? "routing_suggestion" : "message",
485
605
  body: assistantReply.body,
486
606
  structuredPayload: assistantReply.structuredPayload,
487
- transcript,
488
- replyingAgentId,
489
- chatTurnId,
490
- turnVariant,
491
607
  });
492
- createdMessages.push(assistantMessage);
608
+ createdMessages.push(await attachGeneratedFiles(assistantMessage, assistantReply.generatedAttachments));
493
609
  return createdMessages;
494
610
  }
495
- async function persistPartialAssistantMessage(conversation, body, status, turnContext, transcript = [], replyingAgentId = chatReplyingAgentId(conversation)) {
611
+ async function attachGeneratedFilesToPartialMessage(conversation, message, generatedAttachments, replyingAgentId) {
612
+ if (!message || !generatedAttachments || generatedAttachments.length === 0)
613
+ return message;
614
+ const attachments = [];
615
+ for (const generated of generatedAttachments) {
616
+ if (generated.body.length > MAX_ATTACHMENT_BYTES)
617
+ continue;
618
+ const stored = await storage.putFile({
619
+ orgId: conversation.orgId,
620
+ namespace: `chats/${conversation.id}/generated`,
621
+ originalFilename: generated.originalFilename,
622
+ contentType: generated.contentType,
623
+ body: generated.body,
624
+ });
625
+ const attachment = await svc.createAttachment({
626
+ orgId: conversation.orgId,
627
+ conversationId: conversation.id,
628
+ messageId: message.id,
629
+ provider: stored.provider,
630
+ objectKey: stored.objectKey,
631
+ contentType: stored.contentType,
632
+ byteSize: stored.byteSize,
633
+ sha256: stored.sha256,
634
+ originalFilename: stored.originalFilename,
635
+ createdByAgentId: replyingAgentId,
636
+ createdByUserId: null,
637
+ });
638
+ attachments.push(attachment);
639
+ }
640
+ return {
641
+ ...message,
642
+ attachments: [...(message.attachments ?? []), ...attachments],
643
+ };
644
+ }
645
+ async function persistPartialAssistantMessage(conversation, body, status, turnContext, transcript = [], replyingAgentId = chatReplyingAgentId(conversation), existingMessageId) {
496
646
  const trimmed = body.trim();
497
- if (!trimmed)
647
+ const fallbackBody = status === "stopped"
648
+ ? "Chat run stopped before a final reply. Continue the conversation to resume from the preserved context."
649
+ : "Chat run failed before a final reply. Continue the conversation to resume from the preserved context.";
650
+ const durableBody = trimmed || (transcript.length > 0 ? fallbackBody : "");
651
+ if (!durableBody)
498
652
  return null;
499
653
  const chatTurnId = turnContext?.chatTurnId ?? randomUUID();
500
654
  const turnVariant = turnContext?.turnVariant ?? 0;
655
+ if (existingMessageId) {
656
+ const updated = await svc.updateMessage(conversation.id, existingMessageId, {
657
+ kind: "message",
658
+ status,
659
+ body: durableBody,
660
+ transcript,
661
+ replyingAgentId,
662
+ });
663
+ if (updated)
664
+ return updated;
665
+ }
501
666
  const message = await svc.addMessage(conversation.id, {
502
667
  orgId: conversation.orgId,
503
668
  role: "assistant",
504
669
  kind: "message",
505
670
  status,
506
- body: trimmed,
671
+ body: durableBody,
507
672
  transcript,
508
673
  replyingAgentId,
509
674
  chatTurnId,
@@ -524,8 +689,9 @@ export function chatRoutes(db, storage) {
524
689
  const status = statusParam === "resolved" || statusParam === "archived" || statusParam === "all"
525
690
  ? statusParam
526
691
  : "active";
692
+ const q = typeof req.query.q === "string" ? req.query.q : undefined;
527
693
  const userId = req.actor.type === "board" ? (req.actor.userId ?? "local-board") : null;
528
- const conversations = await svc.list(orgId, { status }, userId);
694
+ const conversations = await svc.list(orgId, { status, q }, userId);
529
695
  res.json(await assistantSvc.enrichConversations(conversations));
530
696
  });
531
697
  router.post("/orgs/:orgId/chats", validate(createChatConversationSchema), async (req, res) => {
@@ -538,6 +704,13 @@ export function chatRoutes(db, storage) {
538
704
  }
539
705
  const contextLinks = req.body.contextLinks ?? [];
540
706
  await assertContextLinksBelongToCompany(orgId, contextLinks);
707
+ if (req.body.preferredAgentId) {
708
+ const agent = await agentsSvc.getById(req.body.preferredAgentId);
709
+ if (!agent || agent.orgId !== orgId) {
710
+ res.status(422).json({ error: "Preferred agent must belong to the same organization" });
711
+ return;
712
+ }
713
+ }
541
714
  const actor = getActorInfo(req);
542
715
  const conversation = await svc.create(orgId, {
543
716
  title: req.body.title,
@@ -629,6 +802,9 @@ export function chatRoutes(db, storage) {
629
802
  res.status(404).json({ error: "Chat conversation not found" });
630
803
  return;
631
804
  }
805
+ if (!hasActiveChatGeneration(conversation.id)) {
806
+ await svc.markInterruptedStreamingMessages(conversation.id);
807
+ }
632
808
  const messages = await svc.listMessages(conversation.id);
633
809
  res.json(messages);
634
810
  });
@@ -678,6 +854,7 @@ export function chatRoutes(db, storage) {
678
854
  const assistantInput = await loadAssistantInput(conversation, actor);
679
855
  const transcript = [];
680
856
  const observedTranscript = [];
857
+ let modelTurnInput;
681
858
  let fallbackOutput = null;
682
859
  let finalChatOutput = null;
683
860
  let finalChatStatus = "completed";
@@ -685,6 +862,7 @@ export function chatRoutes(db, storage) {
685
862
  const streamed = await assistantSvc.streamChatAssistantReply({
686
863
  ...assistantInput,
687
864
  onInvocationMeta: async (meta) => {
865
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
688
866
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
689
867
  mergeChatInvocationTraceMetadata(chatObservation, meta);
690
868
  updateExecutionObservation(observation, chatObservation, {
@@ -704,7 +882,7 @@ export function chatRoutes(db, storage) {
704
882
  finalChatStatus = "failed";
705
883
  throw new Error("Chat assistant reply was stopped before completion");
706
884
  }
707
- const created = await persistAssistantReply(assistantInput.conversation, actor, streamed.reply, turnContext, transcript, streamed.replyingAgentId);
885
+ const created = await persistAssistantReply(req, assistantInput.conversation, actor, streamed.reply, turnContext, transcript, streamed.replyingAgentId);
708
886
  finalChatOutput = streamed.reply.body;
709
887
  await logChatMessagesAdded(assistantInput.conversation, created, {
710
888
  actorType: "system",
@@ -735,6 +913,7 @@ export function chatRoutes(db, storage) {
735
913
  context: chatObservation,
736
914
  parentObservation: observation,
737
915
  transcript: observedTranscript,
916
+ initialTurnInput: modelTurnInput,
738
917
  fallbackResult: fallbackOutput
739
918
  ? {
740
919
  output: fallbackOutput,
@@ -779,6 +958,9 @@ export function chatRoutes(db, storage) {
779
958
  });
780
959
  }
781
960
  logger.warn({ err, conversationId: conversation.id }, "chat assistant reply failed");
961
+ if (err instanceof HttpError) {
962
+ throw err;
963
+ }
782
964
  res.status(502).json({
783
965
  error: err instanceof Error ? err.message : "Chat assistant failed to respond",
784
966
  });
@@ -837,6 +1019,42 @@ export function chatRoutes(db, storage) {
837
1019
  let chatObservation = null;
838
1020
  const transcript = [];
839
1021
  const observedTranscript = [];
1022
+ let modelTurnInput;
1023
+ let assistantProgressMessage = null;
1024
+ let assistantProgressMessageId = null;
1025
+ let assistantDraftBody = "";
1026
+ const persistStreamProgress = async (progressConversation, replyingAgentId = chatReplyingAgentId(progressConversation)) => {
1027
+ if (!turnContextForPartial)
1028
+ return null;
1029
+ const input = {
1030
+ kind: "message",
1031
+ status: "streaming",
1032
+ body: assistantDraftBody,
1033
+ transcript,
1034
+ replyingAgentId,
1035
+ };
1036
+ if (assistantProgressMessage) {
1037
+ const updated = await svc.updateMessage(progressConversation.id, assistantProgressMessage.id, input);
1038
+ if (updated) {
1039
+ assistantProgressMessage = updated;
1040
+ assistantProgressMessageId = assistantProgressMessage.id;
1041
+ return assistantProgressMessage;
1042
+ }
1043
+ }
1044
+ assistantProgressMessage = await svc.addMessage(progressConversation.id, {
1045
+ orgId: progressConversation.orgId,
1046
+ role: "assistant",
1047
+ kind: "message",
1048
+ status: "streaming",
1049
+ body: assistantDraftBody,
1050
+ transcript,
1051
+ replyingAgentId,
1052
+ chatTurnId: turnContextForPartial.chatTurnId,
1053
+ turnVariant: turnContextForPartial.turnVariant,
1054
+ });
1055
+ assistantProgressMessageId = assistantProgressMessage.id;
1056
+ return assistantProgressMessage;
1057
+ };
840
1058
  let clientClosed = false;
841
1059
  const handleClosed = () => {
842
1060
  if (clientClosed || res.writableEnded)
@@ -894,6 +1112,7 @@ export function chatRoutes(db, storage) {
894
1112
  ...assistantInput,
895
1113
  abortSignal: abortController.signal,
896
1114
  onInvocationMeta: async (meta) => {
1115
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
897
1116
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
898
1117
  mergeChatInvocationTraceMetadata(chatObservation, meta);
899
1118
  updateExecutionObservation(observation, chatObservation, {
@@ -902,12 +1121,17 @@ export function chatRoutes(db, storage) {
902
1121
  updateExecutionTraceIO(observation, { input: currentChatTraceInput });
903
1122
  },
904
1123
  onAssistantDelta: async (delta) => {
1124
+ assistantDraftBody = `${assistantDraftBody}${delta}`;
1125
+ await persistStreamProgress(assistantInput.conversation);
1126
+ if (clientClosed)
1127
+ return;
905
1128
  writeStreamEvent(res, {
906
1129
  type: "assistant_delta",
907
1130
  delta,
908
1131
  });
909
1132
  },
910
1133
  onAssistantState: async (state) => {
1134
+ await persistStreamProgress(assistantInput.conversation);
911
1135
  if (clientClosed)
912
1136
  return;
913
1137
  writeStreamEvent(res, {
@@ -917,6 +1141,7 @@ export function chatRoutes(db, storage) {
917
1141
  },
918
1142
  onTranscriptEntry: async (entry) => {
919
1143
  transcript.push(entry);
1144
+ await persistStreamProgress(assistantInput.conversation);
920
1145
  if (clientClosed)
921
1146
  return;
922
1147
  writeStreamEvent(res, {
@@ -931,7 +1156,7 @@ export function chatRoutes(db, storage) {
931
1156
  if (streamed.outcome === "stopped") {
932
1157
  finalChatStatus = "stopped";
933
1158
  finalChatOutput = streamed.partialBody;
934
- const stoppedMessage = await persistPartialAssistantMessage(assistantInput.conversation, streamed.partialBody, "stopped", turnContextForPartial, transcript, streamed.replyingAgentId);
1159
+ const stoppedMessage = await persistPartialAssistantMessage(assistantInput.conversation, streamed.partialBody, "stopped", turnContextForPartial, transcript, streamed.replyingAgentId, assistantProgressMessageId);
935
1160
  if (stoppedMessage) {
936
1161
  await logChatMessagesAdded(assistantInput.conversation, [stoppedMessage], {
937
1162
  actorType: "system",
@@ -957,7 +1182,7 @@ export function chatRoutes(db, storage) {
957
1182
  }
958
1183
  return;
959
1184
  }
960
- const createdMessages = await persistAssistantReply(assistantInput.conversation, actor, streamed.reply, turnContextForPartial, transcript, streamed.replyingAgentId);
1185
+ const createdMessages = await persistAssistantReply(req, assistantInput.conversation, actor, streamed.reply, turnContextForPartial, transcript, streamed.replyingAgentId, assistantProgressMessageId);
961
1186
  finalChatOutput = streamed.reply.body;
962
1187
  await logChatMessagesAdded(assistantInput.conversation, createdMessages, {
963
1188
  actorType: "system",
@@ -993,6 +1218,14 @@ export function chatRoutes(db, storage) {
993
1218
  context: chatObservation,
994
1219
  parentObservation: observation,
995
1220
  transcript: observedTranscript,
1221
+ initialTurnInput: modelTurnInput,
1222
+ fallbackResult: finalChatOutput
1223
+ ? {
1224
+ output: finalChatOutput,
1225
+ subtype: finalChatStatus,
1226
+ isError: finalChatStatus === "failed",
1227
+ }
1228
+ : null,
996
1229
  });
997
1230
  finalChatOutput = finalChatOutput ?? transcriptStats.finalOutput ?? null;
998
1231
  }
@@ -1020,8 +1253,10 @@ export function chatRoutes(db, storage) {
1020
1253
  }
1021
1254
  catch (err) {
1022
1255
  const partialBody = err instanceof ChatAssistantStreamError ? err.partialBody : "";
1256
+ const generatedAttachments = err instanceof ChatAssistantStreamError ? err.generatedAttachments : [];
1023
1257
  const failedReplyingAgentId = chatReplyingAgentId(assistantConversationForPartial);
1024
- const failedMessage = await persistPartialAssistantMessage(assistantConversationForPartial ?? conversation, partialBody, "failed", turnContextForPartial, transcript, failedReplyingAgentId).catch(() => null);
1258
+ let failedMessage = await persistPartialAssistantMessage(assistantConversationForPartial ?? conversation, partialBody, "failed", turnContextForPartial, transcript, failedReplyingAgentId, assistantProgressMessageId).catch(() => null);
1259
+ failedMessage = await attachGeneratedFilesToPartialMessage(assistantConversationForPartial ?? conversation, failedMessage, generatedAttachments, failedReplyingAgentId).catch(() => failedMessage);
1025
1260
  if (failedMessage && assistantConversationForPartial) {
1026
1261
  await logChatMessagesAdded(assistantConversationForPartial, [failedMessage], {
1027
1262
  actorType: "system",
@@ -1219,6 +1454,10 @@ export function chatRoutes(db, storage) {
1219
1454
  return;
1220
1455
  }
1221
1456
  }
1457
+ await assertCanConvertIssueProposal(req, conversation, {
1458
+ messageId: req.body.messageId ?? null,
1459
+ proposal: req.body.proposal ?? null,
1460
+ });
1222
1461
  const chatObservation = buildChatObservabilityContext(conversation, {
1223
1462
  rootExecutionId: req.body.messageId ?? `chat-convert:${conversation.id}`,
1224
1463
  trigger: "convert_to_issue",