@straiffi/archon 1.7.2 → 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 (164) 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-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.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-DvF75yLZ.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.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-gm51u2Nn.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.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-BnCBVqcd.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.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 +1256 -214
  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/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 +44 -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/ticketRunOperations.js +51 -6
  126. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  127. package/dist/server/lib/ticketUndo.js +77 -168
  128. package/dist/server/lib/ticketUndo.js.map +1 -1
  129. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  130. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  131. package/dist/server/lib/tickets.js +41 -27
  132. package/dist/server/lib/tickets.js.map +1 -1
  133. package/dist/server/lib/transcriptRuns.js +3 -3
  134. package/dist/server/lib/transcriptRuns.js.map +1 -1
  135. package/dist/server/lib/turnExecutionErrors.js +12 -0
  136. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  137. package/dist/server/lib/types.js +57 -1
  138. package/dist/server/lib/types.js.map +1 -1
  139. package/dist/server/workers/build.js +596 -184
  140. package/dist/server/workers/build.js.map +1 -1
  141. package/dist/server/workers/chat.js +261 -130
  142. package/dist/server/workers/chat.js.map +1 -1
  143. package/dist/server/workers/followUp.js +716 -203
  144. package/dist/server/workers/followUp.js.map +1 -1
  145. package/dist/server/workers/plan.js +527 -188
  146. package/dist/server/workers/plan.js.map +1 -1
  147. package/dist/server/workers/planFollowUp.js +231 -69
  148. package/dist/server/workers/planFollowUp.js.map +1 -1
  149. package/dist/server/workers/review.js +790 -297
  150. package/dist/server/workers/review.js.map +1 -1
  151. package/dist/server/workers/specReview.js +211 -45
  152. package/dist/server/workers/specReview.js.map +1 -1
  153. package/dist/server/workers/specSession.js +425 -252
  154. package/dist/server/workers/specSession.js.map +1 -1
  155. package/package.json +1 -1
  156. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  157. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  158. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  159. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  160. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  161. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  162. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  163. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  164. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
@@ -24,30 +24,50 @@ import { beginTicketThinkingRun, bindTicketThinkingMessage } from '../lib/thinki
24
24
  import { createTicketMessage, upsertTicketDescriptionMessage } from '../lib/ticketMessages.js';
25
25
  import { getTicket, resolveTicketBranch, resolveTicketTool, resolveTicketWorkerKey } from '../lib/tickets.js';
26
26
  import { createOpencodeSubagentMonitor, createSubagentRunTracker, formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect, mergeUsageFromJsonLine, EMPTY_CHAT_USAGE_SNAPSHOT } from '../lib/agent.js';
