@straiffi/archon 1.7.2 → 1.7.4

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 (176) hide show
  1. package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
  3. package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
  33. package/dist/client/assets/index-CJRgmWwY.js +273 -0
  34. package/dist/client/assets/index-mRkJWu70.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +472 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1311 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +228 -71
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/attachments.js +24 -0
  64. package/dist/server/lib/attachments.js.map +1 -1
  65. package/dist/server/lib/automationProgression.js +464 -0
  66. package/dist/server/lib/automationProgression.js.map +1 -0
  67. package/dist/server/lib/automationReconciliation.js +498 -0
  68. package/dist/server/lib/automationReconciliation.js.map +1 -0
  69. package/dist/server/lib/automationStageDetails.js +232 -0
  70. package/dist/server/lib/automationStageDetails.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
  72. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  73. package/dist/server/lib/buildOpenQuestions.js +44 -0
  74. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  75. package/dist/server/lib/buildSequences.js +130 -46
  76. package/dist/server/lib/buildSequences.js.map +1 -1
  77. package/dist/server/lib/bundleReviewOperations.js +91 -9
  78. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  79. package/dist/server/lib/chatOperations.js +67 -12
  80. package/dist/server/lib/chatOperations.js.map +1 -1
  81. package/dist/server/lib/executionReconciliation.js +518 -0
  82. package/dist/server/lib/executionReconciliation.js.map +1 -0
  83. package/dist/server/lib/executionRetry.js +694 -0
  84. package/dist/server/lib/executionRetry.js.map +1 -0
  85. package/dist/server/lib/executionRetryContext.js +489 -0
  86. package/dist/server/lib/executionRetryContext.js.map +1 -0
  87. package/dist/server/lib/executionRetryPredicates.js +74 -0
  88. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  89. package/dist/server/lib/executionRunRetention.js +141 -0
  90. package/dist/server/lib/executionRunRetention.js.map +1 -0
  91. package/dist/server/lib/executionRuns.js +1367 -0
  92. package/dist/server/lib/executionRuns.js.map +1 -0
  93. package/dist/server/lib/executionTimeline.js +45 -0
  94. package/dist/server/lib/executionTimeline.js.map +1 -0
  95. package/dist/server/lib/git.js +385 -6
  96. package/dist/server/lib/git.js.map +1 -1
  97. package/dist/server/lib/integrations/github.js +43 -0
  98. package/dist/server/lib/integrations/github.js.map +1 -1
  99. package/dist/server/lib/integrations/githubActions.js +1 -0
  100. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  101. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  102. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  103. package/dist/server/lib/projectMemorySelections.js +7 -3
  104. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  105. package/dist/server/lib/shutdown.js +2 -2
  106. package/dist/server/lib/shutdown.js.map +1 -1
  107. package/dist/server/lib/specArtifact.js +3 -0
  108. package/dist/server/lib/specArtifact.js.map +1 -1
  109. package/dist/server/lib/specFormatting.js +1 -1
  110. package/dist/server/lib/specFormatting.js.map +1 -1
  111. package/dist/server/lib/specOperations.js +78 -12
  112. package/dist/server/lib/specOperations.js.map +1 -1
  113. package/dist/server/lib/specReviewContext.js +54 -18
  114. package/dist/server/lib/specReviewContext.js.map +1 -1
  115. package/dist/server/lib/specReviewDraft.js +3 -2
  116. package/dist/server/lib/specReviewDraft.js.map +1 -1
  117. package/dist/server/lib/specReviewOperations.js +74 -9
  118. package/dist/server/lib/specReviewOperations.js.map +1 -1
  119. package/dist/server/lib/specReviewValidation.js +217 -0
  120. package/dist/server/lib/specReviewValidation.js.map +1 -0
  121. package/dist/server/lib/specReviews.js +30 -2
  122. package/dist/server/lib/specReviews.js.map +1 -1
  123. package/dist/server/lib/specs.js +18 -1
  124. package/dist/server/lib/specs.js.map +1 -1
  125. package/dist/server/lib/stageSnapshots.js +197 -0
  126. package/dist/server/lib/stageSnapshots.js.map +1 -0
  127. package/dist/server/lib/testSessions.js +160 -18
  128. package/dist/server/lib/testSessions.js.map +1 -1
  129. package/dist/server/lib/ticketAutomation.js +80 -4
  130. package/dist/server/lib/ticketAutomation.js.map +1 -1
  131. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  132. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  133. package/dist/server/lib/ticketFollowUpOperations.js +270 -29
  134. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  135. package/dist/server/lib/ticketMessages.js +16 -0
  136. package/dist/server/lib/ticketMessages.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +51 -6
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/ticketUndo.js +77 -168
  140. package/dist/server/lib/ticketUndo.js.map +1 -1
  141. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  142. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  143. package/dist/server/lib/tickets.js +41 -27
  144. package/dist/server/lib/tickets.js.map +1 -1
  145. package/dist/server/lib/transcriptRuns.js +3 -3
  146. package/dist/server/lib/transcriptRuns.js.map +1 -1
  147. package/dist/server/lib/turnExecutionErrors.js +12 -0
  148. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  149. package/dist/server/lib/types.js +58 -1
  150. package/dist/server/lib/types.js.map +1 -1
  151. package/dist/server/workers/build.js +596 -184
  152. package/dist/server/workers/build.js.map +1 -1
  153. package/dist/server/workers/chat.js +269 -132
  154. package/dist/server/workers/chat.js.map +1 -1
  155. package/dist/server/workers/followUp.js +731 -203
  156. package/dist/server/workers/followUp.js.map +1 -1
  157. package/dist/server/workers/plan.js +546 -188
  158. package/dist/server/workers/plan.js.map +1 -1
  159. package/dist/server/workers/planFollowUp.js +240 -75
  160. package/dist/server/workers/planFollowUp.js.map +1 -1
  161. package/dist/server/workers/review.js +790 -297
  162. package/dist/server/workers/review.js.map +1 -1
  163. package/dist/server/workers/specReview.js +212 -46
  164. package/dist/server/workers/specReview.js.map +1 -1
  165. package/dist/server/workers/specSession.js +434 -254
  166. package/dist/server/workers/specSession.js.map +1 -1
  167. package/package.json +1 -1
  168. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  169. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  170. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  171. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  172. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  173. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  174. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  175. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  176. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
