@straiffi/archon 1.7.1 → 1.7.3

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 (166) hide show
  1. package/dist/client/assets/ActivityView-DTEenQas.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
  3. package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
  33. package/dist/client/assets/index-CEMaJCPS.js +273 -0
  34. package/dist/client/assets/index-lWftq65U.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +471 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1267 -213
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +281 -76
  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/automationProgression.js +464 -0
  64. package/dist/server/lib/automationProgression.js.map +1 -0
  65. package/dist/server/lib/automationReconciliation.js +498 -0
  66. package/dist/server/lib/automationReconciliation.js.map +1 -0
  67. package/dist/server/lib/automationStageDetails.js +232 -0
  68. package/dist/server/lib/automationStageDetails.js.map +1 -0
  69. package/dist/server/lib/buildOpenQuestionOperations.js +223 -0
  70. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestions.js +52 -0
  72. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  73. package/dist/server/lib/buildSequences.js +130 -46
  74. package/dist/server/lib/buildSequences.js.map +1 -1
  75. package/dist/server/lib/bundleReviewOperations.js +82 -9
  76. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  77. package/dist/server/lib/chatOperations.js +31 -0
  78. package/dist/server/lib/chatOperations.js.map +1 -1
  79. package/dist/server/lib/executionReconciliation.js +518 -0
  80. package/dist/server/lib/executionReconciliation.js.map +1 -0
  81. package/dist/server/lib/executionRunRetention.js +141 -0
  82. package/dist/server/lib/executionRunRetention.js.map +1 -0
  83. package/dist/server/lib/executionRuns.js +1264 -0
  84. package/dist/server/lib/executionRuns.js.map +1 -0
  85. package/dist/server/lib/git.js +385 -6
  86. package/dist/server/lib/git.js.map +1 -1
  87. package/dist/server/lib/integrations/github.js +43 -0
  88. package/dist/server/lib/integrations/github.js.map +1 -1
  89. package/dist/server/lib/integrations/githubActions.js +1 -0
  90. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  91. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  92. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  93. package/dist/server/lib/projectMemorySelections.js +7 -3
  94. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  95. package/dist/server/lib/shutdown.js +2 -2
  96. package/dist/server/lib/shutdown.js.map +1 -1
  97. package/dist/server/lib/specArtifact.js +3 -0
  98. package/dist/server/lib/specArtifact.js.map +1 -1
  99. package/dist/server/lib/specFormatting.js +1 -1
  100. package/dist/server/lib/specFormatting.js.map +1 -1
  101. package/dist/server/lib/specOperations.js +31 -0
  102. package/dist/server/lib/specOperations.js.map +1 -1
  103. package/dist/server/lib/specReviewContext.js +44 -14
  104. package/dist/server/lib/specReviewContext.js.map +1 -1
  105. package/dist/server/lib/specReviewDraft.js +3 -2
  106. package/dist/server/lib/specReviewDraft.js.map +1 -1
  107. package/dist/server/lib/specReviewOperations.js +44 -7
  108. package/dist/server/lib/specReviewOperations.js.map +1 -1
  109. package/dist/server/lib/specReviewValidation.js +217 -0
  110. package/dist/server/lib/specReviewValidation.js.map +1 -0
  111. package/dist/server/lib/specReviews.js +30 -2
  112. package/dist/server/lib/specReviews.js.map +1 -1
  113. package/dist/server/lib/specs.js +18 -1
  114. package/dist/server/lib/specs.js.map +1 -1
  115. package/dist/server/lib/stageSnapshots.js +197 -0
  116. package/dist/server/lib/stageSnapshots.js.map +1 -0
  117. package/dist/server/lib/testSessions.js +164 -19
  118. package/dist/server/lib/testSessions.js.map +1 -1
  119. package/dist/server/lib/ticketAutomation.js +80 -4
  120. package/dist/server/lib/ticketAutomation.js.map +1 -1
  121. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  122. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  123. package/dist/server/lib/ticketFollowUpOperations.js +214 -20
  124. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  125. package/dist/server/lib/ticketOpenQuestions.js +30 -14
  126. package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
  127. package/dist/server/lib/ticketRunOperations.js +75 -7
  128. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  129. package/dist/server/lib/ticketUndo.js +77 -168
  130. package/dist/server/lib/ticketUndo.js.map +1 -1
  131. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  132. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  133. package/dist/server/lib/tickets.js +46 -29
  134. package/dist/server/lib/tickets.js.map +1 -1
  135. package/dist/server/lib/transcriptRuns.js +3 -3
  136. package/dist/server/lib/transcriptRuns.js.map +1 -1
  137. package/dist/server/lib/turnExecutionErrors.js +12 -0
  138. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  139. package/dist/server/lib/types.js +57 -1
  140. package/dist/server/lib/types.js.map +1 -1
  141. package/dist/server/workers/build.js +596 -184
  142. package/dist/server/workers/build.js.map +1 -1
  143. package/dist/server/workers/chat.js +261 -130
  144. package/dist/server/workers/chat.js.map +1 -1
  145. package/dist/server/workers/followUp.js +720 -195
  146. package/dist/server/workers/followUp.js.map +1 -1
  147. package/dist/server/workers/plan.js +527 -188
  148. package/dist/server/workers/plan.js.map +1 -1
  149. package/dist/server/workers/planFollowUp.js +231 -69
  150. package/dist/server/workers/planFollowUp.js.map +1 -1
  151. package/dist/server/workers/review.js +796 -239
  152. package/dist/server/workers/review.js.map +1 -1
  153. package/dist/server/workers/specReview.js +211 -45
  154. package/dist/server/workers/specReview.js.map +1 -1
  155. package/dist/server/workers/specSession.js +425 -252
  156. package/dist/server/workers/specSession.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
  159. package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
  160. package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
  161. package/dist/client/assets/channel-B4kCvLCt.js +0 -1
  162. package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
  163. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
  164. package/dist/client/assets/index-9q1GXkUw.js +0 -270
  165. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  166. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.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';