27
- import { clearAndEmitAgentRunsForParent, createAgentRun, finalizeAgentRun, getNextAgentRunIndex } from '../lib/agentRuns.js';
27
+ import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, terminalizeAgentRunsForExecutionRun } from '../lib/agentRuns.js';
28
+ import { addExecutionRunLink, createExecutionRun, transitionExecutionRun } from '../lib/executionRuns.js';
29
+ const resolvePlannerAgentRunParent = (options) => {
30
+ if (options.agentRunParent) {
31
+ return options.agentRunParent;
32
+ }
33
+ if (options.ticketId && options.io) {
34
+ return { parentType: 'ticket', parentId: options.ticketId, io: options.io };
35
+ }
36
+ return undefined;
37
+ };
28
38
  const createPlanningInvocation = (options, label, role) => {
29
- const parentType = options.ticketId ? 'ticket' : options.projectId ? 'planning' : null;
30
- const parentId = options.ticketId ?? options.projectId ?? null;
39
+ const parentType = options.agentRunParent?.parentType ?? (options.ticketId ? 'ticket' : options.projectId ? 'planning' : null);
40
+ const parentId = options.agentRunParent?.parentId ?? options.ticketId ?? options.projectId ?? null;
31
41
  if (!parentType || !parentId) {
32
42
  return null;
33
43
  }
44
+ const runIndex = options.executionContext
45
+ ? getNextAgentRunIndex(options.executionContext.runId)
46
+ : getNextAgentRunIndex(parentType, parentId);
47
+ const persistedRole = role === 'research-selection' || role === 'research' ? role : 'main';
34
48
  return {
35
49
  parentType,
36
50
  parentId,
37
- runId: createAgentRun({
51
+ id: createAgentRun({
52
+ ...(options.executionContext ? { executionRunId: options.executionContext.runId } : {}),
38
53
  parentType,
39
54
  parentId,
40
- runIndex: getNextAgentRunIndex(parentType, parentId),
55
+ ...(!options.executionContext ? { runIndex } : {}),
41
56
  label,
42
- role,
57
+ role: persistedRole,
43
58
  }),
59
+ runIndex,
60
+ label,
61
+ role: persistedRole,
44
62
  };
45
63
  };
46
- const finalizePlanningInvocation = (invocation, status, response) => {
64
+ const finalizePlanningInvocation = (invocation, status, evidence) => {
47
65
  if (invocation) {
48
- finalizeAgentRun(invocation.runId, status, response);
66
+ finalizeAgentRun(invocation.id, status, status === 'done' ? evidence : null, status === 'error' ? evidence : null);
49
67
  }
50
68
  };
69
+ const activePlanProcesses = new Map();
70
+ const activePlannerProcesses = new Set();
51
71
  const createPlannerRecoveryTracker = () => {
52
72
  let sawError = false;
53
73
  let latestEvent = null;
@@ -116,6 +136,19 @@ const extractLeadingJsonObject = (line) => {
116
136
  }
117
137
  return null;
118
138
  };
139
+ const extractLeadingJsonObjects = (line) => {
140
+ const jsonObjects = [];
141
+ let remainder = line.trim();
142
+ while (remainder.startsWith('{')) {
143
+ const jsonObject = extractLeadingJsonObject(remainder);
144
+ if (!jsonObject) {
145
+ break;
146
+ }
147
+ jsonObjects.push(jsonObject);
148
+ remainder = remainder.slice(jsonObject.length).trim();
149
+ }
150
+ return { jsonObjects, remainder };
151
+ };
119
152
  const normalizeToolName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
120
153
  const isPlannerSubagentTool = (name) => {
121
154
  const normalized = normalizeToolName(name);
@@ -332,35 +365,7 @@ const formatPlannerStepTitle = (event) => {
332
365
  const title = event.part?.title?.trim();
333
366
  return title || null;
334
367
  };
335
- const parseOpencodePlannerJsonLine = (line) => {
336
- const trimmedLine = line.trim();
337
- if (!trimmedLine) {
338
- return emptyParsedPlannerStreamLine();
339
- }
340
- const jsonLine = extractLeadingJsonObject(trimmedLine);
341
- if (!jsonLine) {
342
- const envelope = parseOpencodeTaskEnvelope(trimmedLine);
343
- if (envelope) {
344
- return {
345
- ...emptyParsedPlannerStreamLine(),
346
- subagentEvents: [{
347
- kind: 'complete',
348
- callId: null,
349
- sessionId: envelope.childSessionId,
350
- status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
351
- ...(envelope.result ? { text: envelope.result } : {}),
352
- }],
353
- };
354
- }
355
- return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
356
- }
357
- let event;
358
- try {
359
- event = JSON.parse(jsonLine);
360
- }
361
- catch {
362
- return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
363
- }
368
+ const parseOpencodePlannerJsonEvent = (event) => {
364
369
  const providerSessionId = event.sessionID?.trim()
365
370
  || event.sessionId?.trim()
366
371
  || event.part?.sessionID?.trim()
@@ -374,6 +379,7 @@ const parseOpencodePlannerJsonLine = (line) => {
374
379
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
375
380
  subagentEvents: [],
376
381
  providerSessionId,
382
+ hasFinalAnswer: Boolean(text && (!phase || phase === 'final_answer')),
377
383
  recoveryEvents: text && phase === 'final_answer'
378
384
  ? [{ type: 'final_answer', text }]
379
385
  : [],
@@ -460,6 +466,86 @@ const parseOpencodePlannerJsonLine = (line) => {
460
466
  }
461
467
  return { ...emptyParsedPlannerStreamLine(), providerSessionId };
462
468
  };
469
+ const parseOpencodePlannerJsonLine = (line) => {
470
+ const trimmedLine = line.trim();
471
+ if (!trimmedLine) {
472
+ return emptyParsedPlannerStreamLine();
473
+ }
474
+ const { jsonObjects, remainder } = extractLeadingJsonObjects(trimmedLine);
475
+ if (jsonObjects.length === 0) {
476
+ if (trimmedLine.startsWith('{')) {
477
+ return emptyParsedPlannerStreamLine();
478
+ }
479
+ const envelope = parseOpencodeTaskEnvelope(trimmedLine);
480
+ if (envelope) {
481
+ return {
482
+ ...emptyParsedPlannerStreamLine(),
483
+ subagentEvents: [{
484
+ kind: 'complete',
485
+ callId: null,
486
+ sessionId: envelope.childSessionId,
487
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
488
+ ...(envelope.result ? { text: envelope.result } : {}),
489
+ }],
490
+ };
491
+ }
492
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
493
+ }
494
+ const logFragments = [];
495
+ const responseFragments = [];
496
+ const subagentEvents = [];
497
+ const recoveryEvents = [];
498
+ let providerSessionId = null;
499
+ let hasFinalAnswer = false;
500
+ for (const jsonObject of jsonObjects) {
501
+ let event;
502
+ try {
503
+ event = JSON.parse(jsonObject);
504
+ }
505
+ catch {
506
+ continue;
507
+ }
508
+ const parsedEvent = parseOpencodePlannerJsonEvent(event);
509
+ const eventRecoveryEvents = parsedEvent.recoveryEvents ?? [];
510
+ if (eventRecoveryEvents.some(recoveryEvent => recoveryEvent.type === 'error')) {
511
+ responseFragments.length = 0;
512
+ }
513
+ if (parsedEvent.logFragment !== null) {
514
+ logFragments.push(parsedEvent.logFragment);
515
+ }
516
+ if (parsedEvent.responseFragment !== null) {
517
+ responseFragments.push(parsedEvent.responseFragment);
518
+ }
519
+ subagentEvents.push(...parsedEvent.subagentEvents);
520
+ recoveryEvents.push(...eventRecoveryEvents);
521
+ providerSessionId = providerSessionId ?? parsedEvent.providerSessionId;
522
+ hasFinalAnswer ||= parsedEvent.hasFinalAnswer === true;
523
+ }
524
+ if (remainder) {
525
+ const envelope = parseOpencodeTaskEnvelope(remainder);
526
+ if (envelope) {
527
+ subagentEvents.push({
528
+ kind: 'complete',
529
+ callId: null,
530
+ sessionId: envelope.childSessionId,
531
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
532
+ ...(envelope.result ? { text: envelope.result } : {}),
533
+ });
534
+ }
535
+ else if (logFragments.length === 0 && responseFragments.length === 0 && subagentEvents.length === 0 && recoveryEvents.length === 0) {
536
+ logFragments.push(remainder);
537
+ responseFragments.push(remainder);
538
+ }
539
+ }
540
+ return {
541
+ logFragment: logFragments.length > 0 ? logFragments.join('\n\n') : null,
542
+ responseFragment: responseFragments.length > 0 ? responseFragments.join('\n\n') : null,
543
+ subagentEvents,
544
+ providerSessionId,
545
+ recoveryEvents,
546
+ hasFinalAnswer,
547
+ };
548
+ };
463
549
  const attachPlannerTranscriptRuns = (error, transcriptRuns) => {
464
550
  const resolvedError = (error instanceof Error ? error : new Error(String(error)));
465
551
  if (transcriptRuns) {
@@ -883,7 +969,7 @@ const killPlannerProcess = (proc, signal) => {
883
969
  }
884
970
  catch (error) {
885
971
  if (error.code === 'ESRCH') {
886
- return;
972
+ // The child may not have a process group; fall back to signaling it directly.
887
973
  }
888
974
  }
889
975
  }
@@ -894,6 +980,18 @@ const killPlannerProcess = (proc, signal) => {
894
980
  // best-effort
895
981
  }
896
982
  };
983
+ export const stopPlannerProcesses = async (children) => {
984
+ for (const child of children) {
985
+ killPlannerProcess(child, 'SIGTERM');
986
+ }
987
+ const deadline = Date.now() + PLANNER_KILL_GRACE_MS;
988
+ while (children.some(isPlannerProcessAlive) && Date.now() < deadline) {
989
+ await new Promise(resolve => setTimeout(resolve, 100));
990
+ }
991
+ for (const child of children.filter(isPlannerProcessAlive)) {
992
+ killPlannerProcess(child, 'SIGKILL');
993
+ }
994
+ };
897
995
  const opencodeSessionHasCompletedAssistantSince = (sessionId, sinceMs) => {
898
996
  try {
899
997
  const opencodeDb = new Database(resolveOpencodeDbPath(), { readonly: true });
@@ -964,6 +1062,9 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
964
1062
  ? createSubagentRunTracker({
965
1063
  ...options.agentRunParent,
966
1064
  io: options.agentRunParent.io,
1065
+ executionContext: options.executionContext,
1066
+ mainRun: options.mainRun,
1067
+ deferMainFinalization: Boolean(options.mainRun),
967
1068
  })
968
1069
  : null;
969
1070
  let opencodeSubagentMonitor = null;
@@ -1011,11 +1112,28 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1011
1112
  if (tool === 'opencode' && (sessionTracking || subagentTracker)) {
1012
1113
  spawnTimestamp = Date.now();
1013
1114
  }
1014
- const child = spawn(cmd, args, {
1015
- stdio: ['ignore', 'pipe', 'pipe'],
1016
- env: { ...process.env, ...(cwd ? { PWD: cwd } : {}) },
1017
- ...(cwd ? { cwd } : {}),
1018
- });
1115
+ let child;
1116
+ try {
1117
+ child = spawn(cmd, args, {
1118
+ stdio: ['ignore', 'pipe', 'pipe'],
1119
+ env: { ...process.env, ...(cwd ? { PWD: cwd } : {}) },
1120
+ detached: process.platform !== 'win32',
1121
+ ...(cwd ? { cwd } : {}),
1122
+ });
1123
+ }
1124
+ catch (error) {
1125
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
1126
+ const transcriptRuns = subagentTracker?.finalize('error', resolvedError.message, null) ?? null;
1127
+ rejectOnce(attachPlannerTranscriptRuns(resolvedError, transcriptRuns));
1128
+ return;
1129
+ }
1130
+ activePlannerProcesses.add(child);
1131
+ if (options.cancellationState) {
1132
+ options.cancellationState.researchChildren.add(child);
1133
+ if (options.cancellationState.stopRequested) {
1134
+ killPlannerProcess(child, 'SIGTERM');
1135
+ }
1136
+ }
1019
1137
  if (sessionTracking && tool === 'claude' && sessionId) {
1020
1138
  hasResolvedSessionId = true;
1021
1139
  updatePlanningSessionId(sessionTracking.ticketId, sessionId, sessionTracking.io);
@@ -1070,21 +1188,6 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1070
1188
  if (tool === 'claude') {
1071
1189
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1072
1190
  }
1073
- if (tool === 'opencode' && firstFinalAnswerTime === null) {
1074
- const trimmedLine = line.trim();
1075
- if (trimmedLine.startsWith('{')) {
1076
- try {
1077
- const event = JSON.parse(trimmedLine);
1078
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1079
- const phase = event.part.metadata?.openai?.phase;
1080
- if (!phase || phase === 'final_answer') {
1081
- firstFinalAnswerTime = Date.now();
1082
- }
1083
- }
1084
- }
1085
- catch { /* best-effort timing */ }
1086
- }
1087
- }
1088
1191
  const parsedLine = tool === 'claude'
1089
1192
  ? parseClaudePlannerStreamJsonLine(line)
1090
1193
  : parseOpencodePlannerJsonLine(line);
@@ -1093,6 +1196,9 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1093
1196
  responseLines.length = 0;
1094
1197
  firstFinalAnswerTime = null;
1095
1198
  }
1199
+ if (tool === 'opencode' && firstFinalAnswerTime === null && parsedLine.hasFinalAnswer) {
1200
+ firstFinalAnswerTime = Date.now();
1201
+ }
1096
1202
  subagentTracker?.handle(subagentEvents);
1097
1203
  if (providerSessionId) {
1098
1204
  opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
@@ -1117,6 +1223,8 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1117
1223
  });
1118
1224
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
1119
1225
  child.on('close', code => {
1226
+ activePlannerProcesses.delete(child);
1227
+ options.cancellationState?.researchChildren.delete(child);
1120
1228
  clearWatchdogTimer();
1121
1229
  opencodeSubagentMonitor?.refresh();
1122
1230
  const resolvedSessionOnClose = attemptOpencodeSessionLookup();
@@ -1136,6 +1244,9 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1136
1244
  responseLines.length = 0;
1137
1245
  firstFinalAnswerTime = null;
1138
1246
  }
1247
+ if (tool === 'opencode' && firstFinalAnswerTime === null && parsedLine.hasFinalAnswer) {
1248
+ firstFinalAnswerTime = Date.now();
1249
+ }
1139
1250
  subagentTracker?.handle(subagentEvents);
1140
1251
  if (providerSessionId) {
1141
1252
  opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
@@ -1150,11 +1261,14 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1150
1261
  }
1151
1262
  const rawOutput = Buffer.concat(stdoutChunks).toString().trim();
1152
1263
  const recoveredResponse = recoveryTracker.getRecoveredResponse();
1264
+ const parsedResponse = responseLines.join('\n\n').trim();
1153
1265
  const output = tool === 'opencode' && recoveryTracker.hasRecoveredFinalAnswer()
1154
1266
  ? recoveredResponse
1155
- : tool === 'claude' && !rawOutput.startsWith('{')
1156
- ? rawOutput
1157
- : responseLines.join('\n\n').trim() || rawOutput;
1267
+ : tool === 'opencode'
1268
+ ? parsedResponse || (rawOutput.startsWith('{') ? '' : rawOutput)
1269
+ : !rawOutput.startsWith('{')
1270
+ ? rawOutput
1271
+ : parsedResponse || rawOutput;
1158
1272
  const errorOutput = Buffer.concat(stderrChunks).toString().trim();
1159
1273
  const timing = (() => {
1160
1274
  const totalMs = Date.now() - startTime;
@@ -1165,6 +1279,11 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1165
1279
  }
1166
1280
  return { totalMs };
1167
1281
  })();
1282
+ if (options.cancellationState?.stopRequested) {
1283
+ const transcriptRuns = subagentTracker?.finalize('stopped', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1284
+ rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || 'Planning was stopped.'), transcriptRuns));
1285
+ return;
1286
+ }
1168
1287
  if (hardTimeoutForced) {
1169
1288
  const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1170
1289
  rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || `Planner exceeded the ${PLANNER_HARD_TIMEOUT_MS} ms hard timeout.`), transcriptRuns));
@@ -1180,16 +1299,20 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
1180
1299
  const fallbackError = code === 0
1181
1300
  ? 'Planner did not return a usable plan.'
1182
1301
  : `Process exited with code ${code}`;
1183
- const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1302
+ const finalStatus = options.cancellationState?.stopRequested ? 'stopped' : 'error';
1303
+ const transcriptRuns = subagentTracker?.finalize(finalStatus, logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1184
1304
  rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || fallbackError), transcriptRuns));