@@ -11,7 +11,7 @@ import { appendClaudeHeadlessPlanPrompt, buildClaudeHeadlessPlanArgs } from './p
11
11
  import { formatGenericToolCall } from './genericToolCall.js';
12
12
  import { getSpecSession, updateSpecSessionResumeId, updateSpecSessionStatus, updateSpecSessionTitle, updateSpecSessionUsageSnapshot } from './specs.js';
13
13
  import { updateSpecReviewRunStatus, updateSpecReviewRunStreaming, updateSpecReviewRunUsage } from './specReviews.js';
14
- import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
14
+ import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForExecutionRun, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
15
15
  import { resolveOpencodeDbPath } from './runtimePaths.js';
16
16
  import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
17
17
  import { getTicket } from './tickets.js';
@@ -1320,12 +1320,19 @@ const appendOpencodeTranscriptLine = ({ parsed, logLines, responseLines, recover
1320
1320
  }
1321
1321
  return shouldResetResponse;
1322
1322
  };
1323
- export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, }) => {
1323
+ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, executionContext, deferMainFinalization = false, }) => {
1324
1324
  const trackedRuns = [];
1325
- let nextRunIndex = Math.max(getNextAgentRunIndex(parentType, parentId), mainRun ? mainRun.runIndex + 1 : 1);
1325
+ let nextRunIndex = Math.max(executionContext ? getNextAgentRunIndex(executionContext.runId) : getNextAgentRunIndex(parentType, parentId), mainRun ? mainRun.runIndex + 1 : 1);
1326
1326
  const emitUpdate = () => {
1327
1327
  emitAgentRunsUpdated(io, parentType, parentId);
1328
1328
  };
1329
+ const updateMain = (log, response) => {
1330
+ if (!mainRun) {
1331
+ return;
1332
+ }
1333
+ updateAgentRunStatus(mainRun.id, 'running', log, response);
1334
+ emitUpdate();
1335
+ };
1329
1336
  const findRun = (callId, sessionId) => {
1330
1337
  if (callId) {
1331
1338
  const callRun = trackedRuns.find(run => run.callId === callId);
@@ -1368,11 +1375,12 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1368
1375
  emitUpdate();
1369
1376
  continue;
1370
1377
  }
1371
- const runIndex = Math.max(getNextAgentRunIndex(parentType, parentId), nextRunIndex);
1378
+ const runIndex = Math.max(executionContext ? getNextAgentRunIndex(executionContext.runId) : getNextAgentRunIndex(parentType, parentId), nextRunIndex);
1372
1379
  const id = createAgentRun({
1380
+ ...(executionContext ? { executionRunId: executionContext.runId } : {}),
1373
1381
  parentType,
1374
1382
  parentId,
1375
- runIndex,
1383
+ ...(!executionContext ? { runIndex } : {}),
1376
1384
  label: event.label?.trim() || 'Subagent',
1377
1385
  role: 'subagent',
1378
1386
  });
@@ -1468,18 +1476,26 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1468
1476
  run.status = status;
1469
1477
  finalizeAgentRun(run.id, status, run.response || null);
1470
1478
  }
1471
- emitUpdate();
1479
+ if (mainRun) {
1480
+ updateMain(parentLog, parentResponse);
1481
+ if (!deferMainFinalization) {
1482
+ finalizeAgentRun(mainRun.id, status, parentResponse);
1483
+ }
1484
+ }
1485
+ if (!mainRun) {
1486
+ emitUpdate();
1487
+ }
1472
1488
  const trackedIds = new Set(trackedRuns.map(run => run.id));
1473
- const persistedRuns = listAgentRunsForParent(parentType, parentId)
1489
+ const persistedRuns = (executionContext
1490
+ ? listAgentRunsForExecutionRun(executionContext.runId)
1491
+ : listAgentRunsForParent(parentType, parentId))
1474
1492
  .filter(run => trackedIds.has(run.id) || run.id === mainRun?.id);
1475
- const persistedIds = new Set(persistedRuns.map(run => run.id));
1476
1493
  const timestamp = new Date().toISOString();
1477
1494
  for (const run of trackedRuns) {
1478
- if (persistedIds.has(run.id)) {
1479
- continue;
1480
- }
1481
- persistedRuns.push({
1495
+ const persistedRun = persistedRuns.find(candidate => candidate.id === run.id);
1496
+ const snapshotRun = {
1482
1497
  id: run.id,
1498
+ execution_run_id: persistedRun?.execution_run_id ?? executionContext?.runId ?? null,
1483
1499
  parent_type: parentType,
1484
1500
  parent_id: parentId,
1485
1501
  run_index: run.runIndex,
@@ -1489,15 +1505,25 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1489
1505
  agent_log: run.log || null,
1490
1506
  streaming_response: null,
1491
1507
  response_text: run.response || null,
1508
+ failure_message: persistedRun?.failure_message ?? null,
1492
1509
  session_id: run.sessionId,
1493
- created_at: run.createdAt,
1494
- updated_at: timestamp,
1495
- });
1510
+ completed_at: persistedRun?.completed_at ?? timestamp,
1511
+ created_at: persistedRun?.created_at ?? run.createdAt,
1512
+ updated_at: persistedRun?.updated_at ?? timestamp,
1513
+ };
1514
+ const persistedRunIndex = persistedRuns.findIndex(candidate => candidate.id === run.id);
1515
+ if (persistedRunIndex >= 0) {
1516
+ persistedRuns[persistedRunIndex] = snapshotRun;
1517
+ }
1518
+ else {
1519
+ persistedRuns.push(snapshotRun);
1520
+ }
1496
1521
  }
1497
1522
  if (mainRun) {
1498
1523
  const persistedMainRun = persistedRuns.find(run => run.id === mainRun.id);
1499
1524
  const mainSnapshotRun = {
1500
1525
  id: mainRun.id,
1526
+ execution_run_id: persistedMainRun?.execution_run_id ?? null,
1501
1527
  parent_type: parentType,
1502
1528
  parent_id: parentId,
1503
1529
  run_index: mainRun.runIndex,
@@ -1507,7 +1533,9 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1507
1533
  agent_log: parentLog,
1508
1534
  streaming_response: null,
1509
1535
  response_text: parentResponse,
1536
+ failure_message: persistedMainRun?.failure_message ?? null,
1510
1537
  session_id: persistedMainRun?.session_id ?? null,
1538
+ completed_at: persistedMainRun?.completed_at ?? timestamp,
1511
1539
  created_at: persistedMainRun?.created_at ?? timestamp,
1512
1540
  updated_at: persistedMainRun?.updated_at ?? timestamp,
1513
1541
  };
@@ -1522,6 +1550,7 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1522
1550
  else {
1523
1551
  persistedRuns.push({
1524
1552
  id: `${parentType}:${parentId}:main`,
1553
+ execution_run_id: null,
1525
1554
  parent_type: parentType,
1526
1555
  parent_id: parentId,
1527
1556
  run_index: 0,
@@ -1531,14 +1560,22 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1531
1560
  agent_log: parentLog,
1532
1561
  streaming_response: null,
1533
1562
  response_text: parentResponse,
1563
+ failure_message: null,
1534
1564
  session_id: null,
1565
+ completed_at: timestamp,
1535
1566
  created_at: timestamp,
1536
1567
  updated_at: timestamp,
1537
1568
  });
1538
1569
  }
1539
1570
  return createTranscriptRunSnapshots(persistedRuns.sort((left, right) => left.run_index - right.run_index));
1540
1571
  };
1541
- return { applySnapshot, handle, finalize };
1572
+ const setMainSessionId = (sessionId) => {
1573
+ const normalized = sessionId?.trim();
1574
+ if (mainRun && normalized) {
1575
+ updateAgentRunSessionId(mainRun.id, normalized);
1576
+ }
1577
+ };
1578
+ return { applySnapshot, handle, finalize, setMainSessionId, updateMain };
1542
1579
  };
