@rudderhq/server 0.2.0-canary.8 → 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 (199) 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 +124 -117
  7. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +3 -3
  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 +343 -53
  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 -32
  50. package/dist/services/agent-run-context.d.ts.map +1 -1
  51. package/dist/services/agent-run-context.js +26 -128
  52. package/dist/services/agent-run-context.js.map +1 -1
  53. package/dist/services/agents.d.ts +26 -26
  54. package/dist/services/agents.d.ts.map +1 -1
  55. package/dist/services/agents.js +1 -42
  56. package/dist/services/agents.js.map +1 -1
  57. package/dist/services/assets.d.ts +2 -2
  58. package/dist/services/automations.d.ts +2 -2
  59. package/dist/services/calendar.d.ts +4 -4
  60. package/dist/services/chat-assistant.d.ts +12 -3
  61. package/dist/services/chat-assistant.d.ts.map +1 -1
  62. package/dist/services/chat-assistant.js +126 -99
  63. package/dist/services/chat-assistant.js.map +1 -1
  64. package/dist/services/chats.d.ts +88 -15
  65. package/dist/services/chats.d.ts.map +1 -1
  66. package/dist/services/chats.js +217 -14
  67. package/dist/services/chats.js.map +1 -1
  68. package/dist/services/costs.d.ts +21 -0
  69. package/dist/services/costs.d.ts.map +1 -1
  70. package/dist/services/costs.js +76 -2
  71. package/dist/services/costs.js.map +1 -1
  72. package/dist/services/finance.d.ts +2 -2
  73. package/dist/services/goals.d.ts +12 -12
  74. package/dist/services/instance-settings.d.ts.map +1 -1
  75. package/dist/services/instance-settings.js +25 -16
  76. package/dist/services/instance-settings.js.map +1 -1
  77. package/dist/services/issue-review-wakeup.d.ts +49 -1
  78. package/dist/services/issue-review-wakeup.d.ts.map +1 -1
  79. package/dist/services/issue-review-wakeup.js +39 -2
  80. package/dist/services/issue-review-wakeup.js.map +1 -1
  81. package/dist/services/issues.d.ts +2 -1
  82. package/dist/services/issues.d.ts.map +1 -1
  83. package/dist/services/issues.js +126 -5
  84. package/dist/services/issues.js.map +1 -1
  85. package/dist/services/knowledge-portability/organization-skills.d.ts +1 -0
  86. package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
  87. package/dist/services/knowledge-portability/organization-skills.js +3 -2
  88. package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
  89. package/dist/services/messenger.d.ts +5 -0
  90. package/dist/services/messenger.d.ts.map +1 -1
  91. package/dist/services/messenger.js +165 -11
  92. package/dist/services/messenger.js.map +1 -1
  93. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  94. package/dist/services/organization-workspace-browser.js +64 -9
  95. package/dist/services/organization-workspace-browser.js.map +1 -1
  96. package/dist/services/orgs.d.ts +1 -13
  97. package/dist/services/orgs.d.ts.map +1 -1
  98. package/dist/services/orgs.js +0 -2
  99. package/dist/services/orgs.js.map +1 -1
  100. package/dist/services/plugin-registry.d.ts +4 -4
  101. package/dist/services/projects.d.ts +1 -1
  102. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  103. package/dist/services/runtime-kernel/heartbeat.js +567 -29
  104. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  105. package/dist/services/secrets.d.ts +5 -5
  106. package/dist/services/workspace-backups.d.ts.map +1 -1
  107. package/dist/services/workspace-backups.js +6 -0
  108. package/dist/services/workspace-backups.js.map +1 -1
  109. package/dist/services/workspace-runtime.d.ts.map +1 -1
  110. package/dist/services/workspace-runtime.js +2 -0
  111. package/dist/services/workspace-runtime.js.map +1 -1
  112. package/package.json +13 -13
  113. package/resources/bundled-skills/rudder/SKILL.md +72 -7
  114. package/resources/bundled-skills/rudder/references/cli-reference.md +34 -9
  115. package/resources/bundled-skills/rudder/references/organization-skills.md +12 -7
  116. package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +1 -0
  117. package/skills/rudder/SKILL.md +72 -7
  118. package/skills/rudder/references/cli-reference.md +34 -9
  119. package/skills/rudder/references/organization-skills.md +12 -7
  120. package/skills/rudder-create-agent/references/cli-reference.md +1 -0
  121. package/ui-dist/assets/{_basePickBy-DfISC403.js → _basePickBy-3Hg7N37c.js} +1 -1
  122. package/ui-dist/assets/{_baseUniq-s98gUpgR.js → _baseUniq-Bvy8WJh0.js} +1 -1
  123. package/ui-dist/assets/{arc-CR17323Q.js → arc-DrmvGX4U.js} +1 -1
  124. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-BzXAflU_.js → architectureDiagram-2XIMDMQ5-vbevcV-8.js} +1 -1
  125. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-8TtjJ3eI.js → blockDiagram-WCTKOSBZ-DvupMRN9.js} +1 -1
  126. package/ui-dist/assets/{c4Diagram-IC4MRINW-CpDAvPEk.js → c4Diagram-IC4MRINW-CbsNVA8e.js} +1 -1
  127. package/ui-dist/assets/channel-DhW0A-FV.js +1 -0
  128. package/ui-dist/assets/{chunk-4BX2VUAB-DH5y5IcG.js → chunk-4BX2VUAB-BL4OUqNV.js} +1 -1
  129. package/ui-dist/assets/{chunk-55IACEB6-HUQ0oHmD.js → chunk-55IACEB6-DFwq2ebc.js} +1 -1
  130. package/ui-dist/assets/{chunk-FMBD7UC4-B_Tmn_aT.js → chunk-FMBD7UC4-Cyl6kF9G.js} +1 -1
  131. package/ui-dist/assets/{chunk-JSJVCQXG-COsiBBS-.js → chunk-JSJVCQXG-v4mfLtsY.js} +1 -1
  132. package/ui-dist/assets/{chunk-KX2RTZJC-Cvg_71ig.js → chunk-KX2RTZJC-Bfg48g5k.js} +1 -1
  133. package/ui-dist/assets/{chunk-NQ4KR5QH-BEW2kuUb.js → chunk-NQ4KR5QH-BcSdbequ.js} +1 -1
  134. package/ui-dist/assets/{chunk-QZHKN3VN-Bna2V6NY.js → chunk-QZHKN3VN-BT8QI712.js} +1 -1
  135. package/ui-dist/assets/{chunk-WL4C6EOR-DZ8gtX-v.js → chunk-WL4C6EOR-CqH2or9g.js} +1 -1
  136. package/ui-dist/assets/classDiagram-VBA2DB6C-Bw6kzUsz.js +1 -0
  137. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-Bw6kzUsz.js +1 -0
  138. package/ui-dist/assets/clone-Luak8Fsn.js +1 -0
  139. package/ui-dist/assets/{cose-bilkent-S5V4N54A-kr92BRef.js → cose-bilkent-S5V4N54A-CLH06Lnz.js} +1 -1
  140. package/ui-dist/assets/{dagre-KLK3FWXG-D_g61aoF.js → dagre-KLK3FWXG-DxNQPDBj.js} +1 -1
  141. package/ui-dist/assets/{diagram-E7M64L7V-1LfSj6n8.js → diagram-E7M64L7V-BOcSeWh0.js} +1 -1
  142. package/ui-dist/assets/{diagram-IFDJBPK2-DK364c5M.js → diagram-IFDJBPK2-DXyaFKVr.js} +1 -1
  143. package/ui-dist/assets/{diagram-P4PSJMXO-CIq22L7l.js → diagram-P4PSJMXO-DhY_ls3C.js} +1 -1
  144. package/ui-dist/assets/{erDiagram-INFDFZHY-C2QX1WzN.js → erDiagram-INFDFZHY-QtL5Yt_b.js} +1 -1
  145. package/ui-dist/assets/{flowDiagram-PKNHOUZH-BXk0KTTB.js → flowDiagram-PKNHOUZH-BYqyaowc.js} +1 -1
  146. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-D75CuaPG.js → ganttDiagram-A5KZAMGK-D4xd7J_z.js} +1 -1
  147. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-C_SvdzO5.js → gitGraphDiagram-K3NZZRJ6-Co9xqKNH.js} +1 -1
  148. package/ui-dist/assets/{graph-DTC5egc-.js → graph-DEC7S98H.js} +1 -1
  149. package/ui-dist/assets/{index-CEKv8ksn.js → index-4_gJOU3u.js} +1 -1
  150. package/ui-dist/assets/{index-CSzr8t_E.js → index-B8QjK4Xd.js} +1 -1
  151. package/ui-dist/assets/index-BLDnKx7N.js +1478 -0
  152. package/ui-dist/assets/{index-CqKwXFnI.js → index-BX6QyxsL.js} +1 -1
  153. package/ui-dist/assets/{index-DmSssfNa.js → index-BZGiyL9p.js} +1 -1
  154. package/ui-dist/assets/{index-BLvouPW7.js → index-BelfAyHh.js} +1 -1
  155. package/ui-dist/assets/index-BisI78wU.css +1 -0
  156. package/ui-dist/assets/{index-BUyOio4T.js → index-Bm86s0IY.js} +1 -1
  157. package/ui-dist/assets/{index-DLL28Pwh.js → index-Bz0jEwWG.js} +1 -1
  158. package/ui-dist/assets/{index--j9y8Fm1.js → index-CFANc8oH.js} +1 -1
  159. package/ui-dist/assets/{index-CjmpD1gl.js → index-CIAMqUzr.js} +1 -1
  160. package/ui-dist/assets/{index-lXemkB72.js → index-ClrueuiI.js} +1 -1
  161. package/ui-dist/assets/{index-D9UMKH7j.js → index-CpxwEuIg.js} +1 -1
  162. package/ui-dist/assets/{index-BI5DxFdZ.js → index-D1ZkASZY.js} +1 -1
  163. package/ui-dist/assets/{index-BkXN5tcA.js → index-DUP0i_Iv.js} +1 -1
  164. package/ui-dist/assets/{index-Be2KDBoW.js → index-DawkXomB.js} +1 -1
  165. package/ui-dist/assets/{index-CO0DiQaO.js → index-DxchV0Z7.js} +1 -1
  166. package/ui-dist/assets/{index-Bk9vEAxA.js → index-Dzd88G_H.js} +1 -1
  167. package/ui-dist/assets/{index-CyIEQyck.js → index-SklGX83C.js} +1 -1
  168. package/ui-dist/assets/{index-DL9Ygef7.js → index-_xX3B4n0.js} +1 -1
  169. package/ui-dist/assets/{index-8HHVhW-a.js → index-bVqVfFu5.js} +1 -1
  170. package/ui-dist/assets/{index-Dl-MnVEo.js → index-eIjkqSkc.js} +1 -1
  171. package/ui-dist/assets/{index-CNkqdDmO.js → index-mIrYeZR2.js} +1 -1
  172. package/ui-dist/assets/{index-CXBstvbt.js → index-xg2FQeSA.js} +1 -1
  173. package/ui-dist/assets/{infoDiagram-LFFYTUFH-DN2iczkC.js → infoDiagram-LFFYTUFH-BQ0qsBJ6.js} +1 -1
  174. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56--Ku5ltXY.js → ishikawaDiagram-PHBUUO56-B1u2RAnY.js} +1 -1
  175. package/ui-dist/assets/{journeyDiagram-4ABVD52K-9wOJKEaq.js → journeyDiagram-4ABVD52K-Dv5wJGwT.js} +1 -1
  176. package/ui-dist/assets/{kanban-definition-K7BYSVSG-joaPa9x3.js → kanban-definition-K7BYSVSG-CJOykCsT.js} +1 -1
  177. package/ui-dist/assets/{layout-bH9QuYBa.js → layout-BDcM6t-f.js} +1 -1
  178. package/ui-dist/assets/{linear-CnfJ09Re.js → linear-B9Sm5Y96.js} +1 -1
  179. package/ui-dist/assets/{mermaid.core-CYkJAFnD.js → mermaid.core-lZPaf_Ix.js} +4 -4
  180. package/ui-dist/assets/{mindmap-definition-YRQLILUH-BQyC_8UY.js → mindmap-definition-YRQLILUH-Cu4HfP8K.js} +1 -1
  181. package/ui-dist/assets/{pieDiagram-SKSYHLDU-ev5uLtwW.js → pieDiagram-SKSYHLDU-B_v-Vluc.js} +1 -1
  182. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-CQoe3akb.js → quadrantDiagram-337W2JSQ-BU1ZwGcS.js} +1 -1
  183. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-C9fd5-6o.js → requirementDiagram-Z7DCOOCP-DBOqB50G.js} +1 -1
  184. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-Dznsr9L_.js → sankeyDiagram-WA2Y5GQK-CsXDIOlq.js} +1 -1
  185. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-jL7GufkU.js → sequenceDiagram-2WXFIKYE-Cmgr7vKy.js} +1 -1
  186. package/ui-dist/assets/{stateDiagram-RAJIS63D-C_4YyDYV.js → stateDiagram-RAJIS63D-Bd0uRbWd.js} +1 -1
  187. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-qGaY7iN1.js +1 -0
  188. package/ui-dist/assets/{timeline-definition-YZTLITO2-D8jKeAEo.js → timeline-definition-YZTLITO2-B9OfCgYQ.js} +1 -1
  189. package/ui-dist/assets/{treemap-KZPCXAKY-CV4vBepp.js → treemap-KZPCXAKY-FWWMNo03.js} +1 -1
  190. package/ui-dist/assets/{vennDiagram-LZ73GAT5-DAk_ThVs.js → vennDiagram-LZ73GAT5-CGs3T7cn.js} +1 -1
  191. package/ui-dist/assets/{xychartDiagram-JWTSCODW-D8wg1dlm.js → xychartDiagram-JWTSCODW-BJ6DrP1k.js} +1 -1
  192. package/ui-dist/index.html +2 -2
  193. package/ui-dist/assets/channel-BDMh8XFu.js +0 -1
  194. package/ui-dist/assets/classDiagram-VBA2DB6C-B6kvnvP_.js +0 -1
  195. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-B6kvnvP_.js +0 -1
  196. package/ui-dist/assets/clone-B5CU-MlO.js +0 -1
  197. package/ui-dist/assets/index-CLXVCM5X.js +0 -1434
  198. package/ui-dist/assets/index-DoUUx1qN.css +0 -1
  199. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-BAU1MhTJ.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,