1185
1305
  });
1186
1306
  child.on('error', err => {
1307
+ activePlannerProcesses.delete(child);
1308
+ options.cancellationState?.researchChildren.delete(child);
1187
1309
  clearWatchdogTimer();
1188
1310
  if (sessionLookupTimer && hasResolvedSessionId) {
1189
1311
  clearTimeout(sessionLookupTimer);
1190
1312
  sessionLookupTimer = null;
1191
1313
  }
1192
- const transcriptRuns = subagentTracker?.finalize('error', err.message, null) ?? null;
1314
+ const finalStatus = options.cancellationState?.stopRequested ? 'stopped' : 'error';
1315
+ const transcriptRuns = subagentTracker?.finalize(finalStatus, err.message, null) ?? null;
1193
1316
  rejectOnce(attachPlannerTranscriptRuns(err, transcriptRuns));
1194
1317
  });
1195
1318
  if (tool === 'opencode') {
@@ -1475,11 +1598,22 @@ export const prepareLinkedPlanningResearch = async (taskDescription, options = {
1475
1598
  }
1476
1599
  const tool = options.tool ?? config.tool ?? 'opencode';
1477
1600
  let decision;
1601
+ const selectionInvocation = createPlanningInvocation(options, 'Linked Research Selection', 'research-selection');
1478
1602
  try {
1479
- const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, buildLinkedProjectSelectionPrompt(taskDescription, project, linkedProjects), cwd);
1603
+ const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, buildLinkedProjectSelectionPrompt(taskDescription, project, linkedProjects), cwd, {
1604
+ agentRunParent: resolvePlannerAgentRunParent(options),
1605
+ executionContext: options.executionContext,
1606
+ cancellationState: options.cancellationState,
1607
+ ...(selectionInvocation ? { mainRun: selectionInvocation } : {}),
1608
+ });
1480
1609
  decision = extractFirstJsonObject(output);
1610
+ finalizePlanningInvocation(selectionInvocation, 'done', output);
1481
1611
  }