@@ -313,6 +313,7 @@ const hasMatchingUsageSnapshot = (current, next) => {
313
313
  && current.usage_snapshot_reasoning_tokens === next.usage_snapshot_reasoning_tokens;
314
314
  };
315
315
  const activeBuildProcesses = new Map();
316
+ const activeReviewProcesses = new Map();
316
317
  const activeChatProcesses = new Map();
317
318
  const activeSpecProcesses = new Map();
318
319
  const activeSpecReviewProcesses = new Map();
@@ -1319,12 +1320,19 @@ const appendOpencodeTranscriptLine = ({ parsed, logLines, responseLines, recover
1319
1320
  }
1320
1321
  return shouldResetResponse;
1321
1322
  };
1322
- export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, }) => {
1323
+ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, executionContext, deferMainFinalization = false, }) => {
1323
1324
  const trackedRuns = [];
1324
- 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);
1325
1326
  const emitUpdate = () => {
1326
1327
  emitAgentRunsUpdated(io, parentType, parentId);
1327
1328
  };
1329
+ const updateMain = (log, response) => {
1330
+ if (!mainRun) {
1331
+ return;
1332
+ }
1333
+ updateAgentRunStatus(mainRun.id, 'running', log, response);
1334
+ emitUpdate();
1335
+ };
1328
1336
  const findRun = (callId, sessionId) => {
1329
1337
  if (callId) {
1330
1338
  const callRun = trackedRuns.find(run => run.callId === callId);
@@ -1367,11 +1375,12 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1367
1375
  emitUpdate();
1368
1376
  continue;
1369
1377
  }
1370
- const runIndex = Math.max(getNextAgentRunIndex(parentType, parentId), nextRunIndex);
1378
+ const runIndex = Math.max(executionContext ? getNextAgentRunIndex(executionContext.runId) : getNextAgentRunIndex(parentType, parentId), nextRunIndex);
1371
1379
  const id = createAgentRun({
1380
+ ...(executionContext ? { executionRunId: executionContext.runId } : {}),
1372
1381
  parentType,
1373
1382
  parentId,
1374
- runIndex,
1383
+ ...(!executionContext ? { runIndex } : {}),
1375
1384
  label: event.label?.trim() || 'Subagent',
1376
1385
  role: 'subagent',
1377
1386
  });
@@ -1467,18 +1476,26 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1467
1476
  run.status = status;
1468
1477
  finalizeAgentRun(run.id, status, run.response || null);
1469
1478
  }
1470
- 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
+ }
1471
1488
  const trackedIds = new Set(trackedRuns.map(run => run.id));
1472
- const persistedRuns = listAgentRunsForParent(parentType, parentId)
1489
+ const persistedRuns = (executionContext
1490
+ ? listAgentRunsForExecutionRun(executionContext.runId)
1491
+ : listAgentRunsForParent(parentType, parentId))
1473
1492
  .filter(run => trackedIds.has(run.id) || run.id === mainRun?.id);
1474
- const persistedIds = new Set(persistedRuns.map(run => run.id));
1475
1493
  const timestamp = new Date().toISOString();
