circuschief 0.6.0 → 0.8.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 (187) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/index.js +2 -0
  5. package/packages/server/src/api/kanban.js +4 -2
  6. package/packages/server/src/api/projects-helpers.js +20 -3
  7. package/packages/server/src/api/projects-session-helpers.js +35 -4
  8. package/packages/server/src/api/projects.js +11 -0
  9. package/packages/server/src/api/providers.js +11 -1
  10. package/packages/server/src/api/sessions-commands.js +35 -17
  11. package/packages/server/src/api/sessions-draft.js +1 -0
  12. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  13. package/packages/server/src/api/sessions-patch.js +4 -0
  14. package/packages/server/src/api/sessions.js +6 -5
  15. package/packages/server/src/api/settings.js +52 -4
  16. package/packages/server/src/database.js +0 -2
  17. package/packages/server/src/db/ConversationRepository.js +16 -3
  18. package/packages/server/src/db/DatabaseManager.js +5 -1
  19. package/packages/server/src/db/ProjectDefaultsRepository.js +50 -40
  20. package/packages/server/src/db/ProviderRepository.js +87 -32
  21. package/packages/server/src/db/SessionRepository.js +13 -8
  22. package/packages/server/src/db/SettingsRepository.js +44 -16
  23. package/packages/server/src/db/conversation-helpers.js +1 -0
  24. package/packages/server/src/db/index.js +0 -3
  25. package/packages/server/src/db/migrations/index.js +36 -200
  26. package/packages/server/src/db/seedBaselineData.js +137 -0
  27. package/packages/server/src/db/session-helpers.js +9 -3
  28. package/packages/server/src/middleware/sessionLookup.js +81 -8
  29. package/packages/server/src/schema.sql +157 -132
  30. package/packages/server/src/scripts/backupDatabase.js +21 -0
  31. package/packages/server/src/scripts/dbUtils.js +81 -0
  32. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  33. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  34. package/packages/server/src/services/agentCallLogger.js +1 -1
  35. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  36. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  37. package/packages/server/src/services/commandRunner.js +7 -1
  38. package/packages/server/src/services/draftSessionService.js +2 -0
  39. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  40. package/packages/server/src/services/gitCommitAttribution.js +120 -0
  41. package/packages/server/src/services/gitService.js +11 -2
  42. package/packages/server/src/services/gitSessionSetup.js +11 -1
  43. package/packages/server/src/services/kanbanTriggers.js +6 -3
  44. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  45. package/packages/server/src/services/prUrlService.js +3 -3
  46. package/packages/server/src/services/queryParamBuilder.js +90 -0
  47. package/packages/server/src/services/sessionDuplicator.js +1 -5
  48. package/packages/server/src/services/sessionExecution.js +56 -106
  49. package/packages/server/src/services/sessionPrompts.js +16 -47
  50. package/packages/server/src/services/sessionProvider.js +16 -8
  51. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  52. package/packages/server/src/services/streamEventHandler.js +13 -2
  53. package/packages/server/src/services/streamUsageHandler.js +6 -0
  54. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  55. package/packages/server/src/services/summaryModelClient.js +154 -0
  56. package/packages/server/src/services/summaryModelResolver.js +148 -0
  57. package/packages/server/src/services/summaryService.js +11 -7
  58. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  59. package/packages/server/src/services/templateTriggerService.js +3 -1
  60. package/packages/server/src/services/usageTracker.js +5 -1
  61. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  62. package/packages/shared/src/constants.js +4 -2
  63. package/packages/shared/src/contracts/commandButtons.js +16 -2
  64. package/packages/shared/src/contracts/projects.js +4 -2
  65. package/packages/shared/src/contracts/providers.js +60 -0
  66. package/packages/shared/src/contracts/sessions.js +2 -1
  67. package/packages/shared/src/contracts/templates.js +2 -2
  68. package/packages/shared/src/types.js +1 -9
  69. package/packages/shared/src/utils.js +11 -19
  70. package/packages/web/dist/assets/ActiveSessionsView-B0XHqLmv.js +1 -0
  71. package/packages/web/dist/assets/{AgentLogsView-Cdw4nmvd.js → AgentLogsView-DmsjUMlB.js} +2 -2
  72. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +1 -0
  73. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  75. package/packages/web/dist/assets/{CommandButtonDetailView-DnFhJY5A.js → CommandButtonDetailView-CdSCPp78.js} +1 -1
  76. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +1 -0
  77. package/packages/web/dist/assets/{GeneralSettingsView-CQkmdczf.js → GeneralSettingsView-D1nI8_zk.js} +1 -1
  78. package/packages/web/dist/assets/InputWithButton-CAkttyqx.js +1 -0
  79. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  80. package/packages/web/dist/assets/{InterpolationHelp-PfYR3KJo.js → InterpolationHelp-BO1j9Z3_.js} +1 -1
  81. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +2 -0
  82. package/packages/web/dist/assets/{ModelSelector-BZOT1Jc6.css → ModelSelector-BSxKUSus.css} +1 -1
  83. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +1 -0
  84. package/packages/web/dist/assets/NewSessionView-BDPb-1qr.css +1 -0
  85. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +3 -0
  86. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +1 -0
  87. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +1 -0
  88. package/packages/web/dist/assets/{ProjectListView-CuYMmd3O.js → ProjectListView-DcNyuINs.js} +1 -1
  89. package/packages/web/dist/assets/{ProjectNewView-CNaA4Maf.js → ProjectNewView-B5YV62hv.js} +1 -1
  90. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  91. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +1 -0
  92. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +1 -0
  93. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +1 -0
  94. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  95. package/packages/web/dist/assets/{QuickResponsesPanel-BqMYSHb0.js → QuickResponsesPanel-BzSYcCSP.js} +1 -1
  96. package/packages/web/dist/assets/{ResizableTextarea-wYF3K2RO.js → ResizableTextarea-B3YIdIXv.js} +1 -1
  97. package/packages/web/dist/assets/{SessionCard-bLaQEWWX.js → SessionCard-CjE1tXiT.js} +1 -1
  98. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +36 -0
  99. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +1 -0
  100. package/packages/web/dist/assets/SessionFormOptions-B6AxyREh.js +1 -0
  101. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  102. package/packages/web/dist/assets/SessionListView-B5_6gW49.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DTnDAF95.js → SessionLogStream-LlZ3z_Xj.js} +1 -1
  105. package/packages/web/dist/assets/{SettingsView-DNLUSsHV.js → SettingsView-CTGiGvR2.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-CRGFaO8t.js → SlashCommandWizard-Cy04d7-o.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/SummarySettingsView-BR2ZjEa3.js +1 -0
  109. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-Bbjf3fCt.js → commandButtons-BfqR-fqq.js} +1 -1
  112. package/packages/web/dist/assets/index-1zziPL6l.js +1 -0
  113. package/packages/web/dist/assets/index-7kzHPxSF.js +1 -0
  114. package/packages/web/dist/assets/index-B0N_obMc.js +1 -0
  115. package/packages/web/dist/assets/index-BNk_gdfI.js +1 -0
  116. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-BY174HVJ.css} +1 -1
  117. package/packages/web/dist/assets/index-CSqaAH-0.js +1 -0
  118. package/packages/web/dist/assets/index-C_q4WlK8.js +1 -0
  119. package/packages/web/dist/assets/index-D1wpU4y0.js +7 -0
  120. package/packages/web/dist/assets/index-D5zCA8sD.js +1 -0
  121. package/packages/web/dist/assets/index-DGR8ELWY.js +1 -0
  122. package/packages/web/dist/assets/index-DHga8pXo.js +1 -0
  123. package/packages/web/dist/assets/index-DSby02Wl.js +1 -0
  124. package/packages/web/dist/assets/{index-Cf6vdW-B.js → index-DgkC10TW.js} +3 -3
  125. package/packages/web/dist/assets/index-DqjXJTVI.js +1 -0
  126. package/packages/web/dist/assets/{index-Bs7Qf5D6.js → index-DtfUt785.js} +2 -2
  127. package/packages/web/dist/assets/index-_4S2uLDI.js +1 -0
  128. package/packages/web/dist/assets/{index-BQL_L4gL.js → index-fK8FIZgP.js} +15 -14
  129. package/packages/web/dist/assets/index-gmiZeFXN.js +1 -0
  130. package/packages/web/dist/assets/index-irD539ZM.js +3 -0
  131. package/packages/web/dist/assets/index-yq-E1Y00.js +1 -0
  132. package/packages/web/dist/assets/{projects-CPt3AB7U.js → projects-DXYQNJIi.js} +1 -1
  133. package/packages/web/dist/assets/{providers-ChfeMvUq.js → providers-1bnH-exJ.js} +1 -1
  134. package/packages/web/dist/assets/sessions-6zGUlFrt.js +1 -0
  135. package/packages/web/dist/assets/settings-MbfRir0d.js +1 -0
  136. package/packages/web/dist/index.html +2 -2
  137. package/packages/server/src/api/sessions-notes.js +0 -51
  138. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  139. package/packages/server/src/db/migrations/canvasItemsMigrations.js +0 -109
  140. package/packages/server/src/db/migrations/conversationsMigrations.js +0 -183
  141. package/packages/server/src/db/migrations/kanbanMigrations.js +0 -99
  142. package/packages/server/src/db/migrations/miscMigrations.js +0 -369
  143. package/packages/server/src/db/migrations/projectsMigrations.js +0 -99
  144. package/packages/server/src/db/migrations/sessionsMigrations.js +0 -282
  145. package/packages/web/dist/assets/ActiveSessionsView-UCbQrF1b.js +0 -1
  146. package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +0 -1
  147. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  148. package/packages/web/dist/assets/ArchiveConfirmModal-J48eh3zw.js +0 -1
  149. package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +0 -1
  150. package/packages/web/dist/assets/InputWithButton-XyM3k6lN.js +0 -1
  151. package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +0 -2
  152. package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +0 -1
  153. package/packages/web/dist/assets/NewSessionView-D_Hi7M9g.css +0 -1
  154. package/packages/web/dist/assets/NewSessionView-DkjFLvHU.js +0 -3
  155. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  156. package/packages/web/dist/assets/ProjectEditView-embVT7NC.js +0 -1
  157. package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +0 -1
  158. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  159. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  160. package/packages/web/dist/assets/QuickResponseSettings-BTQEKhwJ.js +0 -1
  161. package/packages/web/dist/assets/SessionDetailView-Cv-xMzXp.css +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CvQOUsW2.js +0 -36
  163. package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +0 -1
  164. package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +0 -1
  165. package/packages/web/dist/assets/SessionListView-Dranfb72.js +0 -1
  166. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  167. package/packages/web/dist/assets/SummarySettingsView-C7G_suHp.js +0 -1
  168. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  169. package/packages/web/dist/assets/TemplateDetailView-B78_DLMR.js +0 -1
  170. package/packages/web/dist/assets/index--V7c-VZf.js +0 -1
  171. package/packages/web/dist/assets/index-8Q04yd7H.js +0 -1
  172. package/packages/web/dist/assets/index-B47XRBDH.js +0 -1
  173. package/packages/web/dist/assets/index-BXbgZrhS.js +0 -1
  174. package/packages/web/dist/assets/index-CGhDVPen.js +0 -1
  175. package/packages/web/dist/assets/index-CKcRO1A6.js +0 -1
  176. package/packages/web/dist/assets/index-CTq-SLIW.js +0 -1
  177. package/packages/web/dist/assets/index-CYyos3iC.js +0 -1
  178. package/packages/web/dist/assets/index-CsCREAxF.js +0 -1
  179. package/packages/web/dist/assets/index-DJTTk_8T.js +0 -3
  180. package/packages/web/dist/assets/index-DPqUJ5JK.js +0 -1
  181. package/packages/web/dist/assets/index-EwAe1dKg.js +0 -1
  182. package/packages/web/dist/assets/index-JBA8axyA.js +0 -1
  183. package/packages/web/dist/assets/index-JkVHFtK5.js +0 -7
  184. package/packages/web/dist/assets/index-gMPUwT55.js +0 -1
  185. package/packages/web/dist/assets/index-wadc_0zT.js +0 -1
  186. package/packages/web/dist/assets/sessions-CwPsJOb1.js +0 -1
  187. package/packages/web/dist/assets/settings-BOj6wq6t.js +0 -1