1482
- catch {
1612
+ catch (error) {
1613
+ finalizePlanningInvocation(selectionInvocation, options.cancellationState?.stopRequested ? 'stopped' : 'error', error instanceof Error ? error.message : String(error));
1614
+ if (options.executionContext?.kind === 'planning_agent') {
1615
+ throw error;
1616
+ }
1483
1617
  return [];
1484
1618
  }
1485
1619
  const requests = normalizeLinkedResearchRequests(decision, linkedProjects);
@@ -1492,8 +1626,22 @@ export const prepareLinkedPlanningResearch = async (taskDescription, options = {
1492
1626
  if (!linkedProject) {
1493
1627
  throw new Error(`Linked project ${request.project_id} is no longer available`);
1494
1628
  }
1495
- const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildLinkedProjectResearchPrompt(project, linkedProject, request), options.skills ?? []), linkedProject.repo_path);
1496
- return normalizeLinkedResearchResponse(extractFirstJsonObject(output), linkedProject, request);
1629
+ const invocation = createPlanningInvocation(options, `Linked Research: ${linkedProject.name}`, 'research');
1630
+ try {
1631
+ const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildLinkedProjectResearchPrompt(project, linkedProject, request), options.skills ?? []), linkedProject.repo_path, {
1632
+ agentRunParent: resolvePlannerAgentRunParent(options),
1633
+ executionContext: options.executionContext,
1634
+ cancellationState: options.cancellationState,
1635
+ ...(invocation ? { mainRun: invocation } : {}),
1636
+ });
1637
+ const result = normalizeLinkedResearchResponse(extractFirstJsonObject(output), linkedProject, request);
1638
+ finalizePlanningInvocation(invocation, 'done', output);
1639
+ return result;
1640
+ }
1641
+ catch (error) {
1642
+ finalizePlanningInvocation(invocation, options.cancellationState?.stopRequested ? 'stopped' : 'error', error instanceof Error ? error.message : String(error));
1643
+ throw error;
1644
+ }
1497
1645
  }));