1476
1494
  for (const run of trackedRuns) {
1477
- if (persistedIds.has(run.id)) {
1478
- continue;
1479
- }
1480
- persistedRuns.push({
1495
+ const persistedRun = persistedRuns.find(candidate => candidate.id === run.id);
1496
+ const snapshotRun = {
1481
1497
  id: run.id,
1498
+ execution_run_id: persistedRun?.execution_run_id ?? executionContext?.runId ?? null,
1482
1499
  parent_type: parentType,
1483
1500
  parent_id: parentId,
1484
1501
  run_index: run.runIndex,
@@ -1488,15 +1505,25 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1488
1505
  agent_log: run.log || null,
1489
1506
  streaming_response: null,
1490
1507
  response_text: run.response || null,
1508
+ failure_message: persistedRun?.failure_message ?? null,
1491
1509
  session_id: run.sessionId,
1492
- created_at: run.createdAt,
1493
- updated_at: timestamp,
1494
- });
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
+ }
1495
1521
  }
1496
1522
  if (mainRun) {
1497
1523
  const persistedMainRun = persistedRuns.find(run => run.id === mainRun.id);
1498
1524
  const mainSnapshotRun = {
1499
1525
  id: mainRun.id,
1526
+ execution_run_id: persistedMainRun?.execution_run_id ?? null,
1500
1527
  parent_type: parentType,
1501
1528
  parent_id: parentId,
1502
1529
  run_index: mainRun.runIndex,
@@ -1506,7 +1533,9 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1506
1533
  agent_log: parentLog,
1507
1534
  streaming_response: null,
1508
1535
  response_text: parentResponse,
1536
+ failure_message: persistedMainRun?.failure_message ?? null,
1509
1537
  session_id: persistedMainRun?.session_id ?? null,
1538
+ completed_at: persistedMainRun?.completed_at ?? timestamp,
1510
1539
  created_at: persistedMainRun?.created_at ?? timestamp,
1511
1540
  updated_at: persistedMainRun?.updated_at ?? timestamp,
1512
1541
  };
@@ -1521,6 +1550,7 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1521
1550
  else {
1522
1551
  persistedRuns.push({
1523
1552
  id: `${parentType}:${parentId}:main`,
1553
+ execution_run_id: null,
1524
1554
  parent_type: parentType,
1525
1555
  parent_id: parentId,
1526
1556
  run_index: 0,
@@ -1530,14 +1560,22 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, })
1530
1560
  agent_log: parentLog,
1531
1561
  streaming_response: null,
1532
1562
  response_text: parentResponse,
1563
+ failure_message: null,
1533
1564
  session_id: null,
1565
+ completed_at: timestamp,
1534
1566
  created_at: timestamp,
1535
1567
  updated_at: timestamp,
1536
1568
  });
1537
1569
  }
1538
1570
  return createTranscriptRunSnapshots(persistedRuns.sort((left, right) => left.run_index - right.run_index));
1539
1571
  };
1540
- 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 };
1541
1579
  };
1542
1580
  export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1543
1581
  let log = '';