1543
1580
  export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1544
1581
  let log = '';
@@ -1729,6 +1766,22 @@ const untrackChatProcess = (chatSessionId, state) => {
1729
1766
  state.metadataRefresher?.stop();
1730
1767
  state.metadataRefresher = null;
1731
1768
  };
1769
+ export const registerChatProcessPlaceholder = (chatSessionId) => {
1770
+ const state = {
1771
+ child: null,
1772
+ researchChildren: new Set(),
1773
+ stopRequested: false,
1774
+ metadataRefresher: null,
1775
+ };
1776
+ activeChatProcesses.set(chatSessionId, state);
1777
+ return state;
1778
+ };
1779
+ export const untrackChatProcessById = (chatSessionId) => {
1780
+ const state = activeChatProcesses.get(chatSessionId);
1781
+ if (state) {
1782
+ untrackChatProcess(chatSessionId, state);
1783
+ }
1784
+ };
1732
1785
  const untrackSpecProcess = (specSessionId, state) => {
1733
1786
  if (activeSpecProcesses.get(specSessionId) === state) {
1734
1787
  activeSpecProcesses.delete(specSessionId);
@@ -1744,6 +1797,7 @@ const untrackSpecProcess = (specSessionId, state) => {
1744
1797
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1745
1798
  const state = {
1746
1799
  child: null,
1800
+ researchChildren: new Set(),
1747
1801
  stopRequested: false,
1748
1802
  metadataRefresher: null,
1749
1803
  };
@@ -1820,11 +1874,14 @@ export const stopReviewAgent = async (ticketId) => {
1820
1874
  };
1821
1875
  export const stopChatAgent = async (chatSessionId) => {
1822
1876
  const state = activeChatProcesses.get(chatSessionId);
1823
- if (!state?.child) {
1877
+ if (!state) {
1824
1878
  return false;
1825
1879
  }
1826
1880
  state.stopRequested = true;
1827
1881
  state.metadataRefresher?.stop();
1882
+ if (!state.child) {
1883
+ return true;
1884
+ }
1828
1885
  killProcess(state.child, 'SIGTERM');
1829
1886
  await waitForProcessExit(state.child, 5000);
1830
1887
  if (isProcessAlive(state.child)) {
@@ -1840,20 +1897,22 @@ export const stopSpecAgent = async (specSessionId) => {
1840
1897
  }
1841
1898
  state.stopRequested = true;
1842
1899
  state.metadataRefresher?.stop();
1843
- // No child yet means we are in the pre-response window (the worker is still
1844
- // running prepareLinkedPlanningResearch). There is nothing to kill, but the
1845
- // stop request still succeeds: the worker honors stopRequested and finalizes
1846
- // the session to 'stopped' once its pre-step resolves. This mirrors how Chat
1847
- // treats an interrupt as accepted even before the agent child is live.
1848
- if (!state.child) {
1900
+ const children = [...state.researchChildren];
1901
+ if (state.child) {
1902
+ children.push(state.child);
1903
+ }
1904
+ if (children.length === 0) {
1849
1905
  return true;
1850
1906
  }
1851
- killProcess(state.child, 'SIGTERM');
1852
- await waitForProcessExit(state.child, 5000);
1853
- if (isProcessAlive(state.child)) {
1854
- killProcess(state.child, 'SIGKILL');
1855
- await waitForProcessExit(state.child, 1000);
1907
+ for (const child of children) {
1908
+ killProcess(child, 'SIGTERM');
1909
+ }
1910
+ await Promise.all(children.map(child => waitForProcessExit(child, 5000)));
1911
+ const aliveChildren = children.filter(isProcessAlive);
1912
+ for (const child of aliveChildren) {
1913
+ killProcess(child, 'SIGKILL');
1856
1914
  }
1915
+ await Promise.all(aliveChildren.map(child => waitForProcessExit(child, 1000)));
1857
1916
  return true;
1858
1917
  };
1859
1918
  export const stopSpecReviewAgent = async (specReviewRunId) => {
@@ -1871,7 +1930,7 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1871
1930
  }
1872
1931
  return true;
1873
1932
  };
1874
- export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, }) => {
1933
+ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, }) => {
1875
1934
  const resolvedModel = resolveAgentModel(tool, model);
1876
1935
  const subagentTracker = createSubagentRunTracker({
1877
1936
  parentType: 'ticket',
@@ -1887,6 +1946,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1887
1946
  },
1888
1947
  }
1889
1948
  : {}),
1949
+ executionContext,
1890
1950
  });
1891
1951
  const writeRunStatus = (status, log, streamingResponse = null, options = {}) => {
1892
1952
  if (runStream) {
@@ -1900,6 +1960,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1900
1960
  if (runStream) {
1901
1961
  updateAgentRunSessionId(runStream.runId, sessionId);
1902
1962
  emitAgentRunsUpdated(io, 'ticket', ticketId);
1963
+ if (persistSessionIdToTicket) {
1964
+ updateSessionId(ticketId, sessionId, sessionField, io);
1965
+ }
1903
1966
  return;
1904
1967
  }
1905
1968
  updateSessionId(ticketId, sessionId, sessionField, io);
@@ -1978,12 +2041,21 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1978
2041
  if (tool === 'opencode') {
1979
2042
  spawnTimestamp = Date.now();
1980
2043
  }
1981
- const child = spawn(cmd, args, {
1982
- cwd,
1983
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
1984
- env: { ...process.env, PWD: cwd },
1985
- detached: process.platform !== 'win32',
1986
- });
2044
+ let child;
2045
+ try {
2046
+ child = spawn(cmd, args, {
2047
+ cwd,
2048
+ stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2049
+ env: { ...process.env, PWD: cwd },
2050
+ detached: process.platform !== 'win32',
2051
+ });
2052
+ }
2053
+ catch (error) {
2054
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
2055
+ const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2056
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2057
+ return;
2058
+ }
1987
2059
  reviewChild = child;
1988
2060
  if (tool === 'claude') {
1989
2061
  const claudePrompt = opencodeAgentMode === 'plan'
@@ -2195,12 +2267,15 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2195
2267
  });
2196
2268
  });