1498
1646
  return linkedResearch;
1499
1647
  };
@@ -1509,7 +1657,7 @@ export const generatePlanText = (title, description, options = {}) => {
1509
1657
  stage: 'plan',
1510
1658
  title,
1511
1659
  description,
1512
- ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1660
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.id } } : {}),
1513
1661
  });
1514
1662
  const { output: plannerOutput, responseTiming: plannerTiming, usageSnapshot, transcriptRuns } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildSingleTicketPrompt(title, description, linkedResearchContext, projectMemoryBlock, Boolean(project?.memory_enabled)), options.skills ?? []), resolvePlannerCwd(project, options.branch), {
1515
1663
  onProgress: options.onProgress,
@@ -1527,6 +1675,9 @@ export const generatePlanText = (title, description, options = {}) => {
1527
1675
  },
1528
1676
  }
1529
1677
  : {}),
1678
+ executionContext: options.executionContext,
1679
+ cancellationState: options.cancellationState,
1680
+ ...(invocation ? { mainRun: invocation } : {}),
1530
1681
  });
1531
1682
  try {
1532
1683
  const expanded = extractTaggedJsonObjectWithLeadingText(plannerOutput, PLAN_OUTPUT_TAG)
@@ -1549,8 +1700,10 @@ export const generatePlanText = (title, description, options = {}) => {
1549
1700
  return result;
1550
1701
  }
1551
1702
  catch (error) {
1552
- finalizePlanningInvocation(invocation, 'error', plannerOutput);
1553
- throw attachPlannerTranscriptRuns(error, transcriptRuns ?? null);
1703
+ const finalStatus = options.cancellationState?.stopRequested ? 'stopped' : 'error';
1704
+ finalizePlanningInvocation(invocation, finalStatus, plannerOutput);
1705
+ const finalTranscriptRuns = transcriptRuns?.map(run => (run.id === invocation?.id ? { ...run, agent_status: finalStatus } : run)) ?? null;
1706
+ throw attachPlannerTranscriptRuns(error, finalTranscriptRuns);
1554
1707
  }
1555
1708
  })();
1556
1709
  };
@@ -1558,23 +1711,27 @@ export const generatePlanningTicketOutline = async (description, options = {}) =
1558
1711
  const tool = options.tool ?? config.tool ?? 'opencode';
1559
1712
  const project = options.projectId ? getProjectById(options.projectId) : null;
1560
1713
  const invocation = createPlanningInvocation(options, 'Planning Outline', 'planning-outline');
1561
- const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1562
- const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1563
- const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1564
- flow: 'plan',
1565
- stage: 'plan',
1566
- title: description,
1567
- description,
1568
- ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1569
- });
1570
- const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentOutlinePrompt(description, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch));
1571
1714
  try {
1715
+ const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1716
+ const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1717
+ const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1718
+ flow: 'plan',
1719
+ stage: 'plan',
1720
+ title: description,
1721
+ description,
1722
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.id } } : {}),
1723
+ });
1724
+ const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentOutlinePrompt(description, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch), {
1725
+ agentRunParent: resolvePlannerAgentRunParent(options),
1726
+ executionContext: options.executionContext,
1727
+ ...(invocation ? { mainRun: invocation } : {}),
1728
+ });
1572
1729
  const result = extractFirstJsonObject(output);
1573
1730
  finalizePlanningInvocation(invocation, 'done', output);
1574
1731
  return result;
1575
1732
  }
1576
1733
  catch (error) {
1577
- finalizePlanningInvocation(invocation, 'error', output);
1734
+ finalizePlanningInvocation(invocation, 'error', error instanceof Error ? error.message : String(error));
1578
1735
  throw error;
1579
1736
  }
1580
1737
  };
@@ -1582,23 +1739,27 @@ export const expandPlanningTicketOutline = async (description, outline, options
1582
1739
  const tool = options.tool ?? config.tool ?? 'opencode';
1583
1740
  const project = options.projectId ? getProjectById(options.projectId) : null;
1584
1741
  const invocation = createPlanningInvocation(options, 'Planning Expansion', 'planning-expansion');
1585
- const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1586
- const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1587
- const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1588
- flow: 'plan',
1589
- stage: 'plan',
1590
- title: description,
1591
- description,
1592
- ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1593
- });
1594
- const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentExpansionPrompt(description, outline, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch));
1595
1742
  try {
1743
+ const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1744
+ const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1745
+ const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1746
+ flow: 'plan',
1747
+ stage: 'plan',
1748
+ title: description,
1749
+ description,
1750
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.id } } : {}),
1751
+ });
1752
+ const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentExpansionPrompt(description, outline, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch), {
1753
+ agentRunParent: resolvePlannerAgentRunParent(options),
1754
+ executionContext: options.executionContext,
1755
+ ...(invocation ? { mainRun: invocation } : {}),
1756
+ });
1596
1757
  const result = extractFirstJsonObject(output);
1597
1758
  finalizePlanningInvocation(invocation, 'done', output);
1598
1759
  return result;
1599
1760
  }
1600
1761
  catch (error) {
1601
- finalizePlanningInvocation(invocation, 'error', output);
1762
+ finalizePlanningInvocation(invocation, 'error', error instanceof Error ? error.message : String(error));
1602
1763
  throw error;
1603
1764
  }
1604
1765
  };
@@ -1612,114 +1773,292 @@ const updatePlanningProgress = (ticketId, log, response, io) => {
1612
1773
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('running', log ?? null, response ?? null, ticketId);
1613
1774
  emitTicketUpdate(ticketId, io);
1614
1775
  };