@@ -1728,6 +1766,22 @@ const untrackChatProcess = (chatSessionId, state) => {
1728
1766
  state.metadataRefresher?.stop();
1729
1767
  state.metadataRefresher = null;
1730
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
+ };
1731
1785
  const untrackSpecProcess = (specSessionId, state) => {
1732
1786
  if (activeSpecProcesses.get(specSessionId) === state) {
1733
1787
  activeSpecProcesses.delete(specSessionId);
@@ -1743,6 +1797,7 @@ const untrackSpecProcess = (specSessionId, state) => {
1743
1797
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1744
1798
  const state = {
1745
1799
  child: null,
1800
+ researchChildren: new Set(),
1746
1801
  stopRequested: false,
1747
1802
  metadataRefresher: null,
1748
1803
  };
@@ -1763,6 +1818,24 @@ const untrackSpecReviewProcess = (specReviewRunId, state) => {
1763
1818
  state.metadataRefresher = null;
1764
1819
  };
1765
1820
  export const isBuildAgentRunning = (ticketId) => activeBuildProcesses.has(ticketId);
1821
+ export const registerReviewProcess = (ticketId) => {
1822
+ const state = { children: new Set(), stopRequested: false };
1823
+ activeReviewProcesses.set(ticketId, state);
1824
+ return state;
1825
+ };
1826
+ export const untrackReviewProcess = (ticketId, state) => {
1827
+ if (activeReviewProcesses.get(ticketId) === state) {
1828
+ activeReviewProcesses.delete(ticketId);
1829
+ }
1830
+ };
1831
+ export const isReviewAgentRunning = (ticketId) => activeReviewProcesses.has(ticketId);
1832
+ export const isReviewStopRequested = (ticketId) => {
1833
+ return activeReviewProcesses.get(ticketId)?.stopRequested === true;
1834
+ };
1835
+ export const stopAllReviewAgents = async () => {
1836
+ const ticketIds = [...activeReviewProcesses.keys()];
1837
+ await Promise.all(ticketIds.map(stopReviewAgent));
1838
+ };
1766
1839
  export const stopAllBuildAgents = async () => {
1767
1840
  const ticketIds = [...activeBuildProcesses.keys()];
1768
1841
  await Promise.all(ticketIds.map(stopBuildAgent));
@@ -1781,13 +1854,34 @@ export const stopBuildAgent = async (ticketId) => {
1781
1854
  }
1782
1855
  return true;
1783
1856
  };
1857
+ export const stopReviewAgent = async (ticketId) => {
1858
+ const state = activeReviewProcesses.get(ticketId);
1859
+ if (!state) {
1860
+ return false;
1861
+ }
1862
+ state.stopRequested = true;
1863
+ const children = [...state.children];
1864
+ for (const child of children) {
1865
+ killProcess(child, 'SIGTERM');
1866
+ }
1867
+ await Promise.all(children.map(child => waitForProcessExit(child, 5000)));
1868
+ const aliveChildren = children.filter(isProcessAlive);
1869
+ for (const child of aliveChildren) {
1870
+ killProcess(child, 'SIGKILL');
1871
+ }
1872
+ await Promise.all(aliveChildren.map(child => waitForProcessExit(child, 1000)));
1873
+ return true;
1874
+ };
1784
1875
  export const stopChatAgent = async (chatSessionId) => {
1785
1876
  const state = activeChatProcesses.get(chatSessionId);
1786
- if (!state?.child) {
1877
+ if (!state) {
1787
1878
  return false;
1788
1879
  }
1789
1880
  state.stopRequested = true;
1790
1881
  state.metadataRefresher?.stop();
1882
+ if (!state.child) {
1883
+ return true;
1884
+ }
1791
1885
  killProcess(state.child, 'SIGTERM');
1792
1886
  await waitForProcessExit(state.child, 5000);
1793
1887
  if (isProcessAlive(state.child)) {
@@ -1803,20 +1897,22 @@ export const stopSpecAgent = async (specSessionId) => {
1803
1897
  }
1804
1898
  state.stopRequested = true;
1805
1899
  state.metadataRefresher?.stop();
1806
- // No child yet means we are in the pre-response window (the worker is still
1807
- // running prepareLinkedPlanningResearch). There is nothing to kill, but the
1808
- // stop request still succeeds: the worker honors stopRequested and finalizes
1809
- // the session to 'stopped' once its pre-step resolves. This mirrors how Chat
1810
- // treats an interrupt as accepted even before the agent child is live.
1811
- if (!state.child) {
1900
+ const children = [...state.researchChildren];
1901
+ if (state.child) {
1902
+ children.push(state.child);
1903
+ }
1904
+ if (children.length === 0) {
1812
1905
  return true;
1813
1906
  }
1814
- killProcess(state.child, 'SIGTERM');
1815
- await waitForProcessExit(state.child, 5000);
1816
- if (isProcessAlive(state.child)) {
1817
- killProcess(state.child, 'SIGKILL');
1818
- 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');
1819
1914
  }
1915
+ await Promise.all(aliveChildren.map(child => waitForProcessExit(child, 1000)));
1820
1916
  return true;
1821
1917
  };
1822
1918
  export const stopSpecReviewAgent = async (specReviewRunId) => {
@@ -1834,7 +1930,7 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1834
1930
  }
1835
1931
  return true;
1836
1932
  };
1837
- export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, 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, }) => {
1838
1934
  const resolvedModel = resolveAgentModel(tool, model);
1839
1935
  const subagentTracker = createSubagentRunTracker({
1840
1936
  parentType: 'ticket',
@@ -1850,6 +1946,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1850
1946
  },
1851
1947
  }
1852
1948
  : {}),
1949
+ executionContext,
1853
1950
  });
1854
1951
  const writeRunStatus = (status, log, streamingResponse = null, options = {}) => {
1855
1952
  if (runStream) {
@@ -1863,6 +1960,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1863
1960
  if (runStream) {
1864
1961
  updateAgentRunSessionId(runStream.runId, sessionId);
1865
1962
  emitAgentRunsUpdated(io, 'ticket', ticketId);
1963
+ if (persistSessionIdToTicket) {
1964
+ updateSessionId(ticketId, sessionId, sessionField, io);
1965
+ }
1866
1966
  return;
1867
1967
  }
1868
1968
  updateSessionId(ticketId, sessionId, sessionField, io);
@@ -1885,6 +1985,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1885
1985
  let settled = false;
1886
1986
  let startTime = null;
1887
1987
  let firstFinalAnswerTime = null;
1988
+ let reviewChild = null;
1888
1989
  const buildState = trackBuildProcess
1889
1990
  ? { child: null, stopRequested: false, sessionLookupTimer: null }
1890
1991
  : null;
@@ -1901,6 +2002,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1901
2002
  if (trackBuildProcess && buildState) {
1902
2003
  untrackBuildProcess(ticketId, buildState);
1903
2004
  }
2005
+ if (reviewChild) {
2006
+ reviewProcessState?.children.delete(reviewChild);
2007
+ }
1904
2008
  handler(value);
1905
2009
  };
1906
2010
  const resolveOnce = settle(resolve);
@@ -1937,12 +2041,22 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1937
2041
  if (tool === 'opencode') {
1938
2042
  spawnTimestamp = Date.now();
1939
2043
  }
1940
- const child = spawn(cmd, args, {
1941
- cwd,
1942
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
1943
- env: { ...process.env, PWD: cwd },
1944
- detached: process.platform !== 'win32',
1945
- });
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
+ }
2059
+ reviewChild = child;
1946
2060
  if (tool === 'claude') {
1947
2061
  const claudePrompt = opencodeAgentMode === 'plan'
1948
2062
  ? appendClaudeHeadlessPlanPrompt(prompt)
@@ -1953,6 +2067,12 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1953
2067
  buildState.child = child;
1954
2068
  activeBuildProcesses.set(ticketId, buildState);
1955
2069
  }
2070
+ if (reviewProcessState) {
2071
+ reviewProcessState.children.add(child);
2072
+ if (reviewProcessState.stopRequested) {
2073
+ killProcess(child, 'SIGTERM');
2074
+ }
2075
+ }
1956
2076
  startTime = Date.now();
1957
2077
  if (tool === 'opencode' && spawnTimestamp !== null) {
1958
2078
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
@@ -2018,7 +2138,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2018
2138
  const finalResponse = responseLines.join('\n\n') || errOutput;
2019
2139
  const modelError = isModelResolutionError(finalLog);
2020
2140
  const timing = startTime ? { totalMs: Date.now() - startTime } : null;
2021
- if (buildState?.stopRequested) {
2141
+ if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2022
2142
  const transcriptRuns = finalizeRun('stopped', finalLog, finalResponse);
2023
2143
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing, usageSnapshot }, transcriptRuns));
2024
2144
  return;