@@ -1,5 +1,6 @@
1
1
  import { sessions, attachments, projects, kanbanBoards, kanbanLanes } from '../database.js';
2
2
  import { DEFAULT_SERVER_PORT, DEFAULT_SYSTEM_PROMPT } from '../../../shared/src/index.js';
3
+ import { buildCommandButtonApiInstructions } from './commandButtonPrompts.js';
3
4
 
4
5
  /**
5
6
  * Get the base API URL for canvas and session operations.
@@ -161,10 +162,7 @@ CRITICAL: Do NOT start coding until you have presented a plan and received appro
161
162
  */
162
163
  function buildCanvasWriteSystemPrompt(session) {
163
164
  const apiUrl = getApiBaseUrl();
164
- // Use root session ID, fall back to session.id if getRootSessionId returns null, fall back to 'unknown-session' if session is null
165
- const sessionId = session
166
- ? (sessions.getRootSessionId(session.id) || session.id)
167
- : 'unknown-session';
165
+ const sessionId = session?.id || 'unknown-session';
168
166
  return `When you generate artifacts that should be displayed on the canvas (images, markdown documents, code snippets, data visualizations, PDFs), POST them to:
169
167
 
170
168
  POST ${apiUrl}/api/sessions/${sessionId}/canvas
@@ -186,10 +184,7 @@ The file type is automatically detected from the file extension. Supported forma
186
184
  */
187
185
  function buildCanvasReadSystemPrompt(session) {
188
186
  const apiUrl = getApiBaseUrl();
189
- // Use root session ID, fall back to session.id if getRootSessionId returns null, fall back to 'unknown-session' if session is null
190
- const sessionId = session
191
- ? (sessions.getRootSessionId(session.id) || session.id)
192
- : 'unknown-session';
187
+ const sessionId = session?.id || 'unknown-session';
193
188
  return `## Reading from Canvas
194
189
 
195
190
  To list all files on the canvas:
@@ -224,9 +219,10 @@ function buildSessionCrudOps(apiUrl, projectId) {
224
219
  \`\`\`bash
225
220
  curl -X POST ${apiUrl}/api/projects/${projectId}/sessions \\
226
221
  -H "Content-Type: application/json" \\
227
- -d '{"prompt": "Your task description here", "name": "Optional session name"}'
222
+ -d '{"prompt": "Your task description here"}'
228
223
  \`\`\`
229
- Optional fields: \`name\`, \`mode\`, \`thinkingEnabled\` (boolean), \`effortLevel\` (low/medium/high/max/auto), \`gitBranch\`, \`gitMode\`, \`parentSessionId\` (to create a child session)
224
+ Only \`prompt\` is required. Omitted settings are automatically derived from the project's session defaults, then system defaults, matching UI behavior.
225
+ Optional override fields: \`name\`, \`mode\`, \`thinkingEnabled\` (boolean), \`effortLevel\` (low/medium/high/max/auto), \`model\`, \`providerId\`, \`gitBranch\`, \`gitMode\`, \`templateId\`, \`nextTemplateId\`, \`parentSessionId\` (to create a related follow-up session from the current session), \`startImmediately\`, \`scheduledAt\`, \`autoRescheduleEnabled\`, \`rescheduleDelayMinutes\`, \`rescheduleOnTokenLimit\`, \`rescheduleOnServiceError\`, \`maxRescheduleCount\`, \`maxTotalTokens\`, and \`rescheduleAtTokenCount\`.
230
226
 
231
227
  ### Send a Follow-up Message
232
228
  \`\`\`bash
@@ -257,8 +253,8 @@ curl -X PATCH ${apiUrl}/api/sessions/<session_id> \\
257
253
  \`\`\``;
258
254
  }
259
255
 
260
- /** Build project, notes, and summary operations section */
261
- function buildProjectNotesOps(apiUrl) {
256
+ /** Build project and summary operations section */
257
+ function buildProjectOps(apiUrl, sessionId) {
262
258
  return `### Project Operations
263
259
  \`\`\`bash
264
260
  curl ${apiUrl}/api/projects # List all projects
@@ -270,18 +266,10 @@ curl -X POST ${apiUrl}/api/projects \\
270
266
  \`\`\`
271
267
  Optional field: \`systemPrompt\`
272
268
 
273
- ### Session Notes
269
+ ### Workflow Summary
274
270
  \`\`\`bash
275
- curl ${apiUrl}/api/sessions/<session_id>/notes # Get notes
276
- curl -X POST ${apiUrl}/api/sessions/<session_id>/notes \\
277
- -H "Content-Type: application/json" \\
278
- -d '{"content": "Note content"}'
279
- \`\`\`
280
-
281
- ### Session Summary
282
- \`\`\`bash
283
- curl "${apiUrl}/api/sessions/<session_id>/summary?generate=true"
284
- curl -X POST ${apiUrl}/api/sessions/<session_id>/summary # Regenerate
271
+ curl "${apiUrl}/api/sessions/${sessionId}/summary?generate=true"
272
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/summary # Regenerate
285
273
  \`\`\``;
286
274
  }
287
275
 
@@ -298,7 +286,7 @@ You can create and modify sessions in this system using curl or similar HTTP too
298
286
 
299
287
  ${buildSessionCrudOps(apiUrl, projectId)}
300
288
 
301
- ${buildProjectNotesOps(apiUrl)}`;
289
+ ${buildProjectOps(apiUrl, sessionId)}`;
302
290
  }
303
291
 
304
292
  /**
@@ -335,7 +323,7 @@ ${laneContext}
335
323
  curl ${apiUrl}/api/projects/${projectId}/kanban
336
324
  \`\`\`
337
325
 
338
- ### Add This Session to the Board
326
+ ### Add Current Workflow to the Board
339
327
  \`\`\`bash
340
328
  curl -X POST ${apiUrl}/api/projects/${projectId}/kanban/cards \\
341
329
  -H "Content-Type: application/json" \\
@@ -393,26 +381,6 @@ This session is running in an isolated git worktree:
393
381
  CRITICAL: Do NOT use \`cd\` to navigate to the main repository. Your working directory is already set to the worktree. Running \`cd /home/ubuntu/workspace/circus-chief && ...\` will escape the worktree isolation and affect the main repository instead.`;
394
382
  }
395
383
 
396
- /**
397
- * Build child session context for system prompt
398
- * @param {Object} session - Session object
399
- * @returns {string} Child session context or empty string
400
- */
401
- function buildChildSessionContext(session) {
402
- if (!session || !session.parentSessionId) {
403
- return '';
404
- }
405
-
406
- // Get root session ID using existing method
407
- const rootSessionId = sessions.getRootSessionId(session.id);
408
-
409
- return `## Child Session
410
-
411
- This session is part of a multi-session workflow:
412
- - Parent Session ID: ${session.parentSessionId}
413
- - Root Session ID: ${rootSessionId}`;
414
- }
415
-
416
384
  /**
417
385
  * Build the full system prompt configuration
418
386
  * @param {string} sessionId
@@ -422,14 +390,15 @@ This session is part of a multi-session workflow:
422
390
  * @returns {string} System prompt string
423
391
  */
424
392
  export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt, mode) {
393
+ const apiUrl = getApiBaseUrl();
425
394
  const session = sessions.getById(sessionId);
426
395
  const canvasWriteInstructions = buildCanvasWriteSystemPrompt(session); // Pass session object
427
396
  const canvasReadInstructions = buildCanvasReadSystemPrompt(session); // Pass session object
428
397
  const sessionApiInstructions = buildSessionApiInstructions(sessionId, projectId);
398
+ const commandButtonApiInstructions = buildCommandButtonApiInstructions(apiUrl, sessionId, projectId);
429
399
  const kanbanApiInstructions = buildKanbanApiInstructions(sessionId, projectId);
430
400
  const attachmentsContext = getSessionAttachmentsContext(sessionId);
431
401
  const worktreeContext = buildWorktreeContext(session);
432
- const childSessionContext = buildChildSessionContext(session);
433
402
  const basePrompt = customSystemPrompt || DEFAULT_SYSTEM_PROMPT;
434
403
 
435
404
  // Prepend plan mode instructions if in plan mode
@@ -439,12 +408,12 @@ export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt
439
408
  const parts = [
440
409
  modePrompt,
441
410
  basePrompt,
442
- childSessionContext,
443
411
  worktreeContext,
444
412
  attachmentsContext,
445
413
  canvasWriteInstructions,
446
414
  canvasReadInstructions,
447
415
  sessionApiInstructions,
416
+ commandButtonApiInstructions,
448
417
  kanbanApiInstructions
449
418
  ].filter(Boolean);
450
419
 
@@ -11,6 +11,16 @@ export function resolveProviderFromModel(modelId) {
11
11
  return modelProviders.getProviderByModelId(modelId);
12
12
  }
13
13
 
14
+ export function resolveProviderMetadataFromModel(modelId) {
15
+ if (!modelId) {
16
+ return modelProviders.getById?.('anthropic-default') || null;
17
+ }
18
+ if (typeof modelProviders.getProviderMetadataByModelId === 'function') {
19
+ return modelProviders.getProviderMetadataByModelId(modelId);
20
+ }
21
+ return modelProviders.getProviderByModelId(modelId);
22
+ }
23
+
14
24
  /**
15
25
  * Resolve the agent type (claude-code vs codex) for a given model ID.
16
26
  * Uses the owning provider's kind:
@@ -204,14 +214,12 @@ function stripAnthropicHostEnv(env) {
204
214
 
205
215
  function replaceWithCodexCliEnv(sessionEnv, providerEnv) {
206
216
  const target = sessionEnv;
207
- const allowed = ['HOME', 'PATH', 'USER', 'LOGNAME', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'TMPDIR'];
208
- const cleaned = {};
209
- for (const key of allowed) {
210
- if (target[key] !== undefined) cleaned[key] = target[key];
211
- }
212
- Object.assign(cleaned, providerEnv);
213
- for (const key of Object.keys(target)) delete target[key];
214
- Object.assign(target, cleaned);
217
+ delete target.OPENAI_API_KEY;
218
+ delete target.OPENAI_BASE_URL;
219
+ delete target.OPENAI_API_BASE;
220
+ delete target.OPENAI_ORG_ID;
221
+ delete target.OPENAI_PROJECT;
222
+ Object.assign(target, providerEnv);
215
223
  }
216
224
 
217
225
  function stripOpenAIBaseUrlUnlessProvided(sessionEnv, providerEnv) {
@@ -3,71 +3,102 @@ import { broadcastToSession } from '../websocket.js';
3
3
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
4
4
  import * as summaryService from './summaryService.js';
5
5
  import * as kanbanService from './kanbanService.js';
6
+ import { createVisibleFinalErrorMessage } from './visibleFinalErrorMessage.js';
6
7
  import {
7
8
  lastMessageIds,
8
9
  activeSessions,
9
10
  activeConversationIds,
11
+ finalErrorSessionIds,
10
12
  associateAndBroadcastWorkLogs,
11
13
  broadcastSessionStatus,
12
14
  broadcastChangesUpdate,
13
15
  } from './streamEventHandler.js';
14
16
 
15
17
  /**
16
- * Handle post-turn completion logic (after stream loop ends successfully)
17
- * Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
18
+ * Associate work logs with the last message and clean up tracking state.
18
19
  * @param {string} sessionId
19
- * @param {string} workingDirectory
20
- * @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
21
20
  */
22
- export async function handleTurnCompletion(sessionId, workingDirectory, callbacks = {}) {
23
- const { handleTemplateTriggerIfNeeded, checkProactiveReschedule: _checkProactiveReschedule, handleAutoSendIfNeeded } = callbacks;
24
- // Associate work logs with the last message now that the turn is complete
21
+ function associateAndCleanupWorkLogs(sessionId) {
25
22
  const lastMessageId = lastMessageIds.get(sessionId);
26
23
  if (lastMessageId) {
27
24
  associateAndBroadcastWorkLogs(sessionId, lastMessageId);
28
25
  lastMessageIds.delete(sessionId);
29
26
  }
27
+ }
30
28
 
31
- // Session ready for follow-up - set to waiting instead of completed
32
- const activeSession = activeSessions.get(sessionId);
33
- if (activeSession && !activeSession.controller?.signal?.aborted) {
34
- sessions.update(sessionId, { status: 'waiting' });
35
- broadcastSessionStatus(sessionId, 'waiting');
36
-
37
- // Check if session should be proactively rescheduled based on token threshold
38
- if (_checkProactiveReschedule) {
39
- const wasRescheduled = await _checkProactiveReschedule(sessionId);
40
- if (wasRescheduled) {
41
- return true; // Session was rescheduled, don't continue with normal completion
42
- }
29
+ /**
30
+ * Handle post-turn activities for a session that's ready for follow-up.
31
+ * @param {string} sessionId
32
+ * @param {string} workingDirectory
33
+ * @param {{ checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function, handleTemplateTriggerIfNeeded?: Function }} callbacks
34
+ * @returns {Promise<boolean>} True if rescheduled, false otherwise
35
+ */
36
+ async function handleActiveSessionCompletion(sessionId, workingDirectory, callbacks) {
37
+ sessions.update(sessionId, { status: 'waiting', error: null });
38
+ broadcastSessionStatus(sessionId, 'waiting');
39
+
40
+ // Check if session should be proactively rescheduled based on token threshold
41
+ const { checkProactiveReschedule } = callbacks;
42
+ if (checkProactiveReschedule) {
43
+ const wasRescheduled = await checkProactiveReschedule(sessionId);
44
+ if (wasRescheduled) {
45
+ return true; // Session was rescheduled, don't continue with normal completion
43
46
  }
47
+ }
44
48
 
45
- // Extract PR URL immediately (lightweight, no API call)
46
- summaryService.extractPrUrlIfNeeded(sessionId);
47
- // Trigger debounced summary generation on turn completion (not complete yet)
48
- summaryService.onSessionActivity(sessionId);
49
+ // Extract PR URL immediately (lightweight, no API call)
50
+ summaryService.extractPrUrlIfNeeded(sessionId);
51
+ // Trigger debounced summary generation on turn completion (not complete yet)
52
+ summaryService.onSessionActivity(sessionId);
49
53
 
50
- // Broadcast changes update when turn completes (real-time indicator)
51
- const currentSession = sessions.getById(sessionId);
52
- if (currentSession) {
53
- await broadcastChangesUpdate(sessionId, currentSession.projectId, workingDirectory);
54
- }
54
+ // Broadcast changes update when turn completes (real-time indicator)
55
+ const currentSession = sessions.getById(sessionId);
56
+ if (currentSession) {
57
+ await broadcastChangesUpdate(sessionId, currentSession.projectId, workingDirectory);
58
+ }
55
59
 
56
- // Handle kanban lane movements based on targetLaneId
57
- await kanbanService.handleTurnCompletion(sessionId);
60
+ // Handle kanban lane movements based on targetLaneId
61
+ await kanbanService.handleTurnCompletion(sessionId);
58
62
 
59
- // Auto-send queued prompt if enabled (runs BEFORE template trigger)
60
- let autoSendFired = false;
61
- if (handleAutoSendIfNeeded) {
62
- autoSendFired = await handleAutoSendIfNeeded(sessionId);
63
- }
63
+ // Auto-send queued prompt if enabled (runs BEFORE template trigger)
64
+ const { handleAutoSendIfNeeded, handleTemplateTriggerIfNeeded } = callbacks;
65
+ let autoSendFired = false;
66
+ if (handleAutoSendIfNeeded) {
67
+ autoSendFired = await handleAutoSendIfNeeded(sessionId);
68
+ }
64
69
 
65
- // Only trigger next template if auto-send did NOT fire
66
- // (if auto-send fired, template will trigger after that turn completes)
67
- if (!autoSendFired && handleTemplateTriggerIfNeeded) {
68
- await handleTemplateTriggerIfNeeded(sessionId);
69
- }
70
+ // Only trigger next template if auto-send did NOT fire
71
+ // (if auto-send fired, template will trigger after that turn completes)
72
+ if (!autoSendFired && handleTemplateTriggerIfNeeded) {
73
+ await handleTemplateTriggerIfNeeded(sessionId);
70
74
  }
75
+
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Handle post-turn completion logic (after stream loop ends successfully)
81
+ * Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
82
+ * @param {string} sessionId
83
+ * @param {string} workingDirectory
84
+ * @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
85
+ */
86
+ export async function handleTurnCompletion(sessionId, workingDirectory, callbacks = {}) {
87
+ // Associate work logs with the last message now that the turn is complete
88
+ associateAndCleanupWorkLogs(sessionId);
89
+
90
+ // Sessions with final errors should not transition to waiting
91
+ if (finalErrorSessionIds.has(sessionId)) {
92
+ finalErrorSessionIds.delete(sessionId);
93
+ return false;
94
+ }
95
+
96
+ // Session ready for follow-up - set to waiting instead of completed
97
+ const activeSession = activeSessions.get(sessionId);
98
+ if (activeSession && !activeSession.controller?.signal?.aborted) {
99
+ return handleActiveSessionCompletion(sessionId, workingDirectory, callbacks);
100
+ }
101
+
71
102
  return false;
72
103
  }
73
104
 
@@ -149,6 +180,7 @@ export async function handleSessionError(sessionId, error, options = {}) {
149
180
 
150
181
  // Normal error handling (no reschedule or reschedule limits reached)
151
182
  sessions.update(sessionId, { status: 'error', error: error.message });
183
+ createVisibleFinalErrorMessage(sessionId, error, activeConversationIds);
152
184
  broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: error.message });
153
185
 
154
186
  // Optionally broadcast final conversation state (continueSession does this)
@@ -10,6 +10,10 @@ import {
10
10
  handleTextDelta as _handleTextDelta,
11
11
  handleResultUsage,
12
12
  } from './streamUsageHandler.js';
13
+ import {
14
+ createVisibleFinalErrorMessage,
15
+ normalizeFinalErrorMessage,
16
+ } from './visibleFinalErrorMessage.js';
13
17
 
14
18
  // ── Shared module-level state ──────────────────────────────────────────────
15
19
 
@@ -34,6 +38,9 @@ export const currentModels = new Map();
34
38
  /** @type {Map<string, Set<string>>} Track tool_use IDs that have already been logged per session */
35
39
  export const loggedToolUseIds = new Map();
36
40
 
41
+ /** @type {Set<string>} Track sessions that received a final result.error event */
42
+ export const finalErrorSessionIds = new Set();
43
+
37
44
  // ── Helper functions ───────────────────────────────────────────────────────
38
45
 
39
46
  /**
@@ -404,8 +411,11 @@ function handleResultEvent(sessionId, event) {
404
411
  * @param {Object} event
405
412
  */
406
413
  function handleResultError(sessionId, event) {
407
- sessions.update(sessionId, { status: 'error', error: event.error });
408
- broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: event.error });
414
+ const errorMessage = normalizeFinalErrorMessage(event.error);
415
+ finalErrorSessionIds.add(sessionId);
416
+ sessions.update(sessionId, { status: 'error', error: errorMessage });
417
+ createVisibleFinalErrorMessage(sessionId, errorMessage, activeConversationIds);
418
+ broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: errorMessage });
409
419
  // Broadcast error status to project subscribers for session list updates
410
420
  broadcastSessionStatus(sessionId, 'error');
411
421
  // Extract PR URL before generating summary (PR may have been created before error)
@@ -481,6 +491,7 @@ export function cleanupSessionState(sessionId, includeConversationId = false) {
481
491
  thinkingAccumulators.delete(sessionId);
482
492
  currentModels.delete(sessionId);
483
493
  loggedToolUseIds.delete(sessionId);
494
+ finalErrorSessionIds.delete(sessionId);
484
495
  activeSessions.delete(sessionId);
485
496
  if (includeConversationId) {
486
497
  activeConversationIds.delete(sessionId);
@@ -74,6 +74,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
74
74
  const turnData = currentTurnUsage.get(conversationId) || {
75
75
  inputTokens: 0,
76
76
  outputTokens: 0,
77
+ thinkingTokens: 0,
77
78
  lastMessageOutput: 0,
78
79
  cacheReadInputTokens: 0,
79
80
  cacheCreationInputTokens: 0,
@@ -83,6 +84,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
83
84
  const broadcastUsage = {
84
85
  inputTokens: turnData.inputTokens,
85
86
  outputTokens: turnData.outputTokens + Math.max(turnData.lastMessageOutput, newEstimate),
87
+ thinkingTokens: turnData.thinkingTokens,
86
88
  cacheReadInputTokens: turnData.cacheReadInputTokens,
87
89
  cacheCreationInputTokens: turnData.cacheCreationInputTokens,
88
90
  };
@@ -123,6 +125,7 @@ export function extractTurnUsage(sessionId, event) {
123
125
  return {
124
126
  inputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'inputTokens', snake: 'input_tokens' }),
125
127
  outputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'outputTokens', snake: 'output_tokens' }),
128
+ thinkingTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'thinkingTokens', snake: 'thinking_tokens' }),
126
129
  cacheReadInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheReadInputTokens', snake: 'cache_read_input_tokens' }),