1776
+ const appendPlanResultLink = (executionContext, ticketId, messageId) => {
1777
+ addExecutionRunLink({
1778
+ projectId: executionContext.projectId,
1779
+ runId: executionContext.runId,
1780
+ link: {
1781
+ entityType: 'ticket_message',
1782
+ entityId: messageId,
1783
+ relationship: 'result',
1784
+ expectedParent: { entityType: 'ticket', entityId: ticketId },
1785
+ },
1786
+ });
1787
+ };
1788
+ const persistPlanCancellationMessage = (ticketId, executionContext, transcriptRuns) => {
1789
+ const messageId = createTicketMessage({
1790
+ ticketId,
1791
+ role: 'assistant',
1792
+ kind: 'error',
1793
+ content: 'Planning was stopped.',
1794
+ transcriptRuns,
1795
+ });
1796
+ appendPlanResultLink(executionContext, ticketId, messageId);
1797
+ };
1615
1798
  export const startPlan = (ticket, io, options = {}) => {
1616
1799
  const jobKey = resolveTicketWorkerKey(ticket, 'plan');
1617
1800
  if (!jobKey || isRunning(jobKey)) {
1618
- return;
1619
- }
1620
- enqueue(jobKey, async () => {
1621
- registerBundleActivity(ticket, 'plan');
1622
- beginTicketThinkingRun(ticket.id);
1623
- db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('running', ticket.id);
1624
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1625
- emitTicketUpdate(ticket.id, io);
1626
- try {
1627
- const project = getEffectiveProject(ticket);
1628
- const branch = options.branch === undefined ? resolveTicketBranch(ticket) : options.branch;
1629
- const planningDescription = await resolveExternalPlanningDescription(project?.id ?? ticket.project_id, ticket.description);
1630
- const initiativePrompt = ticket.planning_context?.initiative_prompt?.trim() || planningDescription;
1631
- const planned = await generatePlanText(ticket.title, planningDescription, {
1632
- tool: resolveTicketTool(ticket, config.tool ?? 'opencode'),
1633
- model: ticket.model,
1634
- variant: ticket.variant,
1635
- skills: options.skills ?? ticket.skills ?? [],
1636
- projectId: project?.id ?? ticket.project_id,
1637
- branch,
1638
- planningSessionId: ticket.planning_session_id,
1639
- ticketId: ticket.id,
1640
- io,
1641
- onProgress: progress => {
1642
- updatePlanningProgress(ticket.id, progress.log, progress.response, io);
1643
- },
1644
- });
1645
- const resolvedTool = resolveTicketTool(ticket, config.tool ?? 'opencode');
1646
- if (resolvedTool === 'opencode') {
1647
- const freshTicket = getTicket(ticket.id);
1648
- if (freshTicket?.planning_session_id) {
1649
- storeTicketUsageSnapshot(freshTicket.id, freshTicket.planning_session_id);
1801
+ return { accepted: false, reason: 'busy', error: 'Ticket planning is already scheduled or running' };
1802
+ }
1803
+ const project = getEffectiveProject(ticket);
1804
+ if (!project || project.id !== ticket.project_id) {
1805
+ return { accepted: false, reason: 'project_unavailable', error: 'Ticket project is unavailable' };
1806
+ }
1807
+ const executionRun = createExecutionRun({
1808
+ projectId: project.id,
1809
+ kind: 'plan',
1810
+ domainStatus: 'queued',
1811
+ sourceReference: { kind: 'ticket', id: ticket.id, label: ticket.title },
1812
+ eventType: 'plan_created',
1813
+ }, io);
1814
+ const executionContext = {
1815
+ runId: executionRun.id,
1816
+ correlationId: executionRun.correlation_id,
1817
+ projectId: executionRun.project_id,
1818
+ kind: executionRun.kind,
1819
+ };
1820
+ const cancellationState = { researchChildren: new Set(), stopRequested: false };
1821
+ activePlanProcesses.set(ticket.id, cancellationState);
1822
+ let queuedJob;
1823
+ try {
1824
+ queuedJob = enqueue(jobKey, async () => {
1825
+ let callbackStarted = false;
1826
+ let activityRegistered = false;
1827
+ try {
1828
+ registerBundleActivity(ticket, 'plan');
1829
+ activityRegistered = true;
1830
+ transitionExecutionRun({
1831
+ projectId: executionContext.projectId,
1832
+ runId: executionContext.runId,
1833
+ status: 'running',
1834
+ domainStatus: 'running',
1835
+ eventType: 'plan_started',
1836
+ }, io);
1837
+ const currentTicket = getTicket(ticket.id);
1838
+ if (!currentTicket || currentTicket.project_id !== executionContext.projectId) {
1839
+ throw new Error('Ticket no longer exists in the execution project');
1650
1840
  }
1651
- }
1652
- else if (resolvedTool === 'claude' && planned.usageSnapshot) {
1653
- const freshTicket = getTicket(ticket.id);
1654
- if (freshTicket) {
1655
- storeTicketUsageSnapshotDirect(freshTicket.id, planned.usageSnapshot);
1841
+ beginTicketThinkingRun(ticket.id);
1842
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('running', ticket.id);
1843
+ emitAgentRunsUpdated(io, 'ticket', ticket.id);
1844
+ emitTicketUpdate(ticket.id, io);
1845
+ callbackStarted = true;
1846
+ if (cancellationState.stopRequested) {
1847
+ persistPlanCancellationMessage(ticket.id, executionContext);
1848
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped');
1849
+ db.prepare('UPDATE tickets SET agent_status = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('stopped', ticket.id);
1850
+ emitTicketUpdate(ticket.id, io);
1851
+ transitionExecutionRun({
1852
+ projectId: executionContext.projectId,
1853
+ runId: executionContext.runId,
1854
+ status: 'cancelled',
1855
+ domainStatus: 'stopped',
1856
+ eventType: 'plan_cancelled',
1857
+ }, io);
1858
+ return;
1656
1859
  }
1657
- }
1658
- const planningContext = createPlanningContext({
1659
- sourceKind: 'single',
1660
- initiativePrompt,
1661
- relatedTicketIds: [ticket.id],
1662
- openQuestions: planned.open_questions,
1663
- });
1664
- db.prepare('UPDATE tickets SET description = ?, agent_status = ?, streaming_response = NULL, planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(planned.description, 'done', JSON.stringify(planningContext), ticket.id);
1665
- const suggestionProjectId = project?.id ?? ticket.project_id;
1666
- if (suggestionProjectId && Boolean(project?.memory_enabled)) {
1667
- persistProjectMemorySuggestions(suggestionProjectId, ticket.id, 'plan', planned.memory_suggestions);
1668
- }
1669
- upsertTicketDescriptionMessage(ticket.id, planned.description);
1670
- const assistantMessage = buildPlanningAssistantMessage({
1671
- response: planned.response,
1672
- openQuestions: planned.open_questions,
1673
- }) || (planned.transcriptRuns?.length ? planned.description : '');
1674
- if (assistantMessage) {
1675
- const messageId = createTicketMessage({
1860
+ const branch = options.branch === undefined ? resolveTicketBranch(ticket) : options.branch;
1861
+ const planningDescription = await resolveExternalPlanningDescription(project.id, ticket.description);
1862
+ const initiativePrompt = ticket.planning_context?.initiative_prompt?.trim() || planningDescription;
1863
+ const planned = await generatePlanText(ticket.title, planningDescription, {
1864
+ tool: resolveTicketTool(ticket, config.tool ?? 'opencode'),
1865
+ model: ticket.model,
1866
+ variant: ticket.variant,
1867
+ skills: options.skills ?? ticket.skills ?? [],
1868
+ projectId: project.id,
1869
+ branch,
1870
+ planningSessionId: ticket.planning_session_id,
1676
1871
  ticketId: ticket.id,
1677
- role: 'assistant',
1678
- kind: 'planning_response',
1679
- content: assistantMessage,
1680
- responseTiming: planned.responseTiming,
1681
- transcriptRuns: planned.transcriptRuns,
1872
+ io,
1873
+ executionContext,
1874
+ cancellationState,
1875
+ onProgress: progress => {
1876
+ updatePlanningProgress(ticket.id, progress.log, progress.response, io);
1877
+ },
1682
1878
  });
1683
- if (messageId) {
1684
- bindTicketThinkingMessage(ticket.id, messageId);
1879
+ if (cancellationState.stopRequested || getTicket(ticket.id)?.agent_status === 'stopped') {
1880
+ persistPlanCancellationMessage(ticket.id, executionContext, planned.transcriptRuns);
1881
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped');
1882
+ db.prepare('UPDATE tickets SET agent_status = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('stopped', ticket.id);
1883
+ emitTicketUpdate(ticket.id, io);
1884
+ transitionExecutionRun({
1885
+ projectId: executionContext.projectId,
1886
+ runId: executionContext.runId,
1887
+ status: 'cancelled',
1888
+ domainStatus: 'stopped',
1889
+ eventType: 'plan_cancelled',
1890
+ }, io);
1891
+ return;
1685
1892
  }
1893
+ const resolvedTool = resolveTicketTool(ticket, config.tool ?? 'opencode');
1894
+ if (resolvedTool === 'opencode') {
1895
+ const freshTicket = getTicket(ticket.id);
1896
+ if (freshTicket?.planning_session_id) {
1897
+ storeTicketUsageSnapshot(freshTicket.id, freshTicket.planning_session_id);
1898
+ }
1899
+ }
1900
+ else if (resolvedTool === 'claude' && planned.usageSnapshot) {
1901
+ const freshTicket = getTicket(ticket.id);
1902
+ if (freshTicket) {
1903
+ storeTicketUsageSnapshotDirect(freshTicket.id, planned.usageSnapshot);
1904
+ }
1905
+ }
1906
+ const planningContext = createPlanningContext({
1907
+ sourceKind: 'single',
1908
+ initiativePrompt,
1909
+ relatedTicketIds: [ticket.id],
1910
+ openQuestions: planned.open_questions,
1911
+ });
1912
+ db.prepare('UPDATE tickets SET description = ?, agent_status = ?, streaming_response = NULL, planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(planned.description, 'done', JSON.stringify(planningContext), ticket.id);
1913
+ if (project.memory_enabled) {
1914
+ persistProjectMemorySuggestions(project.id, ticket.id, 'plan', planned.memory_suggestions);
1915
+ }
1916
+ upsertTicketDescriptionMessage(ticket.id, planned.description);
1917
+ const assistantMessage = buildPlanningAssistantMessage({
1918
+ response: planned.response,
1919
+ openQuestions: planned.open_questions,
1920
+ }) || (planned.transcriptRuns?.length ? planned.description : '');
1921
+ if (assistantMessage) {
1922
+ const messageId = createTicketMessage({
1923
+ ticketId: ticket.id,
1924
+ role: 'assistant',
1925
+ kind: 'planning_response',
1926
+ content: assistantMessage,
1927
+ responseTiming: planned.responseTiming,
1928
+ transcriptRuns: planned.transcriptRuns,
1929
+ });
1930
+ if (messageId) {
1931
+ bindTicketThinkingMessage(ticket.id, messageId);
1932
+ appendPlanResultLink(executionContext, ticket.id, messageId);
1933
+ }
1934
+ }
1935
+ emitTicketUpdate(ticket.id, io);
1936
+ transitionExecutionRun({
1937
+ projectId: executionContext.projectId,
1938
+ runId: executionContext.runId,
1939
+ status: 'completed',
1940
+ domainStatus: 'done',
1941
+ eventType: 'plan_completed',
1942
+ }, io);
1686
1943
  }
1687
- if (planned.transcriptRuns) {
1688
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1689
- }
1690
- emitTicketUpdate(ticket.id, io);
1691
- }
1692
- catch (err) {
1693
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
1694
- const message = err instanceof PlanningReferenceError
1695
- ? err.message
1696
- : err instanceof JiraApiError
1697
- ? err.publicMessage
1698
- : err instanceof GitHubApiError
1944
+ catch (err) {
1945
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
1946
+ if (cancellationState.stopRequested || getTicket(ticket.id)?.agent_status === 'stopped') {
1947
+ try {
1948
+ persistPlanCancellationMessage(ticket.id, executionContext, transcriptRuns);
1949
+ }
1950
+ catch {
1951
+ // The ticket may have been deleted while planning was active.
1952
+ }
1953
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped');
1954
+ try {
1955
+ db.prepare('UPDATE tickets SET agent_status = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('stopped', ticket.id);
1956
+ emitTicketUpdate(ticket.id, io);
1957
+ }
1958
+ catch {
1959
+ // Preserve cancellation history when the domain row disappeared.
1960
+ }
1961
+ transitionExecutionRun({
1962
+ projectId: executionContext.projectId,
1963
+ runId: executionContext.runId,
1964
+ status: 'cancelled',
1965
+ domainStatus: 'stopped',
1966
+ eventType: 'plan_cancelled',
1967
+ }, io);
1968
+ return;
1969
+ }
1970
+ const message = err instanceof PlanningReferenceError
1971
+ ? err.message
1972
+ : err instanceof JiraApiError
1699
1973
  ? err.publicMessage
1700
- : err instanceof Error
1701
- ? err.message
1702
- : String(err);
1703
- const errorContent = formatAgentErrorForTranscript(err);
1704
- let errorMessageId = null;
1705
- if (errorContent) {
1706
- errorMessageId = createTicketMessage({
1707
- ticketId: ticket.id,
1708
- role: 'assistant',
1709
- kind: 'error',
1710
- content: errorContent,
1711
- transcriptRuns,
1712
- });
1974
+ : err instanceof GitHubApiError
1975
+ ? err.publicMessage
1976
+ : err instanceof Error
1977
+ ? err.message
1978
+ : String(err);
1979
+ const errorContent = formatAgentErrorForTranscript(err);
1980
+ let errorMessageId = null;
1981
+ try {
1982
+ if (errorContent) {
1983
+ errorMessageId = createTicketMessage({
1984
+ ticketId: ticket.id,
1985
+ role: 'assistant',
1986
+ kind: 'error',
1987
+ content: errorContent,
1988
+ transcriptRuns,
1989
+ });
1990
+ }
1991
+ if (errorMessageId) {
1992
+ appendPlanResultLink(executionContext, ticket.id, errorMessageId);
1993
+ }
1994
+ }
1995
+ catch {
1996
+ // A missing ticket must not prevent the durable execution from terminalizing.
1997
+ }
1998
+ try {
1999
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('error', message, ticket.id);
2000
+ emitTicketUpdate(ticket.id, io);
2001
+ }
2002
+ catch {
2003
+ // Preserve execution failure history when the ticket row disappeared.
2004
+ }
2005
+ try {
2006
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
2007
+ }
2008
+ catch {
2009
+ // Top-level terminalization remains authoritative if detail finalization fails.
2010
+ }
2011
+ transitionExecutionRun({
2012
+ projectId: executionContext.projectId,
2013
+ runId: executionContext.runId,
2014
+ status: 'failed',
2015
+ domainStatus: 'error',
2016
+ failure: { code: callbackStarted ? 'plan_failed' : 'queue_failed', message },
2017
+ eventType: 'plan_failed',
2018
+ }, io);
1713
2019
  }
1714
- if (transcriptRuns && errorMessageId) {
1715
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
2020
+ finally {
2021
+ if (activityRegistered) {
2022
+ unregisterBundleActivity(ticket, 'plan');
2023
+ }
2024
+ if (activePlanProcesses.get(ticket.id) === cancellationState) {
2025
+ activePlanProcesses.delete(ticket.id);
2026
+ }
1716
2027
  }
1717
- db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('error', message, ticket.id);
1718
- emitTicketUpdate(ticket.id, io);
1719
- }
1720
- finally {
1721
- unregisterBundleActivity(ticket, 'plan');
1722
- }
2028
+ });
2029
+ }
2030
+ catch (error) {
2031
+ activePlanProcesses.delete(ticket.id);
2032
+ const message = error instanceof Error ? error.message : String(error);
2033
+ transitionExecutionRun({
2034
+ projectId: executionContext.projectId,
2035
+ runId: executionContext.runId,
2036
+ status: 'failed',
2037
+ domainStatus: ticket.agent_status ?? 'idle',
2038
+ failure: { code: 'queue_failed', message },
2039
+ eventType: 'plan_failed',
2040
+ }, io);
2041
+ return { accepted: false, reason: 'queue_failed', error: message };
2042
+ }
2043
+ queuedJob.catch(() => {
2044
+ // The queued callback owns domain and execution failure finalization.
1723
2045
  });
2046
+ return { accepted: true, executionContext };
2047
+ };
2048
+ export const stopPlan = async (ticketId) => {
2049
+ const state = activePlanProcesses.get(ticketId);
2050
+ if (!state) {
2051
+ return false;
2052
+ }
2053
+ state.stopRequested = true;
2054
+ const children = [...state.researchChildren];
2055
+ await stopPlannerProcesses(children);
2056
+ return true;
2057
+ };
2058
+ export const stopAllPlanAgents = async () => {
2059
+ for (const state of activePlanProcesses.values()) {
2060
+ state.stopRequested = true;
2061
+ }
2062
+ await stopPlannerProcesses([...activePlannerProcesses]);
1724
2063
  };
1725
2064
  //# sourceMappingURL=plan.js.map