@@ -2033,7 +2153,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2033
2153
  });
2034
2154
  child.on('error', err => {
2035
2155
  progressUpdater.cancel();
2036
- if (buildState?.stopRequested) {
2156
+ if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2037
2157
  const transcriptRuns = finalizeRun('stopped', null, null);
2038
2158
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null, usageSnapshot }, transcriptRuns));
2039
2159
  return;
@@ -2117,8 +2237,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2117
2237
  ? recoveredResponse
2118
2238
  : responseLines.join('\n\n') || stderrOutput;
2119
2239
  const modelError = isModelResolutionError(log);
2120
- const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested) });
2121
- if (buildState?.stopRequested) {
2240
+ const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested || reviewProcessState?.stopRequested) });
2241
+ if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2122
2242
  const transcriptRuns = finalizeRun('stopped', log, finalResponse);
2123
2243
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing }, transcriptRuns));
2124
2244
  return;
@@ -2137,7 +2257,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2137
2257
  clearTimeout(sessionLookupTimer);
2138
2258
  sessionLookupTimer = null;
2139
2259
  }
2140
- if (buildState?.stopRequested) {
2260
+ if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2141
2261
  const transcriptRuns = finalizeRun('stopped', null, null);
2142
2262
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
2143
2263
  return;
@@ -2147,12 +2267,15 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2147
2267
  });
2148
2268
  });
2149
2269
  };