127
130
  cacheCreationInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheCreationInputTokens', snake: 'cache_creation_input_tokens' }),
128
131
  webSearchRequests: modelUsageEntry?.webSearchRequests || 0,
@@ -142,6 +145,7 @@ export function buildCumulativeSessionUsage(sessionId, turnUsage) {
142
145
  return {
143
146
  inputTokens: (currentSession.inputTokens || 0) + turnUsage.inputTokens,
144
147
  outputTokens: (currentSession.outputTokens || 0) + turnUsage.outputTokens,
148
+ thinkingTokens: (currentSession.thinkingTokens || 0) + turnUsage.thinkingTokens,
145
149
  cacheReadInputTokens: (currentSession.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
146
150
  cacheCreationInputTokens: (currentSession.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
147
151
  webSearchRequests: (currentSession.webSearchRequests || 0) + turnUsage.webSearchRequests,
@@ -162,6 +166,7 @@ export function updateConversationUsage(conversationId, currentConversation, tur
162
166
  const cumulativeConversationUsage = {
163
167
  inputTokens: (currentConversation.inputTokens || 0) + turnUsage.inputTokens,
164
168
  outputTokens: (currentConversation.outputTokens || 0) + turnUsage.outputTokens,
169
+ thinkingTokens: (currentConversation.thinkingTokens || 0) + turnUsage.thinkingTokens,
165
170
  cacheReadInputTokens: (currentConversation.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
166
171
  cacheCreationInputTokens: (currentConversation.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
167
172
  webSearchRequests: (currentConversation.webSearchRequests || 0) + turnUsage.webSearchRequests,
@@ -197,6 +202,7 @@ export function handleResultUsage(sessionId, event) {
197
202
  usage: updatedConversation ? {
198
203
  inputTokens: updatedConversation.inputTokens,
199
204
  outputTokens: updatedConversation.outputTokens,
205
+ thinkingTokens: updatedConversation.thinkingTokens,
200
206
  cacheReadInputTokens: updatedConversation.cacheReadInputTokens,
201
207
  cacheCreationInputTokens: updatedConversation.cacheCreationInputTokens,
202
208
  webSearchRequests: updatedConversation.webSearchRequests,
@@ -49,11 +49,16 @@ function logResultUsage(callId, event) {
49
49
  /**
50
50
  * Build the query parameters for the Claude SDK call.
51
51
  * @param {string} prompt - The prompt to send
52
- * @param {{ systemPrompt?: string, jsonSchema?: Object }} options
52
+ * @param {{ systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object }} options
53
53
  * @returns {Object} queryParams ready for the SDK query function
54
54
  */
55
55
  function buildClaudeRequest(prompt, options) {
56
- const { systemPrompt = null, jsonSchema = null } = options || {};
56
+ const {
57
+ systemPrompt = null,
58
+ jsonSchema = null,
59
+ model = 'claude-haiku-4-5-20251001',
60
+ env = null,
61
+ } = options || {};
57
62
  const schema = jsonSchema || SESSION_SUMMARY_SCHEMA;
58
63
 
59
64
  return {
@@ -62,7 +67,8 @@ function buildClaudeRequest(prompt, options) {
62
67
  cwd: process.cwd(),
63
68
  permissionMode: 'bypassPermissions',
64
69
  maxTurns: 1,
65
- model: 'claude-haiku-4-5-20251001',
70
+ model,
71
+ ...(env && { env }),
66
72
  ...(systemPrompt && { systemPrompt }),
67
73
  outputFormat: {
68
74
  type: 'json_schema',
@@ -108,6 +114,21 @@ async function handleClaudeResponse(eventStream, callId) {
108
114
  return state.responseText;
109
115
  }
110
116
 
117
+ function buildSummaryCallMetadata({ logMeta, model, providerId, selectionReason, promptLength }) {
118
+ return {
119
+ sessionId: logMeta.sessionId,
120
+ conversationId: logMeta.conversationId || null,
121
+ agentType: 'summary',
122
+ model,
123
+ callType: logMeta.callType,
124
+ promptLength,
125
+ metadata: {
126
+ ...(providerId ? { providerId } : {}),
127
+ ...(selectionReason ? { selectionReason } : {}),
128
+ },
129
+ };
130
+ }
131
+
111
132
  export const SESSION_SUMMARY_SCHEMA = {
112
133
  type: 'object',
113
134
  properties: {
@@ -130,11 +151,16 @@ export const SESSION_SUMMARY_SCHEMA = {
130
151
  * @param {string} prompt - The prompt to send
131
152
  * @param {Array} recentMessages - Messages (for mock mode context)
132
153
  * @param {string} sessionStatus - Session status (for mock mode context)
133
- * @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object }} options - Optional parameters
154
+ * @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object, providerId?: string|null, selectionReason?: string }} options - Optional parameters
134
155
  * @returns {Promise<string>} The text response (JSON string)
135
156
  */
136
157
  export async function callClaude(prompt, recentMessages, sessionStatus, options = {}) {
137
- const { logMeta = null } = options || {};
158
+ const {
159
+ logMeta = null,
160
+ model = 'claude-haiku-4-5-20251001',
161
+ providerId = null,
162
+ selectionReason = null,
163
+ } = options || {};
138
164
  // Build stable key for VCR cassette (session prompts are hardcoded strings in E2E tests)
139
165
  let keyHint = null;
140
166
  if (process.env.VCR_MODE && logMeta?.sessionId) {
@@ -151,14 +177,13 @@ export async function callClaude(prompt, recentMessages, sessionStatus, options
151
177
  // Start logging if metadata provided
152
178
  let callId = null;
153
179
  if (logMeta) {
154
- callId = agentCallLogger.startCall({
155
- sessionId: logMeta.sessionId,
156
- conversationId: logMeta.conversationId || null,
157
- agentType: 'summary',
158
- model: 'claude-haiku-4-5-20251001',
159
- callType: logMeta.callType,
180
+ callId = agentCallLogger.startCall(buildSummaryCallMetadata({
181
+ logMeta,
182
+ model,
183
+ providerId,
184
+ selectionReason,
160
185
  promptLength: prompt.length,
161
- });
186
+ }));
162
187
  }
163
188
 
164
189
  try {