@rudderhq/server 0.2.0-canary.9 → 0.2.1

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 (196) 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 +20 -5
  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 +79 -4
  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 +47 -2
  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 +300 -92
  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 +236 -22
  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 +11 -2
  57. package/dist/services/chat-assistant.d.ts.map +1 -1
  58. package/dist/services/chat-assistant.js +143 -8
  59. package/dist/services/chat-assistant.js.map +1 -1
  60. package/dist/services/chats.d.ts +112 -13
  61. package/dist/services/chats.d.ts.map +1 -1
  62. package/dist/services/chats.js +218 -38
  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 +102 -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 +27 -16
  72. package/dist/services/instance-settings.js.map +1 -1
  73. package/dist/services/issue-approvals.d.ts +16 -2
  74. package/dist/services/issue-approvals.d.ts.map +1 -1
  75. package/dist/services/issue-approvals.js +27 -4
  76. package/dist/services/issue-approvals.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 +154 -15
  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 -1
  97. package/dist/services/plugin-registry.d.ts +4 -4
  98. package/dist/services/projects.d.ts +1 -1
  99. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  100. package/dist/services/runtime-kernel/heartbeat.js +571 -31
  101. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  102. package/dist/services/secrets.d.ts +5 -5
  103. package/dist/services/workspace-backups.d.ts.map +1 -1
  104. package/dist/services/workspace-backups.js +6 -0
  105. package/dist/services/workspace-backups.js.map +1 -1
  106. package/dist/services/workspace-runtime.d.ts.map +1 -1
  107. package/dist/services/workspace-runtime.js +2 -0
  108. package/dist/services/workspace-runtime.js.map +1 -1
  109. package/package.json +13 -13
  110. package/resources/bundled-skills/rudder/SKILL.md +72 -7
  111. package/resources/bundled-skills/rudder/references/cli-reference.md +34 -9
  112. package/resources/bundled-skills/rudder/references/organization-skills.md +12 -7
  113. package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +1 -0
  114. package/skills/rudder/SKILL.md +72 -7
  115. package/skills/rudder/references/cli-reference.md +34 -9
  116. package/skills/rudder/references/organization-skills.md +12 -7
  117. package/skills/rudder-create-agent/references/cli-reference.md +1 -0
  118. package/ui-dist/assets/{_basePickBy-aX2f6dVl.js → _basePickBy-EvWeCTRb.js} +1 -1
  119. package/ui-dist/assets/{_baseUniq-BYwL7heN.js → _baseUniq-C_DXAETg.js} +1 -1
  120. package/ui-dist/assets/{arc-BG9f5pwY.js → arc-BWTkVM-u.js} +1 -1
  121. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-BFFQoJJ1.js → architectureDiagram-2XIMDMQ5-yyX54Dgl.js} +1 -1
  122. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-Bvx1IB1z.js → blockDiagram-WCTKOSBZ-DleWvS8P.js} +1 -1
  123. package/ui-dist/assets/{c4Diagram-IC4MRINW-DJbCE4sh.js → c4Diagram-IC4MRINW-CltWqWC_.js} +1 -1
  124. package/ui-dist/assets/channel-Gdzxe2a1.js +1 -0
  125. package/ui-dist/assets/{chunk-4BX2VUAB-BOVbLFsN.js → chunk-4BX2VUAB-CA6RvGN7.js} +1 -1
  126. package/ui-dist/assets/{chunk-55IACEB6-D5pKj6S9.js → chunk-55IACEB6-D_EpF39w.js} +1 -1
  127. package/ui-dist/assets/{chunk-FMBD7UC4-OY5xuJeR.js → chunk-FMBD7UC4-CYMkBnLy.js} +1 -1
  128. package/ui-dist/assets/{chunk-JSJVCQXG-C5X67KZg.js → chunk-JSJVCQXG-CIY2Cb1T.js} +1 -1
  129. package/ui-dist/assets/{chunk-KX2RTZJC-C-4PZ9Q_.js → chunk-KX2RTZJC-BUyGoIKj.js} +1 -1
  130. package/ui-dist/assets/{chunk-NQ4KR5QH-XysPlqPj.js → chunk-NQ4KR5QH-DkntSLtY.js} +1 -1
  131. package/ui-dist/assets/{chunk-QZHKN3VN-B5wEbFHo.js → chunk-QZHKN3VN-DeEs3yL0.js} +1 -1
  132. package/ui-dist/assets/{chunk-WL4C6EOR-BanwYFa2.js → chunk-WL4C6EOR-Va8TkdTb.js} +1 -1
  133. package/ui-dist/assets/classDiagram-VBA2DB6C-BN6WyuN3.js +1 -0
  134. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-BN6WyuN3.js +1 -0
  135. package/ui-dist/assets/clone-DL9OCUyP.js +1 -0
  136. package/ui-dist/assets/{cose-bilkent-S5V4N54A-Cd4q2swD.js → cose-bilkent-S5V4N54A-Bb6NLaVm.js} +1 -1
  137. package/ui-dist/assets/{dagre-KLK3FWXG-B_VyOhf3.js → dagre-KLK3FWXG-DpqLnZ3A.js} +1 -1
  138. package/ui-dist/assets/{diagram-E7M64L7V-BRoG4Mz6.js → diagram-E7M64L7V-D7J8NbEW.js} +1 -1
  139. package/ui-dist/assets/{diagram-IFDJBPK2-CRU_A9p9.js → diagram-IFDJBPK2-Ds2u81Zi.js} +1 -1
  140. package/ui-dist/assets/{diagram-P4PSJMXO-BYSQDbfb.js → diagram-P4PSJMXO-BwBplO7L.js} +1 -1
  141. package/ui-dist/assets/{erDiagram-INFDFZHY-v5j1kyWr.js → erDiagram-INFDFZHY-Ba-Ynr8U.js} +1 -1
  142. package/ui-dist/assets/{flowDiagram-PKNHOUZH-C06ZQgTj.js → flowDiagram-PKNHOUZH-FnOXpXb_.js} +1 -1
  143. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-Dw9p5nQ1.js → ganttDiagram-A5KZAMGK-B8-MpUjy.js} +1 -1
  144. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-CrpXRIaP.js → gitGraphDiagram-K3NZZRJ6-DvyBGQTF.js} +1 -1
  145. package/ui-dist/assets/{graph-ClTUmULf.js → graph-BdpIVR-I.js} +1 -1
  146. package/ui-dist/assets/{index-DK13xhRv.js → index-3CPMGfu4.js} +1 -1
  147. package/ui-dist/assets/index-44A3IjSd.css +1 -0
  148. package/ui-dist/assets/{index-L6M3nVxh.js → index-B4seykMn.js} +1 -1
  149. package/ui-dist/assets/{index-Bpc2gRVo.js → index-B5Lq7qho.js} +1 -1
  150. package/ui-dist/assets/{index-DkDkjZ-D.js → index-BKWZYXO6.js} +1 -1
  151. package/ui-dist/assets/{index-DxzAgTWd.js → index-BO-P9C91.js} +1 -1
  152. package/ui-dist/assets/{index-BvGogi9q.js → index-BO9KiNr0.js} +1 -1
  153. package/ui-dist/assets/{index-Btwy7Cp-.js → index-Bd_GitJ7.js} +1 -1
  154. package/ui-dist/assets/{index-DNlWBtHa.js → index-BeyQP4jc.js} +1 -1
  155. package/ui-dist/assets/{index-4uxadHo5.js → index-Bp3rYm9R.js} +1 -1
  156. package/ui-dist/assets/{index-DWFMs9uk.js → index-CBAKsDOH.js} +1 -1
  157. package/ui-dist/assets/{index-T81awgzh.js → index-CWPEuLky.js} +1 -1
  158. package/ui-dist/assets/{index-DAhPD1Ss.js → index-Ce0xbQ5p.js} +1 -1
  159. package/ui-dist/assets/{index-_x9smX4T.js → index-ChyWxMPa.js} +1 -1
  160. package/ui-dist/assets/{index-CIr7H9OI.js → index-CkEo4bIl.js} +1 -1
  161. package/ui-dist/assets/{index-sLGLHxIu.js → index-CvzsgQH3.js} +1 -1
  162. package/ui-dist/assets/{index-D-6z8wxx.js → index-DF0X3XZi.js} +1 -1
  163. package/ui-dist/assets/{index-BVfM9ax8.js → index-DNFqhIup.js} +1 -1
  164. package/ui-dist/assets/index-Dfi8PbGx.js +1484 -0
  165. package/ui-dist/assets/{index-C_BTFRTX.js → index-Dys_qAzR.js} +1 -1
  166. package/ui-dist/assets/{index-Cr7n11UG.js → index-DzKALBsQ.js} +1 -1
  167. package/ui-dist/assets/{index-CqYInp-c.js → index-Qe9bMaYk.js} +1 -1
  168. package/ui-dist/assets/{index-CQWmziMF.js → index-baeevrWz.js} +1 -1
  169. package/ui-dist/assets/{index-BYC_xlrx.js → index-bs5pLhnN.js} +1 -1
  170. package/ui-dist/assets/{infoDiagram-LFFYTUFH-BA3VxOIU.js → infoDiagram-LFFYTUFH-51Iz4iFI.js} +1 -1
  171. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-DGrizi0S.js → ishikawaDiagram-PHBUUO56-XMkPw0tW.js} +1 -1
  172. package/ui-dist/assets/{journeyDiagram-4ABVD52K-6ey34a7e.js → journeyDiagram-4ABVD52K-DAX0bTCG.js} +1 -1
  173. package/ui-dist/assets/{kanban-definition-K7BYSVSG-CwNnmsam.js → kanban-definition-K7BYSVSG-DndcgBkd.js} +1 -1
  174. package/ui-dist/assets/{layout-buNxvllr.js → layout-DE8DhR5g.js} +1 -1
  175. package/ui-dist/assets/{linear-BPWhxaRl.js → linear-B6lAW9Wb.js} +1 -1
  176. package/ui-dist/assets/{mermaid.core-Cajx0s-z.js → mermaid.core-BG--kYhA.js} +4 -4
  177. package/ui-dist/assets/{mindmap-definition-YRQLILUH-Bf5InEp-.js → mindmap-definition-YRQLILUH-DkjV0oE3.js} +1 -1
  178. package/ui-dist/assets/{pieDiagram-SKSYHLDU-CZFz7NWC.js → pieDiagram-SKSYHLDU-D03TjqYu.js} +1 -1
  179. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-XBmKVoc9.js → quadrantDiagram-337W2JSQ-C0oqv-xU.js} +1 -1
  180. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-BkgdDv0H.js → requirementDiagram-Z7DCOOCP-okIS8feM.js} +1 -1
  181. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-CASFR28i.js → sankeyDiagram-WA2Y5GQK-WOnxUdkO.js} +1 -1
  182. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-BzY7LMRv.js → sequenceDiagram-2WXFIKYE-RVCXfMRR.js} +1 -1
  183. package/ui-dist/assets/{stateDiagram-RAJIS63D-C9UMSk36.js → stateDiagram-RAJIS63D-CZFHvVtT.js} +1 -1
  184. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DgYYudAJ.js +1 -0
  185. package/ui-dist/assets/{timeline-definition-YZTLITO2-D6m4R4xe.js → timeline-definition-YZTLITO2-S0uy5mlJ.js} +1 -1
  186. package/ui-dist/assets/{treemap-KZPCXAKY-7V9mnT8T.js → treemap-KZPCXAKY-Bhyg_yHs.js} +1 -1
  187. package/ui-dist/assets/{vennDiagram-LZ73GAT5-Ci-sfAyq.js → vennDiagram-LZ73GAT5-EnVupOQz.js} +1 -1
  188. package/ui-dist/assets/{xychartDiagram-JWTSCODW-BayXhRSu.js → xychartDiagram-JWTSCODW-BYpdJxGK.js} +1 -1
  189. package/ui-dist/index.html +2 -2
  190. package/ui-dist/assets/channel-ClX7n84B.js +0 -1
  191. package/ui-dist/assets/classDiagram-VBA2DB6C-DvWbsnVz.js +0 -1
  192. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-DvWbsnVz.js +0 -1
  193. package/ui-dist/assets/clone-Dla3A8ZA.js +0 -1
  194. package/ui-dist/assets/index-CSANx6ee.css +0 -1
  195. package/ui-dist/assets/index-DCa9-Sy-.js +0 -1439
  196. 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,