2150
- 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, }) => {
2151
2271
  const resolvedModel = resolveAgentModel(tool, model);
2152
2272
  const subagentTracker = createSubagentRunTracker({
2153
2273
  parentType: 'chat',
2154
2274
  parentId: chatSessionId,
2155
2275
  io,
2276
+ executionContext,
2277
+ mainRun,
2278
+ deferMainFinalization: true,
2156
2279
  });
2157
2280
  return new Promise((resolve, reject) => {
2158
2281
  let sessionId = null;
@@ -2163,11 +2286,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2163
2286
  let startTime = null;
2164
2287
  let firstFinalAnswerTime = null;
2165
2288
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2166
- const chatState = {
2167
- child: null,
2168
- stopRequested: false,
2169
- metadataRefresher: null,
2170
- };
2289
+ const chatState = activeChatProcesses.get(chatSessionId) ?? registerChatProcessPlaceholder(chatSessionId);
2171
2290
  const settle = (handler) => (value) => {
2172
2291
  if (settled) {
2173
2292
  return;
@@ -2180,6 +2299,16 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2180
2299
  };
2181
2300
  const resolveOnce = settle(resolve);
2182
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
+ }
2183
2312
  const [cmd, args] = tool === 'claude'
2184
2313
  ? (() => {
2185
2314
  sessionId = resumeSessionId || randomUUID();
@@ -2190,8 +2319,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2190
2319
  })()
2191
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]];
2192
2321
  updateChatSessionStatus(chatSessionId, 'running', null, io);
2322
+ subagentTracker.updateMain(null, null);
2193
2323
  if (tool === 'claude' && sessionId) {
2194
2324
  updateChatSessionId(chatSessionId, tool, sessionId, io);
2325
+ subagentTracker.setMainSessionId(sessionId);
2195
2326
  }
2196
2327
  if (tool === 'opencode') {
2197
2328
  spawnTimestamp = Date.now();
@@ -2209,12 +2340,21 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2209
2340
  }
2210
2341
  }
2211
2342
  }
2212
- const child = spawn(cmd, args, {
2213
- cwd,
2214
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2215
- env: { ...process.env, PWD: cwd },
2216
- detached: process.platform !== 'win32',
2217
- });
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
+ }
2218
2358
  chatState.child = child;
2219
2359
  activeChatProcesses.set(chatSessionId, chatState);
2220
2360
  if (tool === 'claude') {
@@ -2233,6 +2373,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2233
2373
  discoverParentSession: false,
2234
2374
  onParentSessionId: discoveredSessionId => {
2235
2375
  updateChatSessionId(chatSessionId, 'opencode', discoveredSessionId, io);
2376
+ subagentTracker.setMainSessionId(discoveredSessionId);
2236
2377
  },
2237
2378
  });
2238
2379
  opencodeSubagentMonitor.start();
@@ -2243,7 +2384,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2243
2384
  let remainder = '';
2244
2385
  const stderrChunks = [];
2245
2386
  const progressUpdater = createThrottledProgressUpdater(() => {
2246
- 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);
2247
2391
  });
2248
2392
  child.stdout?.on('data', (chunk) => {
2249
2393
  const text = remainder + chunk.toString();
@@ -2350,7 +2494,10 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2350
2494
  let remainder = '';
2351
2495
  const stderrChunks = [];
2352
2496
  const progressUpdater = createThrottledProgressUpdater(() => {
2353
- 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);
2354
2501
  });
2355
2502
  child.stdout?.on('data', (chunk) => {
2356
2503
  const text = remainder + chunk.toString();
@@ -2492,12 +2639,15 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2492
2639
  });
2493
2640
  });
2494
2641
  };
2495
- 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, }) => {
2496
2643
  const resolvedModel = resolveAgentModel(tool, model);
2497
2644
  const subagentTracker = createSubagentRunTracker({
2498
2645
  parentType: 'spec',
2499
2646
  parentId: specSessionId,
2500
2647
  io,
2648
+ executionContext,
2649
+ mainRun,
2650
+ deferMainFinalization: true,
2501
2651
  });
2502
2652
  return new Promise((resolve, reject) => {
2503
2653
  let sessionId = null;
@@ -2508,11 +2658,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2508
2658
  let startTime = null;
2509
2659
  let firstFinalAnswerTime = null;
2510
2660
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2511
- const specState = {
2512
- child: null,
2513
- stopRequested: false,
2514
- metadataRefresher: null,
2515
- };
2661
+ const specState = activeSpecProcesses.get(specSessionId) ?? registerSpecProcessPlaceholder(specSessionId);
2516
2662
  const settle = (handler) => (value) => {
2517
2663
  if (settled) {
2518
2664
  return;
@@ -2525,6 +2671,16 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2525
2671
  };
2526
2672
  const resolveOnce = settle(resolve);
2527
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
+ }
2528
2684
  const [cmd, args] = tool === 'claude'
2529
2685
  ? (() => {
2530
2686
  sessionId = resumeSessionId || randomUUID();
@@ -2535,8 +2691,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2535
2691
  })()
2536
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]];
2537
2693
  updateSpecSessionStatusLocal(specSessionId, 'running', null, io);
2694
+ subagentTracker.updateMain(null, null);
2538
2695
  if (tool === 'claude' && sessionId) {
2539
2696
  updateSpecSessionIdLocal(specSessionId, tool, sessionId, io);
2697
+ subagentTracker.setMainSessionId(sessionId);
2540
2698
  }
2541
2699
  if (tool === 'opencode') {
2542
2700
  spawnTimestamp = Date.now();
@@ -2554,12 +2712,21 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2554
2712
  }
2555
2713
  }