2197
2269
  };
2198
- export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', }) => {
2270
+ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, }) => {
2199
2271
  const resolvedModel = resolveAgentModel(tool, model);
2200
2272
  const subagentTracker = createSubagentRunTracker({
2201
2273
  parentType: 'chat',
2202
2274
  parentId: chatSessionId,
2203
2275
  io,
2276
+ executionContext,
2277
+ mainRun,
2278
+ deferMainFinalization: true,
2204
2279
  });
2205
2280
  return new Promise((resolve, reject) => {
2206
2281
  let sessionId = null;
@@ -2211,11 +2286,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2211
2286
  let startTime = null;
2212
2287
  let firstFinalAnswerTime = null;
2213
2288
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2214
- const chatState = {
2215
- child: null,
2216
- stopRequested: false,
2217
- metadataRefresher: null,
2218
- };
2289
+ const chatState = activeChatProcesses.get(chatSessionId) ?? registerChatProcessPlaceholder(chatSessionId);
2219
2290
  const settle = (handler) => (value) => {
2220
2291
  if (settled) {
2221
2292
  return;
@@ -2228,6 +2299,16 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2228
2299
  };
2229
2300
  const resolveOnce = settle(resolve);
2230
2301
  const rejectOnce = settle(reject);
2302
+ if (chatState.stopRequested) {
2303
+ const transcriptRuns = subagentTracker.finalize('stopped', null, null);
2304
+ resolveOnce(withTranscriptRuns({
2305
+ completionStatus: 'stopped',
2306
+ finalResponse: null,
2307
+ usageSnapshot,
2308
+ responseTiming: null,
2309
+ }, transcriptRuns));
2310
+ return;
2311
+ }
2231
2312
  const [cmd, args] = tool === 'claude'
2232
2313
  ? (() => {
2233
2314
  sessionId = resumeSessionId || randomUUID();
@@ -2238,8 +2319,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2238
2319
  })()
2239
2320
  : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
2240
2321
  updateChatSessionStatus(chatSessionId, 'running', null, io);
2322
+ subagentTracker.updateMain(null, null);
2241
2323
  if (tool === 'claude' && sessionId) {
2242
2324
  updateChatSessionId(chatSessionId, tool, sessionId, io);
2325
+ subagentTracker.setMainSessionId(sessionId);
2243
2326
  }
2244
2327
  if (tool === 'opencode') {
2245
2328
  spawnTimestamp = Date.now();
@@ -2257,12 +2340,21 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2257
2340
  }
2258
2341
  }
2259
2342
  }
2260
- const child = spawn(cmd, args, {
2261
- cwd,
2262
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2263
- env: { ...process.env, PWD: cwd },
2264
- detached: process.platform !== 'win32',
2265
- });
2343
+ let child;
2344
+ try {
2345
+ child = spawn(cmd, args, {
2346
+ cwd,
2347
+ stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2348
+ env: { ...process.env, PWD: cwd },
2349
+ detached: process.platform !== 'win32',
2350
+ });
2351
+ }
2352
+ catch (error) {
2353
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
2354
+ const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2355
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2356
+ return;
2357
+ }
2266
2358
  chatState.child = child;
2267
2359
  activeChatProcesses.set(chatSessionId, chatState);
2268
2360
  if (tool === 'claude') {
@@ -2281,6 +2373,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2281
2373
  discoverParentSession: false,
2282
2374
  onParentSessionId: discoveredSessionId => {
2283
2375
  updateChatSessionId(chatSessionId, 'opencode', discoveredSessionId, io);
2376
+ subagentTracker.setMainSessionId(discoveredSessionId);
2284
2377
  },
2285
2378
  });
2286
2379
  opencodeSubagentMonitor.start();
@@ -2291,7 +2384,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2291
2384
  let remainder = '';
2292
2385
  const stderrChunks = [];
2293
2386
  const progressUpdater = createThrottledProgressUpdater(() => {
2294
- updateChatSessionStatus(chatSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
2387
+ const log = logLines.join('\n\n');
2388
+ const response = responseLines.join('\n\n');
2389
+ updateChatSessionStatus(chatSessionId, 'running', log, io, response);
2390
+ subagentTracker.updateMain(log, response);
2295
2391
  });
2296
2392
  child.stdout?.on('data', (chunk) => {
2297
2393
  const text = remainder + chunk.toString();
@@ -2398,7 +2494,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2398
2494
  let remainder = '';
2399
2495
  const stderrChunks = [];
2400
2496
  const progressUpdater = createThrottledProgressUpdater(() => {
2401
- updateChatSessionStatus(chatSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
2497
+ const log = logLines.join('\n\n');
2498
+ const response = responseLines.join('\n\n');
2499
+ updateChatSessionStatus(chatSessionId, 'running', log, io, response);
2500
+ subagentTracker.updateMain(log, response);
2402
2501
  });
2403
2502
  child.stdout?.on('data', (chunk) => {
2404
2503
  const text = remainder + chunk.toString();
@@ -2540,12 +2639,15 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2540
2639
  });
2541
2640
  });
2542
2641
  };
2543
- export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, }) => {
2642
+ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, executionContext, mainRun, }) => {
2544
2643
  const resolvedModel = resolveAgentModel(tool, model);
2545
2644
  const subagentTracker = createSubagentRunTracker({
2546
2645
  parentType: 'spec',
2547
2646
  parentId: specSessionId,
2548
2647
  io,
2648
+ executionContext,
2649
+ mainRun,
2650
+ deferMainFinalization: true,
2549
2651
  });
2550
2652
  return new Promise((resolve, reject) => {
2551
2653
  let sessionId = null;
@@ -2556,11 +2658,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2556
2658
  let startTime = null;
2557
2659
  let firstFinalAnswerTime = null;
2558
2660
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2559
- const specState = {
2560
- child: null,
2561
- stopRequested: false,
2562
- metadataRefresher: null,
2563
- };
2661
+ const specState = activeSpecProcesses.get(specSessionId) ?? registerSpecProcessPlaceholder(specSessionId);
2564
2662
  const settle = (handler) => (value) => {
2565
2663
  if (settled) {
2566
2664
  return;
@@ -2573,6 +2671,16 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2573
2671
  };
2574
2672
  const resolveOnce = settle(resolve);
2575
2673
  const rejectOnce = settle(reject);
2674
+ if (specState.stopRequested) {
2675
+ const transcriptRuns = subagentTracker.finalize('stopped', null, null);
2676
+ resolveOnce(withTranscriptRuns({
2677
+ completionStatus: 'stopped',
2678
+ finalResponse: null,
2679
+ usageSnapshot,
2680
+ responseTiming: null,
2681
+ }, transcriptRuns));
2682
+ return;
2683
+ }
2576
2684
  const [cmd, args] = tool === 'claude'
2577
2685
  ? (() => {
2578
2686
  sessionId = resumeSessionId || randomUUID();
@@ -2583,8 +2691,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2583
2691
  })()
2584
2692
  : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
2585
2693
  updateSpecSessionStatusLocal(specSessionId, 'running', null, io);
2694
+ subagentTracker.updateMain(null, null);
2586
2695
  if (tool === 'claude' && sessionId) {
2587
2696
  updateSpecSessionIdLocal(specSessionId, tool, sessionId, io);
2697
+ subagentTracker.setMainSessionId(sessionId);
2588
2698
  }
2589
2699
  if (tool === 'opencode') {
2590
2700
  spawnTimestamp = Date.now();
@@ -2602,12 +2712,21 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2602
2712
  }
2603
2713
  }
2604
2714
  }
2605
- const child = spawn(cmd, args, {
2606
- cwd,
2607
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2608
- env: { ...process.env, PWD: cwd },
2609
- detached: process.platform !== 'win32',
2610
- });
2715
+ let child;
2716
+ try {
2717
+ child = spawn(cmd, args, {
2718
+ cwd,
2719
+ stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2720
+ env: { ...process.env, PWD: cwd },
2721
+ detached: process.platform !== 'win32',
2722
+ });
2723
+ }
2724
+ catch (error) {
2725
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
2726
+ const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2727
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2728
+ return;
2729
+ }
2611
2730
  specState.child = child;
2612
2731
  activeSpecProcesses.set(specSessionId, specState);
2613
2732
  if (tool === 'claude') {
@@ -2623,6 +2742,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2623
2742
  discoverParentSession: false,
2624
2743
  onParentSessionId: discoveredSessionId => {
2625
2744
  updateSpecSessionIdLocal(specSessionId, 'opencode', discoveredSessionId, io);
2745
+ subagentTracker.setMainSessionId(discoveredSessionId);
2626
2746
  },
2627
2747
  });
2628
2748
  opencodeSubagentMonitor.start();
@@ -2633,7 +2753,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2633
2753
  let remainder = '';
2634
2754
  const stderrChunks = [];
2635
2755
  const progressUpdater = createThrottledProgressUpdater(() => {
2636
- updateSpecSessionStatusLocal(specSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
2756
+ const log = logLines.join('\n\n');
2757
+ const response = responseLines.join('\n\n');
2758
+ updateSpecSessionStatusLocal(specSessionId, 'running', log, io, response);
2759
+ subagentTracker.updateMain(log, response);
2637
2760
  });
2638
2761
  child.stdout?.on('data', (chunk) => {
2639
2762
  const text = remainder + chunk.toString();
@@ -2740,7 +2863,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2740
2863
  let remainder = '';
2741
2864
  const stderrChunks = [];
2742
2865
  const progressUpdater = createThrottledProgressUpdater(() => {
2743
- updateSpecSessionStatusLocal(specSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
2866
+ const log = logLines.join('\n\n');
2867
+ const response = responseLines.join('\n\n');
2868
+ updateSpecSessionStatusLocal(specSessionId, 'running', log, io, response);
2869
+ subagentTracker.updateMain(log, response);
2744
2870
  });
2745
2871
  child.stdout?.on('data', (chunk) => {
2746
2872
  const text = remainder + chunk.toString();
@@ -2893,15 +3019,24 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2893
3019
  * updateSpecReviewRunStatus / updateSpecReviewRunStreaming / updateSpecReviewRunUsage —
2894
3020
  * the spec_sessions row is never touched by this path.
2895
3021
  */
2896
- export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [], cwd, io, model, variant = null, }) => {
3022
+ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, executionContext, }) => {
2897
3023
  const resolvedModel = resolveAgentModel(tool, model);
3024
+ const subagentTracker = createSubagentRunTracker({
3025
+ parentType: 'spec',
3026
+ parentId: specSessionId,
3027
+ io,
3028
+ executionContext,
3029
+ });
2898
3030
  return new Promise((resolve, reject) => {
3031
+ let spawnTimestamp = null;
3032
+ let opencodeSubagentMonitor = null;
2899
3033
  let settled = false;
2900
3034
  let startTime = null;
2901
3035
  let firstFinalAnswerTime = null;
2902
3036
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2903
3037
  const reviewState = {
2904
3038
  child: null,
3039
+ researchChildren: new Set(),
2905
3040
  stopRequested: false,
2906
3041
  metadataRefresher: null,
2907
3042
  };
@@ -2910,14 +3045,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2910
3045
  return;
2911
3046
  }
2912
3047
  settled = true;
3048
+ opencodeSubagentMonitor?.stop();
2913
3049
  untrackSpecReviewProcess(specReviewRunId, reviewState);
2914
3050
  handler(value);
2915
3051
  };
2916
3052
  const resolveOnce = settle(resolve);
2917
3053
  const rejectOnce = settle(reject);
2918
- const emitReviewUpdated = () => {
2919
- io.emit('spec-review:updated', { spec_review_run_id: specReviewRunId });
2920
- };
2921
3054
  const [cmd, args] = tool === 'claude'
2922
3055
  ? [
2923
3056
  'claude',
@@ -2925,7 +3058,6 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2925
3058
  ]
2926
3059
  : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
2927
3060
  updateSpecReviewRunStreaming(specReviewRunId, null);
2928
- emitReviewUpdated();
2929
3061
  const child = spawn(cmd, args, {
2930
3062
  cwd,
2931
3063
  stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
@@ -2938,6 +3070,16 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2938
3070
  writeClaudeStdin(child, buildClaudeStdinMessage(appendClaudeHeadlessPlanPrompt(prompt), files, cwd));
2939
3071
  }
2940
3072
  startTime = Date.now();
3073
+ if (tool === 'opencode') {
3074
+ spawnTimestamp = startTime;
3075
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
3076
+ initialParentSessionId: null,
3077
+ spawnTimestamp,
3078
+ cwd,
3079
+ tracker: subagentTracker,
3080
+ });
3081
+ opencodeSubagentMonitor.start();
3082
+ }
2941
3083
  if (tool === 'claude') {
2942
3084
  const logLines = [];
2943
3085
  const responseLines = [];
@@ -2952,7 +3094,8 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2952
3094
  remainder = lines.pop() ?? '';
2953
3095
  for (const line of lines) {
2954
3096
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2955
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line);
3097
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line);
3098
+ subagentTracker.handle(subagentEvents);
2956
3099
  if (logFragment !== null) {
2957
3100
  logLines.push(logFragment);
2958
3101
  }
@@ -2969,7 +3112,8 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2969
3112
  progressUpdater.cancel();
2970
3113
  if (remainder.trim()) {
2971
3114
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2972
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder);
3115
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
3116
+ subagentTracker.handle(subagentEvents);
2973
3117
  if (logFragment !== null) {
2974
3118
  logLines.push(logFragment);
2975
3119
  }
@@ -2982,6 +3126,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2982
3126
  const finalResponse = responseLines.join('\n\n') || errOutput;
2983
3127
  if (reviewState.stopRequested) {
2984
3128
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3129
+ subagentTracker.finalize('stopped', finalLog || null, finalResponse || null);
2985
3130
  resolveOnce({
2986
3131
  completionStatus: 'stopped',
2987
3132
  finalResponse,
@@ -2993,6 +3138,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2993
3138
  const modelError = isModelResolutionError(finalLog);
2994
3139
  if (code === 0 && !modelError) {
2995
3140
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3141
+ subagentTracker.finalize('done', finalLog || null, finalResponse || null);
2996
3142
  resolveOnce({
2997
3143
  completionStatus: 'done',
2998
3144
  finalResponse,
@@ -3001,10 +3147,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3001
3147
  });
3002
3148
  return;
3003
3149
  }
3150
+ subagentTracker.finalize('error', finalLog || null, finalResponse || null);
3004
3151
  rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
3005
3152
  });
3006
3153
  child.on('error', err => {
3007
3154
  progressUpdater.cancel();
3155
+ subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3008
3156
  rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3009
3157
  });
3010
3158
  return;
@@ -3039,7 +3187,9 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3039
3187
  }
3040
3188
  }
3041
3189
  const parsed = parseOpencodeJsonLine(line, cwd);
3042
- const { logFragment, responseFragment } = parsed;
3190
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
3191
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3192
+ subagentTracker.handle(subagentEvents);
3043
3193
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3044
3194
  firstFinalAnswerTime = null;
3045
3195
  }
@@ -3051,9 +3201,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3051
3201
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
3052
3202
  child.on('close', code => {
3053
3203
  progressUpdater.cancel();
3204
+ opencodeSubagentMonitor?.refresh();
3054
3205
  if (remainder.trim()) {
3055
3206
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3056
3207
  const parsed = parseOpencodeJsonLine(remainder, cwd);
3208
+ opencodeSubagentMonitor?.setParentSessionId(parsed.providerSessionId);
3209
+ subagentTracker.handle(parsed.subagentEvents);
3057
3210
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3058
3211
  firstFinalAnswerTime = null;
3059
3212
  }
@@ -3066,6 +3219,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3066
3219
  : responseLines.join('\n\n') || stderrOutput;
3067
3220
  if (reviewState.stopRequested) {
3068
3221
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3222
+ subagentTracker.finalize('stopped', log || null, finalResponse || null);
3069
3223
  resolveOnce({
3070
3224
  completionStatus: 'stopped',
3071
3225
  finalResponse,
@@ -3077,6 +3231,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3077
3231
  const modelError = isModelResolutionError(log);
3078
3232
  if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
3079
3233
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3234
+ subagentTracker.finalize('done', log || null, finalResponse || null);
3080
3235
  resolveOnce({
3081
3236
  completionStatus: 'done',
3082
3237
  finalResponse,
@@ -3085,10 +3240,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3085
3240
  });
3086
3241
  return;
3087
3242
  }
3243
+ subagentTracker.finalize('error', log || null, finalResponse || null);
3088
3244
  rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
3089
3245
  });
3090
3246
  child.on('error', err => {
3091
3247
  progressUpdater.cancel();
3248
+ subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3092
3249
  rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3093
3250
  });
3094
3251
  });