@@ -313,50 +349,6 @@ export function chatRoutes(db, storage) {
313
349
  function chatReplyingAgentId(conversation) {
314
350
  return conversation?.chatRuntime?.runtimeAgentId ?? conversation?.preferredAgentId ?? null;
315
351
  }
316
- async function defaultIssueAssigneeAgentId(conversation) {
317
- const candidateAgentIds = [conversation?.preferredAgentId, conversation?.routedAgentId]
318
- .filter((id) => typeof id === "string" && id.trim().length > 0);
319
- if (!conversation)
320
- return null;
321
- for (const candidateAgentId of candidateAgentIds) {
322
- const agent = await agentsSvc.getById(candidateAgentId);
323
- if (!agent || agent.orgId !== conversation.orgId)
324
- continue;
325
- if (agent.status === "pending_approval" || agent.status === "terminated")
326
- continue;
327
- return agent.id;
328
- }
329
- return null;
330
- }
331
- function hasIssueProposalAssignee(proposal) {
332
- return Boolean((typeof proposal.assigneeAgentId === "string" && proposal.assigneeAgentId.trim().length > 0)
333
- || (typeof proposal.assigneeUserId === "string" && proposal.assigneeUserId.trim().length > 0));
334
- }
335
- function withDefaultIssueProposalAssignee(structuredPayload, assigneeAgentId) {
336
- if (!structuredPayload || !assigneeAgentId)
337
- return structuredPayload ?? null;
338
- const nestedProposal = structuredPayload.issueProposal
339
- && typeof structuredPayload.issueProposal === "object"
340
- && !Array.isArray(structuredPayload.issueProposal)
341
- ? structuredPayload.issueProposal
342
- : null;
343
- const proposal = nestedProposal ?? structuredPayload;
344
- if (hasIssueProposalAssignee(proposal))
345
- return structuredPayload;
346
- if (nestedProposal) {
347
- return {
348
- ...structuredPayload,
349
- issueProposal: {
350
- ...nestedProposal,
351
- assigneeAgentId,
352
- },
353
- };
354
- }
355
- return {
356
- ...structuredPayload,
357
- assigneeAgentId,
358
- };
359
- }
360
352
  function proposedIssuePayload(structuredPayload) {
361
353
  if (!structuredPayload)
362
354
  return structuredPayload ?? null;
@@ -367,25 +359,121 @@ export function chatRoutes(db, storage) {
367
359
  ? structuredPayload.issueProposal
368
360
  : structuredPayload;
369
361
  }
370
- async function persistAssistantReply(conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation)) {
362
+ function proposalAssignsOrReviewsIssue(proposal) {
363
+ if (!proposal)
364
+ return false;
365
+ return Boolean((typeof proposal.assigneeAgentId === "string" && proposal.assigneeAgentId.trim().length > 0)
366
+ || (typeof proposal.assigneeUserId === "string" && proposal.assigneeUserId.trim().length > 0)
367
+ || (typeof proposal.reviewerAgentId === "string" && proposal.reviewerAgentId.trim().length > 0)
368
+ || (typeof proposal.reviewerUserId === "string" && proposal.reviewerUserId.trim().length > 0));
369
+ }
370
+ async function proposedIssuePayloadForConversion(conversationId, input) {
371
+ if (input.proposal)
372
+ return proposedIssuePayload(input.proposal);
373
+ if (input.messageId) {
374
+ const message = await svc.getMessage(conversationId, input.messageId);
375
+ return proposedIssuePayload(message?.structuredPayload ?? null);
376
+ }
377
+ const messages = await svc.listMessages(conversationId);
378
+ const message = [...messages].reverse().find((entry) => entry.kind === "issue_proposal");
379
+ return proposedIssuePayload(message?.structuredPayload ?? null);
380
+ }
381
+ async function assertCanConvertIssueProposal(req, conversation, input) {
382
+ const proposal = await proposedIssuePayloadForConversion(conversation.id, input);
383
+ if (proposalAssignsOrReviewsIssue(proposal)) {
384
+ await assertCanAssignTasks(req, conversation.orgId);
385
+ }
386
+ }
387
+ function proposedPlanDocumentPayload(structuredPayload) {
388
+ if (!structuredPayload)
389
+ return null;
390
+ const rawDocument = structuredPayload.planDocument
391
+ && typeof structuredPayload.planDocument === "object"
392
+ && !Array.isArray(structuredPayload.planDocument)
393
+ ? structuredPayload.planDocument
394
+ : structuredPayload.plan && typeof structuredPayload.plan === "object" && !Array.isArray(structuredPayload.plan)
395
+ ? structuredPayload.plan
396
+ : null;
397
+ return rawDocument ? rawDocument : null;
398
+ }
399
+ async function persistAssistantReply(req, conversation, actor, assistantReply, turnContext, transcript = [], replyingAgentId = assistantReply.replyingAgentId ?? chatReplyingAgentId(conversation), existingMessageId) {
371
400
  const createdMessages = [];
372
401
  const { chatTurnId, turnVariant } = turnContext;
402
+ const attachGeneratedFiles = async (message, generatedAttachments) => {
403
+ if (!generatedAttachments || generatedAttachments.length === 0)
404
+ return message;
405
+ const attachments = [];
406
+ for (const generated of generatedAttachments) {
407
+ if (generated.body.length > MAX_ATTACHMENT_BYTES) {
408
+ throw new ChatAssistantStreamError(`Generated attachment exceeds ${MAX_ATTACHMENT_BYTES} bytes`, assistantReply.body, generatedAttachments);
409
+ }
410
+ const stored = await storage.putFile({
411
+ orgId: conversation.orgId,
412
+ namespace: `chats/${conversation.id}/generated`,
413
+ originalFilename: generated.originalFilename,
414
+ contentType: generated.contentType,
415
+ body: generated.body,
416
+ });
417
+ const attachment = await svc.createAttachment({
418
+ orgId: conversation.orgId,
419
+ conversationId: conversation.id,
420
+ messageId: message.id,
421
+ provider: stored.provider,
422
+ objectKey: stored.objectKey,
423
+ contentType: stored.contentType,
424
+ byteSize: stored.byteSize,
425
+ sha256: stored.sha256,
426
+ originalFilename: stored.originalFilename,
427
+ createdByAgentId: replyingAgentId,
428
+ createdByUserId: null,
429
+ });
430
+ attachments.push(attachment);
431
+ }
432
+ return {
433
+ ...message,
434
+ attachments: [...(message.attachments ?? []), ...attachments],
435
+ };
436
+ };
437
+ const saveAssistantMessage = async (input) => {
438
+ if (existingMessageId) {
439
+ const updated = await svc.updateMessage(conversation.id, existingMessageId, {
440
+ kind: input.kind,
441
+ status: "completed",
442
+ body: input.body,
443
+ structuredPayload: input.structuredPayload ?? null,
444
+ transcript,
445
+ approvalId: input.approvalId ?? null,
446
+ replyingAgentId,
447
+ });
448
+ if (updated)
449
+ return updated;
450
+ }
451
+ return svc.addMessage(conversation.id, {
452
+ orgId: conversation.orgId,
453
+ role: "assistant",
454
+ kind: input.kind,
455
+ body: input.body,
456
+ structuredPayload: input.structuredPayload ?? null,
457
+ transcript,
458
+ approvalId: input.approvalId ?? null,
459
+ replyingAgentId,
460
+ chatTurnId,
461
+ turnVariant,
462
+ });
463
+ };
373
464
  if (assistantReply.kind === "issue_proposal") {
374
- const issueProposalStructuredPayload = withDefaultIssueProposalAssignee(assistantReply.structuredPayload, await defaultIssueAssigneeAgentId(conversation));
375
- const shouldAutoCreateIssue = conversation.planMode || conversation.issueCreationMode === "auto_create";
465
+ const issueProposalStructuredPayload = assistantReply.structuredPayload ?? null;
466
+ const shouldAutoCreateIssue = !conversation.planMode && conversation.issueCreationMode === "auto_create";
376
467
  if (shouldAutoCreateIssue) {
377
- const proposalMessage = await svc.addMessage(conversation.id, {
378
- orgId: conversation.orgId,
379
- role: "assistant",
468
+ const proposalMessage = await saveAssistantMessage({
380
469
  kind: "issue_proposal",
381
470
  body: assistantReply.body,
382
471
  structuredPayload: issueProposalStructuredPayload,
383
- transcript,
384
- replyingAgentId,
385
- chatTurnId,
386
- turnVariant,
387
472
  });
388
- createdMessages.push(proposalMessage);
473
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
474
+ await assertCanConvertIssueProposal(req, conversation, {
475
+ proposal: issueProposalStructuredPayload,
476
+ });
389
477
  const issue = await svc.convertToIssue(conversation.id, {
390
478
  actorUserId: actor.actorType === "user" ? actor.actorId : null,
391
479
  messageId: proposalMessage.id,
@@ -414,32 +502,28 @@ export function chatRoutes(db, storage) {
414
502
  details: {
415
503
  issueId: issue.id,
416
504
  issueIdentifier: issue.identifier,
417
- source: conversation.planMode ? "plan_mode" : "auto_create",
505
+ source: "auto_create",
418
506
  },
419
507
  });
420
508
  return createdMessages;
421
509
  }
510
+ const planDocument = proposedPlanDocumentPayload(issueProposalStructuredPayload);
422
511
  const approval = await svc.createProposalApproval(conversation.orgId, {
423
512
  type: "chat_issue_creation",
424
513
  requestedByUserId: actor.actorType === "user" ? actor.actorId : null,
425
514
  payload: {
426
515
  chatConversationId: conversation.id,
427
516
  proposedIssue: proposedIssuePayload(issueProposalStructuredPayload),
517
+ ...(planDocument ? { planDocument } : {}),
428
518
  },
429
519
  });
430
- const proposalMessage = await svc.addMessage(conversation.id, {
431
- orgId: conversation.orgId,
432
- role: "assistant",
520
+ const proposalMessage = await saveAssistantMessage({
433
521
  kind: "issue_proposal",
434
522
  body: assistantReply.body,
435
523
  structuredPayload: issueProposalStructuredPayload,
436
- transcript,
437
524
  approvalId: approval.id,
438
- replyingAgentId,
439
- chatTurnId,
440
- turnVariant,
441
525
  });
442
- createdMessages.push(proposalMessage);
526
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
443
527
  return createdMessages;
444
528
  }
445
529
  if (assistantReply.kind === "operation_proposal") {
@@ -455,9 +539,7 @@ export function chatRoutes(db, storage) {
455
539
  : assistantReply.structuredPayload,
456
540
  },
457
541
  });
458
- const proposalMessage = await svc.addMessage(conversation.id, {
459
- orgId: conversation.orgId,
460
- role: "assistant",
542
+ const proposalMessage = await saveAssistantMessage({
461
543
  kind: "operation_proposal",
462
544
  body: assistantReply.body,
463
545
  structuredPayload: {
@@ -469,41 +551,80 @@ export function chatRoutes(db, storage) {
469
551
  decidedAt: null,
470
552
  },
471
553
  },
472
- transcript,
473
554
  approvalId: approval.id,
474
- replyingAgentId,
475
- chatTurnId,
476
- turnVariant,
477
555
  });
478
- createdMessages.push(proposalMessage);
556
+ createdMessages.push(await attachGeneratedFiles(proposalMessage, assistantReply.generatedAttachments));
479
557
  return createdMessages;
480
558
  }
481
- const assistantMessage = await svc.addMessage(conversation.id, {
482
- orgId: conversation.orgId,
483
- role: "assistant",
484
- kind: assistantReply.kind === "routing_suggestion" ? "routing_suggestion" : "message",
559
+ const assistantMessage = await saveAssistantMessage({
560
+ kind: "message",
485
561
  body: assistantReply.body,
486
562
  structuredPayload: assistantReply.structuredPayload,
487
- transcript,
488
- replyingAgentId,
489
- chatTurnId,
490
- turnVariant,
491
563
  });
492
- createdMessages.push(assistantMessage);
564
+ createdMessages.push(await attachGeneratedFiles(assistantMessage, assistantReply.generatedAttachments));
493
565
  return createdMessages;
494
566
  }
495
- async function persistPartialAssistantMessage(conversation, body, status, turnContext, transcript = [], replyingAgentId = chatReplyingAgentId(conversation)) {
567
+ async function attachGeneratedFilesToPartialMessage(conversation, message, generatedAttachments, replyingAgentId) {
568
+ if (!message || !generatedAttachments || generatedAttachments.length === 0)
569
+ return message;
570
+ const attachments = [];
571
+ for (const generated of generatedAttachments) {
572
+ if (generated.body.length > MAX_ATTACHMENT_BYTES)
573
+ continue;
574
+ const stored = await storage.putFile({
575
+ orgId: conversation.orgId,
576
+ namespace: `chats/${conversation.id}/generated`,
577
+ originalFilename: generated.originalFilename,
578
+ contentType: generated.contentType,
579
+ body: generated.body,
580
+ });
581
+ const attachment = await svc.createAttachment({
582
+ orgId: conversation.orgId,
583
+ conversationId: conversation.id,
584
+ messageId: message.id,
585
+ provider: stored.provider,
586
+ objectKey: stored.objectKey,
587
+ contentType: stored.contentType,
588
+ byteSize: stored.byteSize,
589
+ sha256: stored.sha256,
590
+ originalFilename: stored.originalFilename,
591
+ createdByAgentId: replyingAgentId,
592
+ createdByUserId: null,
593
+ });
594
+ attachments.push(attachment);
595
+ }
596
+ return {
597
+ ...message,
598
+ attachments: [...(message.attachments ?? []), ...attachments],
599
+ };
600
+ }
601
+ async function persistPartialAssistantMessage(conversation, body, status, turnContext, transcript = [], replyingAgentId = chatReplyingAgentId(conversation), existingMessageId) {
496
602
  const trimmed = body.trim();
497
- if (!trimmed)
603
+ const fallbackBody = status === "stopped"
604
+ ? "Chat run stopped before a final reply. Continue the conversation to resume from the preserved context."
605
+ : "Chat run failed before a final reply. Continue the conversation to resume from the preserved context.";
606
+ const durableBody = trimmed || (transcript.length > 0 ? fallbackBody : "");
607
+ if (!durableBody)
498
608
  return null;
499
609
  const chatTurnId = turnContext?.chatTurnId ?? randomUUID();
500
610
  const turnVariant = turnContext?.turnVariant ?? 0;
611
+ if (existingMessageId) {
612
+ const updated = await svc.updateMessage(conversation.id, existingMessageId, {
613
+ kind: "message",
614
+ status,
615
+ body: durableBody,
616
+ transcript,
617
+ replyingAgentId,
618
+ });
619
+ if (updated)
620
+ return updated;
621
+ }
501
622
  const message = await svc.addMessage(conversation.id, {
502
623
  orgId: conversation.orgId,
503
624
  role: "assistant",
504
625
  kind: "message",
505
626
  status,
506
- body: trimmed,
627
+ body: durableBody,
507
628
  transcript,
508
629
  replyingAgentId,
509
630
  chatTurnId,
@@ -524,8 +645,9 @@ export function chatRoutes(db, storage) {
524
645
  const status = statusParam === "resolved" || statusParam === "archived" || statusParam === "all"
525
646
  ? statusParam
526
647
  : "active";
648
+ const q = typeof req.query.q === "string" ? req.query.q : undefined;
527
649
  const userId = req.actor.type === "board" ? (req.actor.userId ?? "local-board") : null;
528
- const conversations = await svc.list(orgId, { status }, userId);
650
+ const conversations = await svc.list(orgId, { status, q }, userId);
529
651
  res.json(await assistantSvc.enrichConversations(conversations));
530
652
  });
531
653
  router.post("/orgs/:orgId/chats", validate(createChatConversationSchema), async (req, res) => {
@@ -538,6 +660,13 @@ export function chatRoutes(db, storage) {
538
660
  }
539
661
  const contextLinks = req.body.contextLinks ?? [];
540
662
  await assertContextLinksBelongToCompany(orgId, contextLinks);
663
+ if (req.body.preferredAgentId) {
664
+ const agent = await agentsSvc.getById(req.body.preferredAgentId);
665
+ if (!agent || agent.orgId !== orgId) {
666
+ res.status(422).json({ error: "Preferred agent must belong to the same organization" });
667
+ return;
668
+ }
669
+ }
541
670
  const actor = getActorInfo(req);
542
671
  const conversation = await svc.create(orgId, {
543
672
  title: req.body.title,
@@ -629,6 +758,9 @@ export function chatRoutes(db, storage) {
629
758
  res.status(404).json({ error: "Chat conversation not found" });
630
759
  return;
631
760
  }
761
+ if (!hasActiveChatGeneration(conversation.id)) {
762
+ await svc.markInterruptedStreamingMessages(conversation.id);
763
+ }
632
764
  const messages = await svc.listMessages(conversation.id);
633
765
  res.json(messages);
634
766
  });
@@ -678,6 +810,7 @@ export function chatRoutes(db, storage) {
678
810
  const assistantInput = await loadAssistantInput(conversation, actor);
679
811
  const transcript = [];
680
812
  const observedTranscript = [];
813
+ let modelTurnInput;
681
814
  let fallbackOutput = null;
682
815
  let finalChatOutput = null;
683
816
  let finalChatStatus = "completed";
@@ -685,6 +818,7 @@ export function chatRoutes(db, storage) {
685
818
  const streamed = await assistantSvc.streamChatAssistantReply({
686
819
  ...assistantInput,
687
820
  onInvocationMeta: async (meta) => {
821
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
688
822
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
689
823
  mergeChatInvocationTraceMetadata(chatObservation, meta);
690
824
  updateExecutionObservation(observation, chatObservation, {
@@ -704,7 +838,7 @@ export function chatRoutes(db, storage) {
704
838
  finalChatStatus = "failed";
705
839
  throw new Error("Chat assistant reply was stopped before completion");
706
840
  }
707
- const created = await persistAssistantReply(assistantInput.conversation, actor, streamed.reply, turnContext, transcript, streamed.replyingAgentId);
841
+ const created = await persistAssistantReply(req, assistantInput.conversation, actor, streamed.reply, turnContext, transcript, streamed.replyingAgentId);
708
842
  finalChatOutput = streamed.reply.body;
709
843
  await logChatMessagesAdded(assistantInput.conversation, created, {
710
844
  actorType: "system",
@@ -735,6 +869,7 @@ export function chatRoutes(db, storage) {
735
869
  context: chatObservation,
736
870
  parentObservation: observation,
737
871
  transcript: observedTranscript,
872
+ initialTurnInput: modelTurnInput,
738
873
  fallbackResult: fallbackOutput
739
874
  ? {
740
875
  output: fallbackOutput,
@@ -779,6 +914,9 @@ export function chatRoutes(db, storage) {
779
914
  });
780
915
  }
781
916
  logger.warn({ err, conversationId: conversation.id }, "chat assistant reply failed");
917
+ if (err instanceof HttpError) {
918
+ throw err;
919
+ }
782
920
  res.status(502).json({
783
921
  error: err instanceof Error ? err.message : "Chat assistant failed to respond",
784
922
  });
@@ -837,6 +975,42 @@ export function chatRoutes(db, storage) {
837
975
  let chatObservation = null;
838
976
  const transcript = [];
839
977
  const observedTranscript = [];
978
+ let modelTurnInput;
979
+ let assistantProgressMessage = null;
980
+ let assistantProgressMessageId = null;
981
+ let assistantDraftBody = "";
982
+ const persistStreamProgress = async (progressConversation, replyingAgentId = chatReplyingAgentId(progressConversation)) => {
983
+ if (!turnContextForPartial)
984
+ return null;
985
+ const input = {
986
+ kind: "message",
987
+ status: "streaming",
988
+ body: assistantDraftBody,
989
+ transcript,
990
+ replyingAgentId,
991
+ };
992
+ if (assistantProgressMessage) {
993
+ const updated = await svc.updateMessage(progressConversation.id, assistantProgressMessage.id, input);
994
+ if (updated) {
995
+ assistantProgressMessage = updated;
996
+ assistantProgressMessageId = assistantProgressMessage.id;
997
+ return assistantProgressMessage;
998
+ }
999
+ }
1000
+ assistantProgressMessage = await svc.addMessage(progressConversation.id, {
1001
+ orgId: progressConversation.orgId,
1002
+ role: "assistant",
1003
+ kind: "message",
1004
+ status: "streaming",
1005
+ body: assistantDraftBody,
1006
+ transcript,
1007
+ replyingAgentId,
1008
+ chatTurnId: turnContextForPartial.chatTurnId,
1009
+ turnVariant: turnContextForPartial.turnVariant,
1010
+ });
1011
+ assistantProgressMessageId = assistantProgressMessage.id;
1012
+ return assistantProgressMessage;
1013
+ };
840
1014
  let clientClosed = false;
841
1015
  const handleClosed = () => {
842
1016
  if (clientClosed || res.writableEnded)
@@ -894,6 +1068,7 @@ export function chatRoutes(db, storage) {
894
1068
  ...assistantInput,
895
1069
  abortSignal: abortController.signal,
896
1070
  onInvocationMeta: async (meta) => {
1071
+ modelTurnInput = modelTurnInputFromInvocationMeta(meta);
897
1072
  currentChatTraceInput = buildChatTraceInput(traceInputBase, meta);
898
1073
  mergeChatInvocationTraceMetadata(chatObservation, meta);
899
1074
  updateExecutionObservation(observation, chatObservation, {
@@ -902,12 +1077,17 @@ export function chatRoutes(db, storage) {
902
1077
  updateExecutionTraceIO(observation, { input: currentChatTraceInput });
903
1078
  },
904
1079
  onAssistantDelta: async (delta) => {
1080
+ assistantDraftBody = `${assistantDraftBody}${delta}`;
1081
+ await persistStreamProgress(assistantInput.conversation);
1082
+ if (clientClosed)
1083
+ return;
905
1084
  writeStreamEvent(res, {
906
1085
  type: "assistant_delta",
907
1086
  delta,
908
1087
  });
909
1088
  },
910
1089
  onAssistantState: async (state) => {
1090
+ await persistStreamProgress(assistantInput.conversation);
911
1091
  if (clientClosed)
912
1092
  return;
913
1093
  writeStreamEvent(res, {
@@ -917,6 +1097,7 @@ export function chatRoutes(db, storage) {
917
1097
  },
918
1098
  onTranscriptEntry: async (entry) => {
919
1099
  transcript.push(entry);
1100
+ await persistStreamProgress(assistantInput.conversation);
920
1101
  if (clientClosed)
921
1102
  return;
922
1103
  writeStreamEvent(res, {
@@ -931,7 +1112,7 @@ export function chatRoutes(db, storage) {
931
1112
  if (streamed.outcome === "stopped") {
932
1113
  finalChatStatus = "stopped";
933
1114
  finalChatOutput = streamed.partialBody;
934
- const stoppedMessage = await persistPartialAssistantMessage(assistantInput.conversation, streamed.partialBody, "stopped", turnContextForPartial, transcript, streamed.replyingAgentId);
1115
+ const stoppedMessage = await persistPartialAssistantMessage(assistantInput.conversation, streamed.partialBody, "stopped", turnContextForPartial, transcript, streamed.replyingAgentId, assistantProgressMessageId);
935
1116
  if (stoppedMessage) {
936
1117
  await logChatMessagesAdded(assistantInput.conversation, [stoppedMessage], {
937
1118
  actorType: "system",
@@ -957,7 +1138,7 @@ export function chatRoutes(db, storage) {
957
1138
  }
958
1139
  return;
959
1140
  }
960
- const createdMessages = await persistAssistantReply(assistantInput.conversation, actor, streamed.reply, turnContextForPartial, transcript, streamed.replyingAgentId);
1141
+ const createdMessages = await persistAssistantReply(req, assistantInput.conversation, actor, streamed.reply, turnContextForPartial, transcript, streamed.replyingAgentId, assistantProgressMessageId);
961
1142
  finalChatOutput = streamed.reply.body;
962
1143
  await logChatMessagesAdded(assistantInput.conversation, createdMessages, {
963
1144
  actorType: "system",
@@ -993,6 +1174,14 @@ export function chatRoutes(db, storage) {
993
1174
  context: chatObservation,
994
1175
  parentObservation: observation,
995
1176
  transcript: observedTranscript,
1177
+ initialTurnInput: modelTurnInput,
1178
+ fallbackResult: finalChatOutput
1179
+ ? {
1180
+ output: finalChatOutput,
1181
+ subtype: finalChatStatus,
1182
+ isError: finalChatStatus === "failed",
1183
+ }
1184
+ : null,
996
1185
  });
997
1186
  finalChatOutput = finalChatOutput ?? transcriptStats.finalOutput ?? null;
998
1187
  }
@@ -1020,8 +1209,10 @@ export function chatRoutes(db, storage) {
1020
1209
  }
1021
1210
  catch (err) {
1022
1211
  const partialBody = err instanceof ChatAssistantStreamError ? err.partialBody : "";
1212
+ const generatedAttachments = err instanceof ChatAssistantStreamError ? err.generatedAttachments : [];
1023
1213
  const failedReplyingAgentId = chatReplyingAgentId(assistantConversationForPartial);
1024
- const failedMessage = await persistPartialAssistantMessage(assistantConversationForPartial ?? conversation, partialBody, "failed", turnContextForPartial, transcript, failedReplyingAgentId).catch(() => null);
1214
+ let failedMessage = await persistPartialAssistantMessage(assistantConversationForPartial ?? conversation, partialBody, "failed", turnContextForPartial, transcript, failedReplyingAgentId, assistantProgressMessageId).catch(() => null);
1215
+ failedMessage = await attachGeneratedFilesToPartialMessage(assistantConversationForPartial ?? conversation, failedMessage, generatedAttachments, failedReplyingAgentId).catch(() => failedMessage);
1025
1216
  if (failedMessage && assistantConversationForPartial) {
1026
1217
  await logChatMessagesAdded(assistantConversationForPartial, [failedMessage], {
1027
1218
  actorType: "system",
@@ -1186,6 +1377,11 @@ export function chatRoutes(db, storage) {
1186
1377
  entityId: projectId,
1187
1378
  }]);
1188
1379
  }
1380
+ const messages = await svc.listMessages(conversation.id);
1381
+ if (messages.length > 0) {
1382
+ res.status(409).json({ error: "Project context is locked after conversation starts" });
1383
+ return;
1384
+ }
1189
1385
  const updated = await svc.setProjectContextLink(conversation.id, conversation.orgId, projectId);
1190
1386
  if (!updated) {
1191
1387
  res.status(404).json({ error: "Chat conversation not found" });
@@ -1219,6 +1415,10 @@ export function chatRoutes(db, storage) {
1219
1415
  return;
1220
1416
  }
1221
1417
  }
1418
+ await assertCanConvertIssueProposal(req, conversation, {
1419
+ messageId: req.body.messageId ?? null,
1420
+ proposal: req.body.proposal ?? null,
1421
+ });
1222
1422
  const chatObservation = buildChatObservabilityContext(conversation, {
1223
1423
  rootExecutionId: req.body.messageId ?? `chat-convert:${conversation.id}`,
1224
1424
  trigger: "convert_to_issue",
@@ -1365,6 +1565,14 @@ export function chatRoutes(db, storage) {
1365
1565
  if (typeof req.body.pinned === "boolean") {
1366
1566
  await svc.setPinned(conversation.id, conversation.orgId, userId, req.body.pinned);
1367
1567
  }
1568
+ if (typeof req.body.unread === "boolean") {
1569
+ if (req.body.unread) {
1570
+ await svc.markUnread(conversation.id, conversation.orgId, userId);
1571
+ }
1572
+ else {
1573
+ await svc.markRead(conversation.id, conversation.orgId, userId);
1574
+ }
1575
+ }
1368
1576
  const refreshed = await svc.getById(conversation.id, userId);
1369
1577
  res.json(await assistantSvc.enrichConversation(refreshed));
1370
1578
  });