2556
2714
  }
2557
- const child = spawn(cmd, args, {
2558
- cwd,
2559
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2560
- env: { ...process.env, PWD: cwd },
2561
- detached: process.platform !== 'win32',
2562
- });
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
+ }
2563
2730
  specState.child = child;
2564
2731
  activeSpecProcesses.set(specSessionId, specState);
2565
2732
  if (tool === 'claude') {
@@ -2575,6 +2742,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2575
2742
  discoverParentSession: false,
2576
2743
  onParentSessionId: discoveredSessionId => {
2577
2744
  updateSpecSessionIdLocal(specSessionId, 'opencode', discoveredSessionId, io);
2745
+ subagentTracker.setMainSessionId(discoveredSessionId);
2578
2746
  },
2579
2747
  });
2580
2748
  opencodeSubagentMonitor.start();
@@ -2585,7 +2753,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2585
2753
  let remainder = '';
2586
2754
  const stderrChunks = [];
2587
2755
  const progressUpdater = createThrottledProgressUpdater(() => {
2588
- 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);
2589
2760
  });
2590
2761
  child.stdout?.on('data', (chunk) => {
2591
2762
  const text = remainder + chunk.toString();
@@ -2692,7 +2863,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2692
2863
  let remainder = '';
2693
2864
  const stderrChunks = [];
2694
2865
  const progressUpdater = createThrottledProgressUpdater(() => {
2695
- 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);
2696
2870
  });
2697
2871
  child.stdout?.on('data', (chunk) => {
2698
2872
  const text = remainder + chunk.toString();
@@ -2845,15 +3019,24 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2845
3019
  * updateSpecReviewRunStatus / updateSpecReviewRunStreaming / updateSpecReviewRunUsage —
2846
3020
  * the spec_sessions row is never touched by this path.
2847
3021
  */
2848
- 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, }) => {
2849
3023
  const resolvedModel = resolveAgentModel(tool, model);
3024
+ const subagentTracker = createSubagentRunTracker({
3025
+ parentType: 'spec',
3026
+ parentId: specSessionId,
3027
+ io,
3028
+ executionContext,
3029
+ });
2850
3030
  return new Promise((resolve, reject) => {
3031
+ let spawnTimestamp = null;
3032
+ let opencodeSubagentMonitor = null;
2851
3033
  let settled = false;
2852
3034
  let startTime = null;
2853
3035
  let firstFinalAnswerTime = null;
2854
3036
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
2855
3037
  const reviewState = {
2856
3038
  child: null,
3039
+ researchChildren: new Set(),
2857
3040
  stopRequested: false,
2858
3041
  metadataRefresher: null,
2859
3042
  };
@@ -2862,14 +3045,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2862
3045
  return;
2863
3046
  }
2864
3047
  settled = true;
3048
+ opencodeSubagentMonitor?.stop();
2865
3049
  untrackSpecReviewProcess(specReviewRunId, reviewState);
2866
3050
  handler(value);
2867
3051
  };
2868
3052
  const resolveOnce = settle(resolve);
2869
3053
  const rejectOnce = settle(reject);
2870
- const emitReviewUpdated = () => {
2871
- io.emit('spec-review:updated', { spec_review_run_id: specReviewRunId });
2872
- };
2873
3054
  const [cmd, args] = tool === 'claude'
2874
3055
  ? [
2875
3056
  'claude',
@@ -2877,7 +3058,6 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2877
3058
  ]
2878
3059
  : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
2879
3060
  updateSpecReviewRunStreaming(specReviewRunId, null);
2880
- emitReviewUpdated();
2881
3061
  const child = spawn(cmd, args, {
2882
3062
  cwd,
2883
3063
  stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
@@ -2890,6 +3070,16 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2890
3070
  writeClaudeStdin(child, buildClaudeStdinMessage(appendClaudeHeadlessPlanPrompt(prompt), files, cwd));
2891
3071
  }
2892
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
+ }
2893
3083
  if (tool === 'claude') {
2894
3084
  const logLines = [];
2895
3085
  const responseLines = [];
@@ -2904,7 +3094,8 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2904
3094
  remainder = lines.pop() ?? '';
2905
3095
  for (const line of lines) {
2906
3096
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2907
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line);
3097
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line);
3098
+ subagentTracker.handle(subagentEvents);
2908
3099
  if (logFragment !== null) {
2909
3100
  logLines.push(logFragment);
2910
3101
  }
@@ -2921,7 +3112,8 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2921
3112
  progressUpdater.cancel();
2922
3113
  if (remainder.trim()) {
2923
3114
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2924
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder);
3115
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
3116
+ subagentTracker.handle(subagentEvents);
2925
3117
  if (logFragment !== null) {
2926
3118
  logLines.push(logFragment);
2927
3119
  }
@@ -2934,6 +3126,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2934
3126
  const finalResponse = responseLines.join('\n\n') || errOutput;
2935
3127
  if (reviewState.stopRequested) {
2936
3128
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3129
+ subagentTracker.finalize('stopped', finalLog || null, finalResponse || null);
2937
3130
  resolveOnce({
2938
3131
  completionStatus: 'stopped',
2939
3132
  finalResponse,
@@ -2945,6 +3138,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2945
3138
  const modelError = isModelResolutionError(finalLog);
2946
3139
  if (code === 0 && !modelError) {
2947
3140
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3141
+ subagentTracker.finalize('done', finalLog || null, finalResponse || null);
2948
3142
  resolveOnce({
2949
3143
  completionStatus: 'done',
2950
3144
  finalResponse,
@@ -2953,10 +3147,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2953
3147
  });
2954
3148
  return;
2955
3149
  }
3150
+ subagentTracker.finalize('error', finalLog || null, finalResponse || null);
2956
3151
  rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
2957
3152
  });
2958
3153
  child.on('error', err => {
2959
3154
  progressUpdater.cancel();
3155
+ subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
2960
3156
  rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2961
3157
  });
2962
3158
  return;
@@ -2991,7 +3187,9 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2991
3187
  }
2992
3188
  }
2993
3189
  const parsed = parseOpencodeJsonLine(line, cwd);
2994
- const { logFragment, responseFragment } = parsed;
3190
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
3191
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3192
+ subagentTracker.handle(subagentEvents);
2995
3193
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2996
3194
  firstFinalAnswerTime = null;
2997
3195
  }