@@ -313,24 +349,175 @@ export function chatRoutes(db, storage) {
313
349
  function chatReplyingAgentId(conversation) {
314
350
  return conversation?.chatRuntime?.runtimeAgentId ?? conversation?.preferredAgentId ?? null;
315
351
  }
316
- async function persistAssistantReply(conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation)) {
352
+ async function defaultIssueAssigneeAgentId(conversation) {
353
+ const candidateAgentIds = [conversation?.preferredAgentId, conversation?.routedAgentId]
354
+ .filter((id) => typeof id === "string" && id.trim().length > 0);
355
+ if (!conversation)
356
+ return null;
357
+ for (const candidateAgentId of candidateAgentIds) {
358
+ const agent = await agentsSvc.getById(candidateAgentId);
359
+ if (!agent || agent.orgId !== conversation.orgId)
360
+ continue;
361
+ if (agent.status === "pending_approval" || agent.status === "terminated")
362
+ continue;
363
+ return agent.id;
364
+ }
365
+ return null;
366
+ }
367
+ function hasIssueProposalAssignee(proposal) {
368
+ return Boolean((typeof proposal.assigneeAgentId === "string" && proposal.assigneeAgentId.trim().length > 0)
369
+ || (typeof proposal.assigneeUserId === "string" && proposal.assigneeUserId.trim().length > 0));
370
+ }
371
+ function withDefaultIssueProposalAssignee(structuredPayload, assigneeAgentId) {
372
+ if (!structuredPayload || !assigneeAgentId)
373
+ return structuredPayload ?? null;
374
+ const nestedProposal = structuredPayload.issueProposal
375
+ && typeof structuredPayload.issueProposal === "object"
376
+ && !Array.isArray(structuredPayload.issueProposal)
377
+ ? structuredPayload.issueProposal
378
+ : null;
379
+ const proposal = nestedProposal ?? structuredPayload;
380
+ if (hasIssueProposalAssignee(proposal))
381
+ return structuredPayload;
382
+ if (nestedProposal) {
383
+ return {
384
+ ...structuredPayload,
385
+ issueProposal: {
386
+ ...nestedProposal,
387
+ assigneeAgentId,
388
+ },
389
+ };
390
+ }
391
+ return {
392
+ ...structuredPayload,
393
+ assigneeAgentId,
394
+ };
395
+ }
396
+ function proposedIssuePayload(structuredPayload) {
397
+ if (!structuredPayload)
398
+ return structuredPayload ?? null;
399
+ return structuredPayload.issueProposal
400
+ && typeof structuredPayload.issueProposal === "object"
401
+ && !Array.isArray(structuredPayload.issueProposal)
402
+ && structuredPayload.issueProposal !== null
403
+ ? structuredPayload.issueProposal
404
+ : structuredPayload;
405
+ }
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) {
317
444
  const createdMessages = [];
318
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
+ };
319
508
  if (assistantReply.kind === "issue_proposal") {
320
- const shouldAutoCreateIssue = conversation.planMode || conversation.issueCreationMode === "auto_create";
509
+ const issueProposalStructuredPayload = withDefaultIssueProposalAssignee(assistantReply.structuredPayload, await defaultIssueAssigneeAgentId(conversation));
510
+ const shouldAutoCreateIssue = !conversation.planMode && conversation.issueCreationMode === "auto_create";
321
511
  if (shouldAutoCreateIssue) {
322
- const proposalMessage = await svc.addMessage(conversation.id, {
323
- orgId: conversation.orgId,
324
- role: "assistant",
512
+ const proposalMessage = await saveAssistantMessage({
325
513
  kind: "issue_proposal",
326
514
  body: assistantReply.body,
327
- structuredPayload: assistantReply.structuredPayload,
328
- transcript,
329
- replyingAgentId,
330
- chatTurnId,
331
- turnVariant,
515
+ structuredPayload: issueProposalStructuredPayload,
516
+ });
517
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
518
+ await assertCanConvertIssueProposal(req, conversation, {
519
+ proposal: issueProposalStructuredPayload,
332
520
  });
333
- createdMessages.push(proposalMessage);
334
521
  const issue = await svc.convertToIssue(conversation.id, {
335
522
  actorUserId: actor.actorType === "user" ? actor.actorId : null,
336
523
  messageId: proposalMessage.id,
@@ -359,36 +546,28 @@ export function chatRoutes(db, storage) {
359
546
  details: {
360
547
  issueId: issue.id,
361
548
  issueIdentifier: issue.identifier,
362
- source: conversation.planMode ? "plan_mode" : "auto_create",
549
+ source: "auto_create",
363
550
  },
364
551
  });
365
552
  return createdMessages;
366
553
  }
554
+ const planDocument = proposedPlanDocumentPayload(issueProposalStructuredPayload);
367
555
  const approval = await svc.createProposalApproval(conversation.orgId, {
368
556
  type: "chat_issue_creation",
369
557
  requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
370
558
  payload: {
371
559
  chatConversationId: conversation.id,
372
- proposedIssue: assistantReply.structuredPayload &&
373
- typeof assistantReply.structuredPayload.issueProposal === "object" &&
374
- assistantReply.structuredPayload.issueProposal !== null
375
- ? assistantReply.structuredPayload.issueProposal
376
- : assistantReply.structuredPayload,
560
+ proposedIssue: proposedIssuePayload(issueProposalStructuredPayload),
561
+ ...(planDocument ? { planDocument } : {}),
377
562
  },
378
563
  });
379
- const proposalMessage = await svc.addMessage(conversation.id, {
380
- orgId: conversation.orgId,
381
- role: "assistant",
564
+ const proposalMessage = await saveAssistantMessage({
382
565
  kind: "issue_proposal",
383
566
  body: assistantReply.body,
384
- structuredPayload: assistantReply.structuredPayload,
385
- transcript,
567
+ structuredPayload: issueProposalStructuredPayload,
386
568
  approvalId: approval.id,
387
- replyingAgentId,
388
- chatTurnId,
389
- turnVariant,
390
569
  });
391
- createdMessages.push(proposalMessage);
570
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
392
571
  return createdMessages;
393
572
  }
394
573
  if (assistantReply.kind === "operation_proposal") {
@@ -404,9 +583,7 @@ export function chatRoutes(db, storage) {
404
583
  : assistantReply.structuredPayload,
405
584
  },
406
585
  });
407
- const proposalMessage = await svc.addMessage(conversation.id, {
408
- orgId: conversation.orgId,
409
- role: "assistant",
586
+ const proposalMessage = await saveAssistantMessage({
410
587
  kind: "operation_proposal",
411
588
  body: assistantReply.body,
412
589
  structuredPayload: {
@@ -418,41 +595,80 @@ export function chatRoutes(db, storage) {
418
595
  decidedAt: null,
419
596
  },
420
597
  },
421
- transcript,
422
598
  approvalId: approval.id,
423
- replyingAgentId,
424
- chatTurnId,
425
- turnVariant,
426
599
  });
427
- createdMessages.push(proposalMessage);
600
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
428
601
  return createdMessages;
429
602
  }
430
- const assistantMessage = await svc.addMessage(conversation.id, {
431
- orgId: conversation.orgId,
432
- role: "assistant",
603
+ const assistantMessage = await saveAssistantMessage({
433
604
  kind: assistantReply.kind === "routing_suggestion" ? "routing_suggestion" : "message",
434
605
  body: assistantReply.body,
435
606
  structuredPayload: assistantReply.structuredPayload,
436
- transcript,
437
- replyingAgentId,
438
- chatTurnId,
439
- turnVariant,
440
607
  });
441
- createdMessages.push(assistantMessage);
608
+ createdMessages.push(await attachGeneratedFiles(assistantMessage, assistantReply.generatedAttachments));
442
609
  return createdMessages;
443
610
  }
444
- 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) {
445
646
  const trimmed = body.trim();
446
- 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)
447
652
  return null;
448
653
  const chatTurnId = turnContext?.chatTurnId ?? randomUUID();
449
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
+ }
450
666
  const message = await svc.addMessage(conversation.id, {
451
667
  orgId: conversation.orgId,
452
668
  role: "assistant",
453
669
  kind: "message",
454
670
  status,
455
- body: trimmed,
671
+ body: durableBody,
456
672
  transcript,
457
673
  replyingAgentId,
458
674
  chatTurnId,
@@ -473,8 +689,9 @@ export function chatRoutes(db, storage) {
473
689
  const status = statusParam === "resolved" || statusParam === "archived" || statusParam === "all"
474
690
  ? statusParam
475
691
  : "active";
692
+ const q = typeof req.query.q === "string" ? req.query.q : undefined;
476
693
  const userId = req.actor.type === "board" ? (req.actor.userId ?? "local-board") : null;
477
- const conversations = await svc.list(orgId, { status }, userId);
694
+ const conversations = await svc.list(orgId, { status, q }, userId);
478
695
  res.json(await assistantSvc.enrichConversations(conversations));
479
696
  });
480
697
  router.post("/orgs/:orgId/chats", validate(createChatConversationSchema), async (req, res) => {
@@ -487,6 +704,13 @@ export function chatRoutes(db, storage) {
487
704
  }
488
705
  const contextLinks = req.body.contextLinks ?? [];
489
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
+ }
490
714
  const actor = getActorInfo(req);
491
715
  const conversation = await svc.create(orgId, {
492
716
  title: req.body.title,
@@ -578,6 +802,9 @@ export function chatRoutes(db, storage) {
578
802
  res.status(404).json({ error: "Chat conversation not found" });
579
803
  return;
580
804
  }
805
+ if (!hasActiveChatGeneration(conversation.id)) {
806
+ await svc.markInterruptedStreamingMessages(conversation.id);
807
+ }
581
808
  const messages = await svc.listMessages(conversation.id);
582
809
  res.json(messages);
583
810
  });
@@ -627,6 +854,7 @@ export function chatRoutes(db, storage) {
627
854
  const assistantInput = await loadAssistantInput(conversation, actor);
628
855
  const transcript = [];
629
856
  const observedTranscript = [];
857
+ let modelTurnInput;
630
858
  let fallbackOutput = null;
631
859
  let finalChatOutput = null;
632
860
  let finalChatStatus = "completed";
@@ -634,6 +862,7 @@ export function chatRoutes(db, storage) {
634
862
  const streamed = await assistantSvc.streamChatAssistantReply({
635
863
  ...assistantInput,
636
864
  onInvocationMeta: async (meta) => {
865
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
637
866
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
638
867
  mergeChatInvocationTraceMetadata(chatObservation, meta);
639
868
  updateExecutionObservation(observation, chatObservation, {
@@ -653,7 +882,7 @@ export function chatRoutes(db, storage) {
653
882
  finalChatStatus = "failed";
654
883
  throw new Error("Chat assistant reply was stopped before completion");
655
884
  }
656
- 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);
657
886
  finalChatOutput = streamed.reply.body;
658
887
  await logChatMessagesAdded(assistantInput.conversation, created, {
659
888
  actorType: "system",
@@ -684,6 +913,7 @@ export function chatRoutes(db, storage) {
684
913
  context: chatObservation,
685
914
  parentObservation: observation,
686
915
  transcript: observedTranscript,
916
+ initialTurnInput: modelTurnInput,
687
917
  fallbackResult: fallbackOutput
688
918
  ? {
689
919
  output: fallbackOutput,
@@ -728,6 +958,9 @@ export function chatRoutes(db, storage) {
728
958
  });
729
959
  }
730
960
  logger.warn({ err, conversationId: conversation.id }, "chat assistant reply failed");
961
+ if (err instanceof HttpError) {
962
+ throw err;
963
+ }
731
964
  res.status(502).json({
732
965
  error: err instanceof Error ? err.message : "Chat assistant failed to respond",
733
966
  });
@@ -786,6 +1019,42 @@ export function chatRoutes(db, storage) {
786
1019
  let chatObservation = null;
787
1020
  const transcript = [];
788
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
+ };
789
1058
  let clientClosed = false;
790
1059
  const handleClosed = () => {
791
1060
  if (clientClosed || res.writableEnded)
@@ -843,6 +1112,7 @@ export function chatRoutes(db, storage) {
843
1112
  ...assistantInput,
844
1113
  abortSignal: abortController.signal,
845
1114
  onInvocationMeta: async (meta) => {
1115
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
846
1116
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
847
1117
  mergeChatInvocationTraceMetadata(chatObservation, meta);
848
1118
  updateExecutionObservation(observation, chatObservation, {
@@ -851,12 +1121,17 @@ export function chatRoutes(db, storage) {
851
1121
  updateExecutionTraceIO(observation, { input: currentChatTraceInput });
852
1122
  },
853
1123
  onAssistantDelta: async (delta) => {
1124
+ assistantDraftBody = `${assistantDraftBody}${delta}`;
1125
+ await persistStreamProgress(assistantInput.conversation);
1126
+ if (clientClosed)
1127
+ return;
854
1128
  writeStreamEvent(res, {
855
1129
  type: "assistant_delta",
856
1130
  delta,
857
1131
  });
858
1132
  },
859
1133
  onAssistantState: async (state) => {
1134
+ await persistStreamProgress(assistantInput.conversation);
860
1135
  if (clientClosed)
861
1136
  return;
862
1137
  writeStreamEvent(res, {
@@ -866,6 +1141,7 @@ export function chatRoutes(db, storage) {
866
1141
  },
867
1142
  onTranscriptEntry: async (entry) => {
868
1143
  transcript.push(entry);
1144
+ await persistStreamProgress(assistantInput.conversation);
869
1145
  if (clientClosed)
870
1146
  return;
871
1147
  writeStreamEvent(res, {
@@ -880,7 +1156,7 @@ export function chatRoutes(db, storage) {
880
1156
  if (streamed.outcome === "stopped") {
881
1157
  finalChatStatus = "stopped";
882
1158
  finalChatOutput = streamed.partialBody;
883
- 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);
884
1160
  if (stoppedMessage) {
885
1161
  await logChatMessagesAdded(assistantInput.conversation, [stoppedMessage], {
886
1162
  actorType: "system",
@@ -906,7 +1182,7 @@ export function chatRoutes(db, storage) {
906
1182
  }
907
1183
  return;
908
1184
  }
909
- 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);
910
1186
  finalChatOutput = streamed.reply.body;
911
1187
  await logChatMessagesAdded(assistantInput.conversation, createdMessages, {
912
1188
  actorType: "system",
@@ -942,6 +1218,14 @@ export function chatRoutes(db, storage) {
942
1218
  context: chatObservation,
943
1219
  parentObservation: observation,
944
1220
  transcript: observedTranscript,
1221
+ initialTurnInput: modelTurnInput,
1222
+ fallbackResult: finalChatOutput
1223
+ ? {
1224
+ output: finalChatOutput,
1225
+ subtype: finalChatStatus,
1226
+ isError: finalChatStatus === "failed",
1227
+ }
1228
+ : null,
945
1229
  });
946
1230
  finalChatOutput = finalChatOutput ?? transcriptStats.finalOutput ?? null;
947
1231
  }
@@ -969,8 +1253,10 @@ export function chatRoutes(db, storage) {
969
1253
  }
970
1254
  catch (err) {
971
1255
  const partialBody = err instanceof ChatAssistantStreamError ? err.partialBody : "";
1256
+ const generatedAttachments = err instanceof ChatAssistantStreamError ? err.generatedAttachments : [];
972
1257
  const failedReplyingAgentId = chatReplyingAgentId(assistantConversationForPartial);
973
- 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);
974
1260
  if (failedMessage && assistantConversationForPartial) {
975
1261
  await logChatMessagesAdded(assistantConversationForPartial, [failedMessage], {
976
1262
  actorType: "system",
@@ -1168,6 +1454,10 @@ export function chatRoutes(db, storage) {
1168
1454
  return;
1169
1455
  }
1170
1456
  }
1457
+ await assertCanConvertIssueProposal(req, conversation, {
1458
+ messageId: req.body.messageId ?? null,
1459
+ proposal: req.body.proposal ?? null,
1460
+ });
1171
1461
  const chatObservation = buildChatObservabilityContext(conversation, {
1172
1462
  rootExecutionId: req.body.messageId ?? `chat-convert:${conversation.id}`,
1173
1463
  trigger: "convert_to_issue",