@@ -3003,9 +3201,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3003
3201
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
3004
3202
  child.on('close', code => {
3005
3203
  progressUpdater.cancel();
3204
+ opencodeSubagentMonitor?.refresh();
3006
3205
  if (remainder.trim()) {
3007
3206
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3008
3207
  const parsed = parseOpencodeJsonLine(remainder, cwd);
3208
+ opencodeSubagentMonitor?.setParentSessionId(parsed.providerSessionId);
3209
+ subagentTracker.handle(parsed.subagentEvents);
3009
3210
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3010
3211
  firstFinalAnswerTime = null;
3011
3212
  }
@@ -3018,6 +3219,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3018
3219
  : responseLines.join('\n\n') || stderrOutput;
3019
3220
  if (reviewState.stopRequested) {
3020
3221
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3222
+ subagentTracker.finalize('stopped', log || null, finalResponse || null);
3021
3223
  resolveOnce({
3022
3224
  completionStatus: 'stopped',
3023
3225
  finalResponse,
@@ -3029,6 +3231,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3029
3231
  const modelError = isModelResolutionError(log);
3030
3232
  if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
3031
3233
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3234
+ subagentTracker.finalize('done', log || null, finalResponse || null);
3032
3235
  resolveOnce({
3033
3236
  completionStatus: 'done',
3034
3237
  finalResponse,
@@ -3037,10 +3240,12 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
3037
3240
  });
3038
3241
  return;
3039
3242
  }
3243
+ subagentTracker.finalize('error', log || null, finalResponse || null);
3040
3244
  rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
3041
3245
  });
3042
3246
  child.on('error', err => {
3043
3247
  progressUpdater.cancel();
3248
+ subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3044
3249
  rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3045
3250
  });
3046
3251
  });