@straiffi/archon 1.4.12 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/client/assets/{GitHubActionsDialog-DcW7fUgz.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
  2. package/dist/client/assets/{TestsDialog-xNEttQOx.js → TestsDialog-elElGi9L.js} +3 -4
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DHADo63Y.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-D_LqGLuD.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-mIqv7fCU.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
  6. package/dist/client/assets/channel-DAW1OrrC.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DP-b68f-.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-D4GWQbqc.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-HftAkT9j.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-CvB0JQZP.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-DSB28EAb.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-DuSgBocb.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-cm_ppJ7v.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-BzDHIV9v.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-BruMwvko.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-DrnvodkL.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-QVsa0LuF.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-DJFhq2gr.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-ieLjE2uQ.js → chunk-WU5MYG2G-DBvmvPPt.js} +1 -1
  20. package/dist/client/assets/classDiagram-4FO5ZUOK-BmFh-n6n.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tA1aAbh4.js +1 -0
  22. package/dist/client/assets/{dagre-BM42HDAG-CFRh82aT.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-CPk1WJvx.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-DHMJ735r.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-Cq9d2BWb.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CgWJTR_b.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-HoFYGw81.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-B7yuPZck.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DaXXRHQj.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-pKsFmtHB.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-Dwx5kc1M.js → gitGraphDiagram-PVQCEYII-DEAsC0GZ.js} +1 -1
  32. package/dist/client/assets/index-BzDXl4AF.js +265 -0
  33. package/dist/client/assets/index-DSap5tSX.css +2 -0
  34. package/dist/client/assets/{infoDiagram-5YYISTIA-6Taupp-l.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BBKjiFtW.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQQ1EKJ3.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-BvsroFzw.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
  38. package/dist/client/assets/{line-C6BHsie7.js → line-B1SFN8mz.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-Cb4G8u3H.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-NCO8Z4wR.js → mermaid.core-nm4BMZuF.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DYzSjAq3.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-CWMyFFuU.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D1ifIucc.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DW68fC21.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DnnV47BL.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dw-9ZBU_.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-C7dkf8nk.js → stateDiagram-AJRCARHV-CrgZ4Uwa.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CUEBQOsD.js +1 -0
  49. package/dist/client/assets/{timeline-definition-PNZ67QCA-BmVfK_16.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-B9j3QbmD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C520auMy.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPqUJuqs.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +291 -3
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +339 -97
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/agent.js +853 -161
  59. package/dist/server/lib/agent.js.map +1 -1
  60. package/dist/server/lib/agentRuns.js +22 -0
  61. package/dist/server/lib/agentRuns.js.map +1 -1
  62. package/dist/server/lib/chatMessages.js +24 -8
  63. package/dist/server/lib/chatMessages.js.map +1 -1
  64. package/dist/server/lib/genericToolCall.js +151 -0
  65. package/dist/server/lib/genericToolCall.js.map +1 -0
  66. package/dist/server/lib/git.js +18 -0
  67. package/dist/server/lib/git.js.map +1 -1
  68. package/dist/server/lib/mobileAccess.js +45 -2
  69. package/dist/server/lib/mobileAccess.js.map +1 -1
  70. package/dist/server/lib/models.js +65 -48
  71. package/dist/server/lib/models.js.map +1 -1
  72. package/dist/server/lib/opencodeTaskSessions.js +216 -0
  73. package/dist/server/lib/opencodeTaskSessions.js.map +1 -0
  74. package/dist/server/lib/projectMemory.js +1073 -276
  75. package/dist/server/lib/projectMemory.js.map +1 -1
  76. package/dist/server/lib/projectMemoryCursor.js +27 -0
  77. package/dist/server/lib/projectMemoryCursor.js.map +1 -0
  78. package/dist/server/lib/projectMemoryEvents.js +24 -0
  79. package/dist/server/lib/projectMemoryEvents.js.map +1 -0
  80. package/dist/server/lib/projectMemoryLimits.js +61 -0
  81. package/dist/server/lib/projectMemoryLimits.js.map +1 -0
  82. package/dist/server/lib/projectMemoryPrompt.js +299 -110
  83. package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
  84. package/dist/server/lib/projectMemoryScan.js +461 -493
  85. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  86. package/dist/server/lib/projectMemorySelections.js +97 -0
  87. package/dist/server/lib/projectMemorySelections.js.map +1 -0
  88. package/dist/server/lib/projectMemorySuggestions.js +19 -4
  89. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  90. package/dist/server/lib/projectMemoryTransfer.js +635 -83
  91. package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
  92. package/dist/server/lib/projectPathGlob.js +40 -0
  93. package/dist/server/lib/projectPathGlob.js.map +1 -0
  94. package/dist/server/lib/projects.js +50 -0
  95. package/dist/server/lib/projects.js.map +1 -1
  96. package/dist/server/lib/promptSkills.js +5 -0
  97. package/dist/server/lib/promptSkills.js.map +1 -1
  98. package/dist/server/lib/specMessages.js +9 -6
  99. package/dist/server/lib/specMessages.js.map +1 -1
  100. package/dist/server/lib/specReviewContext.js +2 -1
  101. package/dist/server/lib/specReviewContext.js.map +1 -1
  102. package/dist/server/lib/ticketMessages.js +22 -7
  103. package/dist/server/lib/ticketMessages.js.map +1 -1
  104. package/dist/server/lib/tickets.js +6 -0
  105. package/dist/server/lib/tickets.js.map +1 -1
  106. package/dist/server/lib/transcriptRuns.js +85 -0
  107. package/dist/server/lib/transcriptRuns.js.map +1 -0
  108. package/dist/server/workers/build.js +18 -4
  109. package/dist/server/workers/build.js.map +1 -1
  110. package/dist/server/workers/chat.js +24 -7
  111. package/dist/server/workers/chat.js.map +1 -1
  112. package/dist/server/workers/followUp.js +27 -10
  113. package/dist/server/workers/followUp.js.map +1 -1
  114. package/dist/server/workers/plan.js +405 -69
  115. package/dist/server/workers/plan.js.map +1 -1
  116. package/dist/server/workers/planFollowUp.js +30 -7
  117. package/dist/server/workers/planFollowUp.js.map +1 -1
  118. package/dist/server/workers/review.js +76 -20
  119. package/dist/server/workers/review.js.map +1 -1
  120. package/dist/server/workers/specReview.js +10 -0
  121. package/dist/server/workers/specReview.js.map +1 -1
  122. package/dist/server/workers/specSession.js +37 -3
  123. package/dist/server/workers/specSession.js.map +1 -1
  124. package/package.json +1 -1
  125. package/dist/client/assets/channel-D7PWuiXU.js +0 -1
  126. package/dist/client/assets/classDiagram-4FO5ZUOK-BNIT931s.js +0 -1
  127. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-TRtc0GGS.js +0 -1
  128. package/dist/client/assets/index-5Eun897r.js +0 -260
  129. package/dist/client/assets/index-CWWEw76C.css +0 -2
  130. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BFmoroJA.js +0 -1
@@ -6,13 +6,64 @@ import Database from 'better-sqlite3';
6
6
  import db from '../db.js';
7
7
  import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
8
8
  import { normalizeClaudeModelId, normalizeModelId } from './models.js';
9
+ import { createOpencodeTaskSessionReader, parseOpencodeTaskEnvelope } from './opencodeTaskSessions.js';
9
10
  import { PLAN_MODE_QUESTION_TOOL_NAME } from './planModeQuestions.js';
11
+ import { formatGenericToolCall } from './genericToolCall.js';
10
12
  import { getSpecSession, updateSpecSessionResumeId, updateSpecSessionStatus, updateSpecSessionTitle, updateSpecSessionUsageSnapshot } from './specs.js';
11
13
  import { updateSpecReviewRunStatus, updateSpecReviewRunStreaming, updateSpecReviewRunUsage } from './specReviews.js';
12
- import { emitAgentRunsUpdated, finalizeAgentRun, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
14
+ import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
13
15
  import { resolveOpencodeDbPath } from './runtimePaths.js';
14
16
  import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
15
17
  import { getTicket } from './tickets.js';
18
+ import { createTranscriptRunSnapshots } from './transcriptRuns.js';
19
+ const withTranscriptRuns = (value, transcriptRuns) => {
20
+ return transcriptRuns ? { ...value, transcriptRuns } : value;
21
+ };
22
+ const createOpencodeRecoveryTracker = () => {
23
+ let sawError = false;
24
+ let latestEvent = null;
25
+ let recoveredFinalAnswerFragments = [];
26
+ return {
27
+ consume: events => {
28
+ let shouldResetResponse = false;
29
+ for (const event of events ?? []) {
30
+ latestEvent = event.type;
31
+ if (event.type === 'error') {
32
+ sawError = true;
33
+ recoveredFinalAnswerFragments = [];
34
+ shouldResetResponse = true;
35
+ continue;
36
+ }
37
+ if (sawError) {
38
+ recoveredFinalAnswerFragments.push(event.text);
39
+ }
40
+ }
41
+ return shouldResetResponse;
42
+ },
43
+ hasRecoveredFinalAnswer: () => (sawError
44
+ && latestEvent === 'final_answer'
45
+ && recoveredFinalAnswerFragments.length > 0),
46
+ hasUnrecoveredError: () => latestEvent === 'error',
47
+ getRecoveredResponse: () => recoveredFinalAnswerFragments.join('\n\n'),
48
+ };
49
+ };
50
+ const isSuccessfulOpencodeExit = (code, modelError, recoveryTracker) => (!modelError
51
+ && !recoveryTracker.hasUnrecoveredError()
52
+ && (code === 0 || recoveryTracker.hasRecoveredFinalAnswer()));
53
+ const attachTranscriptRunsToError = (error, transcriptRuns) => {
54
+ const resolvedError = (error instanceof Error ? error : new Error(String(error)));
55
+ if (transcriptRuns) {
56
+ resolvedError.transcriptRuns = transcriptRuns;
57
+ }
58
+ return resolvedError;
59
+ };
60
+ export const getAgentErrorTranscriptRuns = (error) => {
61
+ if (!error || typeof error !== 'object' || !('transcriptRuns' in error)) {
62
+ return null;
63
+ }
64
+ const transcriptRuns = error.transcriptRuns;
65
+ return Array.isArray(transcriptRuns) && transcriptRuns.length > 0 ? transcriptRuns : null;
66
+ };
16
67
  const OPENCODE_PLACEHOLDER_TITLE_PATTERN = /^New session\b/i;
17
68
  const OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS = 1500;
18
69
  const AGENT_PROGRESS_UPDATE_INTERVAL_MS = 100;
@@ -702,7 +753,6 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
702
753
  },
703
754
  };
704
755
  };
705
- const TOOL_CALL_PREVIEW_MAX_LENGTH = 100;
706
756
  const APPLY_PATCH_START_MARKER = '[[archon-apply-patch]]';
707
757
  const APPLY_PATCH_END_MARKER = '[[/archon-apply-patch]]';
708
758
  const normalizeToolName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -836,22 +886,6 @@ const formatWriteToolCall = (input, cwd) => {
836
886
  displayLines.push(APPLY_PATCH_END_MARKER);
837
887
  return displayLines.join('\n');
838
888
  };
839
- const pickGenericToolVal = (name, input) => {
840
- const explicitVal = {
841
- Read: input?.filePath ?? input?.file_path,
842
- Bash: input?.command,
843
- Glob: input?.pattern,
844
- Grep: input?.pattern,
845
- WebFetch: input?.url,
846
- WebSearch: input?.query,
847
- Task: input?.description,
848
- NotebookEdit: input?.notebook_path,
849
- }[name];
850
- if (typeof explicitVal === 'string' && explicitVal.length > 0) {
851
- return explicitVal;
852
- }
853
- return Object.values(input ?? {}).find(v => typeof v === 'string') ?? null;
854
- };
855
889
  const formatToolCall = (name, input, cwd) => {
856
890
  if (isTodoWriteTool(name)) {
857
891
  return formatTodoWriteToolCall(input);
@@ -865,40 +899,87 @@ const formatToolCall = (name, input, cwd) => {
865
899
  if (formatted !== null) {
866
900
  return formatted;
867
901
  }
868
- return genericToolCallLabel(name, input);
902
+ return formatGenericToolCall(name, input);
869
903
  }
870
904
  if (normalized === 'write') {
871
905
  const formatted = formatWriteToolCall(input, cwd);
872
906
  if (formatted !== null) {
873
907
  return formatted;
874
908
  }
875
- return genericToolCallLabel(name, input);
909
+ return formatGenericToolCall(name, input);
876
910
  }
877
- return genericToolCallLabel(name, input);
911
+ return formatGenericToolCall(name, input);
878
912
  };
879
- const genericToolCallLabel = (name, input) => {
880
- const val = pickGenericToolVal(name, input);
881
- if (typeof val !== 'string' || val.length === 0) {
882
- return `_${name}_`;
913
+ const isSubagentTool = (name) => {
914
+ const normalized = normalizeToolName(name);
915
+ return normalized === 'task' || normalized === 'subagent' || normalized === 'agent';
916
+ };
917
+ const getSubagentDescription = (input, fallback) => {
918
+ if (input) {
919
+ for (const key of ['description', 'title', 'task', 'subagent_type', 'prompt']) {
920
+ const value = input[key];
921
+ if (typeof value === 'string' && value.trim()) {
922
+ const firstLine = value.trim().split('\n')[0] ?? '';
923
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
924
+ }
925
+ }
883
926
  }
884
- const display = val.length > TOOL_CALL_PREVIEW_MAX_LENGTH ? val.slice(0, TOOL_CALL_PREVIEW_MAX_LENGTH) + '…' : val;
885
- return `_${name}_ \`${display}\``;
927
+ return fallback;
886
928
  };
929
+ const stringifyToolResult = (content) => {
930
+ if (typeof content === 'string') {
931
+ return content.trim() || null;
932
+ }
933
+ if (Array.isArray(content)) {
934
+ const text = content
935
+ .map(item => item && typeof item === 'object' && typeof item.text === 'string'
936
+ ? item.text.trim()
937
+ : '')
938
+ .filter(Boolean)
939
+ .join('\n\n');
940
+ return text || null;
941
+ }
942
+ return content === null || content === undefined ? null : JSON.stringify(content);
943
+ };
944
+ const getSubagentCompletionStatus = (state, isError = false) => {
945
+ const normalizedState = state?.toLowerCase();
946
+ return isError || normalizedState === 'failed' || normalizedState === 'error' ? 'error' : 'done';
947
+ };
948
+ const emptyParsedStreamLine = () => ({
949
+ logFragment: null,
950
+ responseFragment: null,
951
+ subagentEvents: [],
952
+ providerSessionId: null,
953
+ });
887
954
  const parseClaudeStreamJsonLine = (line, cwd) => {
888
955
  const trimmed = line.trim();
889
956
  if (!trimmed) {
890
- return { logFragment: null, responseFragment: null };
957
+ return emptyParsedStreamLine();
891
958
  }
892
959
  let event;
893
960
  try {
894
961
  event = JSON.parse(trimmed);
895
962
  }
896
963
  catch {
897
- return { logFragment: trimmed, responseFragment: trimmed };
964
+ const envelope = parseOpencodeTaskEnvelope(trimmed);
965
+ if (envelope) {
966
+ return {
967
+ ...emptyParsedStreamLine(),
968
+ subagentEvents: [{
969
+ kind: 'complete',
970
+ callId: null,
971
+ sessionId: envelope.childSessionId,
972
+ status: getSubagentCompletionStatus(envelope.state),
973
+ ...(envelope.result ? { text: envelope.result } : {}),
974
+ }],
975
+ };
976
+ }
977
+ return { logFragment: trimmed, responseFragment: trimmed, subagentEvents: [], providerSessionId: null };
898
978
  }
899
979
  if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
900
980
  const logParts = [];
901
981
  const responseParts = [];
982
+ const subagentEvents = [];
902
983
  for (const block of event.message.content) {
903
984
  if (block.type === 'text' && block.text) {
904
985
  const text = block.text.trim();
@@ -908,6 +989,16 @@ const parseClaudeStreamJsonLine = (line, cwd) => {
908
989
  }
909
990
  }
910
991
  else if (block.type === 'tool_use' && block.name) {
992
+ if (isSubagentTool(block.name)) {
993
+ const description = getSubagentDescription(block.input, block.name);
994
+ logParts.push(`Subagent: ${description}`);
995
+ subagentEvents.push({
996
+ kind: 'start',
997
+ callId: block.id ?? null,
998
+ label: `Subagent: ${description}`,
999
+ });
1000
+ continue;
1001
+ }
911
1002
  const toolFragment = formatToolCall(block.name, block.input, cwd);
912
1003
  if (toolFragment) {
913
1004
  logParts.push(toolFragment);
@@ -917,9 +1008,44 @@ const parseClaudeStreamJsonLine = (line, cwd) => {
917
1008
  return {
918
1009
  logFragment: logParts.length > 0 ? logParts.join('\n\n') : null,
919
1010
  responseFragment: responseParts.length > 0 ? responseParts.join('\n\n') : null,
1011
+ subagentEvents,
1012
+ providerSessionId: null,
920
1013
  };
921
1014
  }
922
- return { logFragment: null, responseFragment: null };
1015
+ if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
1016
+ const block = event.event.content_block;
1017
+ if (block?.type === 'tool_use' && block.name && isSubagentTool(block.name)) {
1018
+ const description = getSubagentDescription(block.input, block.name);
1019
+ return {
1020
+ ...emptyParsedStreamLine(),
1021
+ logFragment: `Subagent: ${description}`,
1022
+ subagentEvents: [{
1023
+ kind: 'start',
1024
+ callId: block.id ?? null,
1025
+ label: `Subagent: ${description}`,
1026
+ }],
1027
+ };
1028
+ }
1029
+ }
1030
+ if (event.type === 'user' && Array.isArray(event.message?.content)) {
1031
+ const subagentEvents = event.message.content.flatMap(block => {
1032
+ if (block.type !== 'tool_result' || !block.tool_use_id) {
1033
+ return [];
1034
+ }
1035
+ const rawText = stringifyToolResult(block.content);
1036
+ const envelope = parseOpencodeTaskEnvelope(rawText);
1037
+ const text = envelope?.result ?? rawText;
1038
+ return [{
1039
+ kind: 'complete',
1040
+ callId: block.tool_use_id,
1041
+ status: getSubagentCompletionStatus(envelope?.state, block.is_error),
1042
+ ...(text ? { text } : {}),
1043
+ ...(envelope ? { sessionId: envelope.childSessionId } : {}),
1044
+ }];
1045
+ });
1046
+ return { ...emptyParsedStreamLine(), subagentEvents };
1047
+ }
1048
+ return emptyParsedStreamLine();
923
1049
  };
924
1050
  const formatOpencodeToolUse = (event, cwd) => {
925
1051
  const toolName = event.part?.tool;
@@ -989,12 +1115,22 @@ const extractLeadingJsonObjects = (line) => {
989
1115
  };
990
1116
  };
991
1117
  const parseOpencodeJsonEvent = (event, cwd) => {
1118
+ const providerSessionId = event.sessionID?.trim()
1119
+ || event.sessionId?.trim()
1120
+ || event.part?.sessionID?.trim()
1121
+ || event.part?.sessionId?.trim()
1122
+ || null;
992
1123
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
993
1124
  const text = event.part.text.trim();
994
1125
  const phase = event.part.metadata?.openai?.phase;
995
1126
  return {
996
1127
  logFragment: text || null,
997
1128
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
1129
+ subagentEvents: [],
1130
+ providerSessionId,
1131
+ recoveryEvents: text && phase === 'final_answer'
1132
+ ? [{ type: 'final_answer', text }]
1133
+ : [],
998
1134
  };
999
1135
  }
1000
1136
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -1002,25 +1138,74 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1002
1138
  return {
1003
1139
  logFragment: text || null,
1004
1140
  responseFragment: null,
1141
+ subagentEvents: [],
1142
+ providerSessionId,
1005
1143
  };
1006
1144
  }
1007
- if (event.type === 'tool_use') {
1145
+ if (event.type === 'tool' || event.type === 'tool_use' || event.type === 'tool_result') {
1146
+ const isToolInvocation = event.type === 'tool' || event.type === 'tool_use';
1147
+ const toolName = event.part?.tool;
1148
+ if (toolName && isSubagentTool(toolName)) {
1149
+ const description = getSubagentDescription(event.part?.state?.input, toolName);
1150
+ const rawOutput = event.part?.state?.output?.trim();
1151
+ const envelope = parseOpencodeTaskEnvelope(rawOutput);
1152
+ const output = envelope ? envelope.result : rawOutput;
1153
+ const taskStatus = event.part?.state?.status?.toLowerCase() ?? envelope?.state?.toLowerCase();
1154
+ const outputKind = taskStatus === 'running' || taskStatus === 'pending' ? 'fragment' : 'complete';
1155
+ const completionStatus = taskStatus === 'error' || taskStatus === 'failed' ? 'error' : 'done';
1156
+ const childSessionId = envelope?.childSessionId
1157
+ ?? (typeof event.part?.state?.metadata?.sessionID === 'string' ? event.part.state.metadata.sessionID : null)
1158
+ ?? (typeof event.part?.state?.metadata?.sessionId === 'string' ? event.part.state.metadata.sessionId : null);
1159
+ return {
1160
+ logFragment: isToolInvocation ? `Subagent: ${description}` : null,
1161
+ responseFragment: null,
1162
+ subagentEvents: [
1163
+ ...(isToolInvocation
1164
+ ? [{
1165
+ kind: 'start',
1166
+ callId: event.part?.callID ?? null,
1167
+ label: `Subagent: ${description}`,
1168
+ sessionId: childSessionId,
1169
+ }]
1170
+ : []),
1171
+ ...(output
1172
+ ? [{
1173
+ kind: outputKind,
1174
+ callId: event.part?.callID ?? null,
1175
+ text: output,
1176
+ sessionId: childSessionId,
1177
+ status: completionStatus,
1178
+ }]
1179
+ : []),
1180
+ ],
1181
+ providerSessionId,
1182
+ };
1183
+ }
1184
+ if (!isToolInvocation) {
1185
+ return { ...emptyParsedStreamLine(), providerSessionId };
1186
+ }
1008
1187
  const toolFragment = formatOpencodeToolUse(event, cwd);
1009
1188
  return {
1010
1189
  logFragment: toolFragment,
1011
1190
  responseFragment: null,
1191
+ subagentEvents: [],
1192
+ providerSessionId,
1012
1193
  };
1013
1194
  }
1014
1195
  if (event.type === 'step_start') {
1015
1196
  return {
1016
1197
  logFragment: formatOpencodeStepTitle(event),
1017
1198
  responseFragment: null,
1199
+ subagentEvents: [],
1200
+ providerSessionId,
1018
1201
  };
1019
1202
  }
1020
1203
  if (event.type === 'step_finish') {
1021
1204
  return {
1022
1205
  logFragment: null,
1023
1206
  responseFragment: null,
1207
+ subagentEvents: [],
1208
+ providerSessionId,
1024
1209
  };
1025
1210
  }
1026
1211
  if (event.type === 'error') {
@@ -1032,29 +1217,46 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1032
1217
  || JSON.stringify(event.error))
1033
1218
  : '';
1034
1219
  const text = event.part?.text?.trim() || errorText || '';
1035
- if (text) {
1036
- return {
1037
- logFragment: `**Error:** ${text}`,
1038
- responseFragment: text,
1039
- };
1040
- }
1220
+ return {
1221
+ logFragment: text ? `**Error:** ${text}` : null,
1222
+ responseFragment: null,
1223
+ subagentEvents: [],
1224
+ providerSessionId,
1225
+ recoveryEvents: [{ type: 'error' }],
1226
+ };
1041
1227
  }
1042
- return { logFragment: null, responseFragment: null };
1228
+ return emptyParsedStreamLine();
1043
1229
  };
1044
1230
  const parseOpencodeJsonLine = (line, cwd) => {
1045
1231
  const trimmed = line.trim();
1046
1232
  if (!trimmed) {
1047
- return { logFragment: null, responseFragment: null };
1233
+ return emptyParsedStreamLine();
1048
1234
  }
1049
1235
  const { jsonObjects, remainder } = extractLeadingJsonObjects(trimmed);
1050
1236
  if (jsonObjects.length === 0) {
1051
1237
  if (trimmed.startsWith('{')) {
1052
- return { logFragment: null, responseFragment: null };
1238
+ return emptyParsedStreamLine();
1053
1239
  }
1054
- return { logFragment: trimmed, responseFragment: trimmed };
1240
+ const envelope = parseOpencodeTaskEnvelope(trimmed);
1241
+ if (envelope) {
1242
+ return {
1243
+ ...emptyParsedStreamLine(),
1244
+ subagentEvents: [{
1245
+ kind: 'complete',
1246
+ callId: null,
1247
+ sessionId: envelope.childSessionId,
1248
+ status: getSubagentCompletionStatus(envelope.state),
1249
+ ...(envelope.result ? { text: envelope.result } : {}),
1250
+ }],
1251
+ };
1252
+ }
1253
+ return { logFragment: trimmed, responseFragment: trimmed, subagentEvents: [], providerSessionId: null };
1055
1254
  }
1056
1255
  const logFragments = [];
1057
1256
  const responseFragments = [];
1257
+ const subagentEvents = [];
1258
+ const recoveryEvents = [];
1259
+ let providerSessionId = null;
1058
1260
  for (const jsonObject of jsonObjects) {
1059
1261
  let event;
1060
1262
  try {
@@ -1063,7 +1265,11 @@ const parseOpencodeJsonLine = (line, cwd) => {
1063
1265
  catch {
1064
1266
  continue;
1065
1267
  }
1066
- const { logFragment, responseFragment } = parseOpencodeJsonEvent(event, cwd);
1268
+ const parsedEvent = parseOpencodeJsonEvent(event, cwd);
1269
+ const { logFragment, responseFragment } = parsedEvent;
1270
+ subagentEvents.push(...parsedEvent.subagentEvents);
1271
+ recoveryEvents.push(...(parsedEvent.recoveryEvents ?? []));
1272
+ providerSessionId = providerSessionId ?? parsedEvent.providerSessionId;
1067
1273
  if (logFragment !== null) {
1068
1274
  logFragments.push(logFragment);
1069
1275
  }
@@ -1071,16 +1277,386 @@ const parseOpencodeJsonLine = (line, cwd) => {
1071
1277
  responseFragments.push(responseFragment);
1072
1278
  }
1073
1279
  }
1074
- if (logFragments.length > 0 || responseFragments.length > 0) {
1280
+ if (remainder) {
1281
+ const envelope = parseOpencodeTaskEnvelope(remainder);
1282
+ if (envelope) {
1283
+ subagentEvents.push({
1284
+ kind: 'complete',
1285
+ callId: null,
1286
+ sessionId: envelope.childSessionId,
1287
+ status: getSubagentCompletionStatus(envelope.state),
1288
+ ...(envelope.result ? { text: envelope.result } : {}),
1289
+ });
1290
+ }
1291
+ else if (logFragments.length === 0 && responseFragments.length === 0 && subagentEvents.length === 0) {
1292
+ logFragments.push(remainder);
1293
+ responseFragments.push(remainder);
1294
+ }
1295
+ }
1296
+ if (logFragments.length > 0 || responseFragments.length > 0 || subagentEvents.length > 0) {
1075
1297
  return {
1076
1298
  logFragment: logFragments.length > 0 ? logFragments.join('\n\n') : null,
1077
1299
  responseFragment: responseFragments.length > 0 ? responseFragments.join('\n\n') : null,
1300
+ subagentEvents,
1301
+ providerSessionId,
1302
+ recoveryEvents,
1078
1303
  };
1079
1304
  }
1080
- if (remainder) {
1081
- return { logFragment: remainder, responseFragment: remainder };
1305
+ return emptyParsedStreamLine();
1306
+ };
1307
+ const appendTranscriptText = (current, fragment) => {
1308
+ const trimmed = fragment.trim();
1309
+ if (!trimmed) {
1310
+ return current;
1311
+ }
1312
+ return current ? `${current}\n\n${trimmed}` : trimmed;
1313
+ };
1314
+ const appendOpencodeTranscriptLine = ({ parsed, logLines, responseLines, recoveryTracker, }) => {
1315
+ const shouldResetResponse = recoveryTracker.consume(parsed.recoveryEvents);
1316
+ if (shouldResetResponse) {
1317
+ responseLines.length = 0;
1318
+ }
1319
+ if (parsed.logFragment !== null) {
1320
+ logLines.push(parsed.logFragment);
1321
+ }
1322
+ if (parsed.responseFragment !== null) {
1323
+ responseLines.push(parsed.responseFragment);
1324
+ }
1325
+ return shouldResetResponse;
1326
+ };
1327
+ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, }) => {
1328
+ const trackedRuns = [];
1329
+ let nextRunIndex = Math.max(getNextAgentRunIndex(parentType, parentId), mainRun ? mainRun.runIndex + 1 : 1);
1330
+ const emitUpdate = () => {
1331
+ emitAgentRunsUpdated(io, parentType, parentId);
1332
+ };
1333
+ const findRun = (callId, sessionId) => {
1334
+ if (callId) {
1335
+ const callRun = trackedRuns.find(run => run.callId === callId);
1336
+ if (callRun) {
1337
+ return callRun;
1338
+ }
1339
+ return sessionId ? trackedRuns.find(run => run.sessionId === sessionId) ?? null : null;
1340
+ }
1341
+ if (sessionId) {
1342
+ const sessionRun = trackedRuns.find(run => run.sessionId === sessionId);
1343
+ if (sessionRun) {
1344
+ return sessionRun;
1345
+ }
1346
+ }
1347
+ return [...trackedRuns].reverse().find(run => run.status === 'running') ?? null;
1348
+ };
1349
+ const bindSessionId = (run, sessionId) => {
1350
+ const normalized = sessionId?.trim() || null;
1351
+ if (!normalized || normalized === run.sessionId) {
1352
+ return;
1353
+ }
1354
+ run.sessionId = normalized;
1355
+ updateAgentRunSessionId(run.id, normalized);
1356
+ };
1357
+ const updateLabel = (run, label) => {
1358
+ const normalized = label?.trim() || null;
1359
+ if (!normalized || normalized === run.label) {
1360
+ return;
1361
+ }
1362
+ run.label = normalized;
1363
+ updateAgentRunLabel(run.id, normalized);
1364
+ };
1365
+ const handle = (events) => {
1366
+ for (const event of events) {
1367
+ if (event.kind === 'start') {
1368
+ const existingRun = event.callId || event.sessionId ? findRun(event.callId, event.sessionId) : null;
1369
+ if (existingRun) {
1370
+ bindSessionId(existingRun, event.sessionId);
1371
+ updateLabel(existingRun, event.label);
1372
+ emitUpdate();
1373
+ continue;
1374
+ }
1375
+ const runIndex = Math.max(getNextAgentRunIndex(parentType, parentId), nextRunIndex);
1376
+ const id = createAgentRun({
1377
+ parentType,
1378
+ parentId,
1379
+ runIndex,
1380
+ label: event.label?.trim() || 'Subagent',
1381
+ role: 'subagent',
1382
+ });
1383
+ nextRunIndex = runIndex + 1;
1384
+ const run = {
1385
+ id,
1386
+ callId: event.callId,
1387
+ runIndex,
1388
+ log: '',
1389
+ response: '',
1390
+ status: 'running',
1391
+ sessionId: null,
1392
+ label: event.label?.trim() || 'Subagent',
1393
+ createdAt: new Date().toISOString(),
1394
+ };
1395
+ trackedRuns.push(run);
1396
+ bindSessionId(run, event.sessionId);
1397
+ emitUpdate();
1398
+ continue;
1399
+ }
1400
+ let run = findRun(event.callId, event.sessionId);
1401
+ if (!run && event.kind === 'complete' && !event.callId) {
1402
+ handle([{
1403
+ kind: 'start',
1404
+ callId: event.callId,
1405
+ label: 'Subagent',
1406
+ sessionId: event.sessionId,
1407
+ }]);
1408
+ run = findRun(event.callId, event.sessionId);
1409
+ }
1410
+ if (!run || run.status !== 'running') {
1411
+ continue;
1412
+ }
1413
+ bindSessionId(run, event.sessionId);
1414
+ if (event.text) {
1415
+ if (event.kind === 'fragment') {
1416
+ run.log = appendTranscriptText(run.log, event.text);
1417
+ }
1418
+ else {
1419
+ run.response = appendTranscriptText(run.response, event.text);
1420
+ }
1421
+ }
1422
+ if (event.kind === 'complete') {
1423
+ const completionStatus = event.status ?? 'done';
1424
+ run.status = completionStatus;
1425
+ updateAgentRunStatus(run.id, 'running', run.log || null, run.response || null);
1426
+ finalizeAgentRun(run.id, completionStatus, run.response || null);
1427
+ }
1428
+ else {
1429
+ updateAgentRunStatus(run.id, 'running', run.log || null, run.response || null);
1430
+ }
1431
+ emitUpdate();
1432
+ }
1433
+ };
1434
+ const applySnapshot = ({ callId, label, status, log, response, sessionId, }) => {
1435
+ let run = findRun(callId);
1436
+ if (!run) {
1437
+ handle([{ kind: 'start', callId, label, sessionId }]);
1438
+ run = findRun(callId);
1439
+ }
1440
+ if (!run) {
1441
+ return;
1442
+ }
1443
+ bindSessionId(run, sessionId);
1444
+ updateLabel(run, label);
1445
+ const nextLog = log?.trim() ?? '';
1446
+ const nextResponse = response?.trim() ?? '';
1447
+ const nextStatus = run.status !== 'running' && status === 'running' ? run.status : status;
1448
+ const hasChanged = run.log !== nextLog || run.response !== nextResponse || run.status !== nextStatus;
1449
+ if (!hasChanged) {
1450
+ return;
1451
+ }
1452
+ run.log = nextLog;
1453
+ run.response = nextResponse;
1454
+ run.status = nextStatus;
1455
+ if (nextStatus === 'running') {
1456
+ updateAgentRunStatus(run.id, 'running', run.log || null, run.response || null);
1457
+ }
1458
+ else {
1459
+ updateAgentRunStatus(run.id, 'running', run.log || null, run.response || null);
1460
+ finalizeAgentRun(run.id, nextStatus, run.response || null);
1461
+ }
1462
+ emitUpdate();
1463
+ };
1464
+ const finalize = (status, parentLog, parentResponse) => {
1465
+ if (trackedRuns.length === 0 && !mainRun) {
1466
+ return null;
1467
+ }
1468
+ for (const run of trackedRuns) {
1469
+ if (run.status !== 'running') {
1470
+ continue;
1471
+ }
1472
+ run.status = status;
1473
+ finalizeAgentRun(run.id, status, run.response || null);
1474
+ }
1475
+ emitUpdate();
1476
+ const trackedIds = new Set(trackedRuns.map(run => run.id));
1477
+ const persistedRuns = listAgentRunsForParent(parentType, parentId)
1478
+ .filter(run => trackedIds.has(run.id) || run.id === mainRun?.id);
1479
+ const persistedIds = new Set(persistedRuns.map(run => run.id));
1480
+ const timestamp = new Date().toISOString();
1481
+ for (const run of trackedRuns) {
1482
+ if (persistedIds.has(run.id)) {
1483
+ continue;
1484
+ }
1485
+ persistedRuns.push({
1486
+ id: run.id,
1487
+ parent_type: parentType,
1488
+ parent_id: parentId,
1489
+ run_index: run.runIndex,
1490
+ label: run.label,
1491
+ role: 'subagent',
1492
+ agent_status: run.status,
1493
+ agent_log: run.log || null,
1494
+ streaming_response: null,
1495
+ response_text: run.response || null,
1496
+ session_id: run.sessionId,
1497
+ created_at: run.createdAt,
1498
+ updated_at: timestamp,
1499
+ });
1500
+ }
1501
+ if (mainRun) {
1502
+ const persistedMainRun = persistedRuns.find(run => run.id === mainRun.id);
1503
+ const mainSnapshotRun = {
1504
+ id: mainRun.id,
1505
+ parent_type: parentType,
1506
+ parent_id: parentId,
1507
+ run_index: mainRun.runIndex,
1508
+ label: mainRun.label,
1509
+ role: mainRun.role,
1510
+ agent_status: status,
1511
+ agent_log: parentLog,
1512
+ streaming_response: null,
1513
+ response_text: parentResponse,
1514
+ session_id: persistedMainRun?.session_id ?? null,
1515
+ created_at: persistedMainRun?.created_at ?? timestamp,
1516
+ updated_at: persistedMainRun?.updated_at ?? timestamp,
1517
+ };
1518
+ const mainRunIndex = persistedRuns.findIndex(run => run.id === mainRun.id);
1519
+ if (mainRunIndex >= 0) {
1520
+ persistedRuns[mainRunIndex] = mainSnapshotRun;
1521
+ }
1522
+ else {
1523
+ persistedRuns.push(mainSnapshotRun);
1524
+ }
1525
+ }
1526
+ else {
1527
+ persistedRuns.push({
1528
+ id: `${parentType}:${parentId}:main`,
1529
+ parent_type: parentType,
1530
+ parent_id: parentId,
1531
+ run_index: 0,
1532
+ label: 'Main agent',
1533
+ role: 'main',
1534
+ agent_status: status,
1535
+ agent_log: parentLog,
1536
+ streaming_response: null,
1537
+ response_text: parentResponse,
1538
+ session_id: null,
1539
+ created_at: timestamp,
1540
+ updated_at: timestamp,
1541
+ });
1542
+ }
1543
+ return createTranscriptRunSnapshots(persistedRuns.sort((left, right) => left.run_index - right.run_index));
1544
+ };
1545
+ return { applySnapshot, handle, finalize };
1546
+ };
1547
+ export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1548
+ let log = '';
1549
+ let response = '';
1550
+ for (const persistedPart of parts) {
1551
+ if (persistedPart.role && persistedPart.role !== 'assistant') {
1552
+ continue;
1553
+ }
1554
+ const part = persistedPart.data;
1555
+ const partType = part?.type;
1556
+ const eventType = partType === 'tool'
1557
+ ? 'tool_use'
1558
+ : partType === 'step-start'
1559
+ ? 'step_start'
1560
+ : partType === 'step-finish'
1561
+ ? 'step_finish'
1562
+ : partType;
1563
+ const parsed = parseOpencodeJsonEvent({ type: eventType, part }, cwd);
1564
+ if (parsed.logFragment && parsed.logFragment !== parsed.responseFragment) {
1565
+ log = appendTranscriptText(log, parsed.logFragment);
1566
+ }
1567
+ if (parsed.responseFragment) {
1568
+ response = appendTranscriptText(response, parsed.responseFragment);
1569
+ }
1082
1570
  }
1083
- return { logFragment: null, responseFragment: null };
1571
+ return { log: log || null, response: response || null };
1572
+ };
1573
+ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, taskSessionReader, discoverParentSession = true, }) => {
1574
+ const reader = taskSessionReader === undefined ? createOpencodeTaskSessionReader() : taskSessionReader;
1575
+ let parentSessionId = initialParentSessionId;
1576
+ let timer = null;
1577
+ const childSessionByCallId = new Map();
1578
+ const setParentSessionId = (sessionId) => {
1579
+ const normalized = sessionId?.trim() || null;
1580
+ if (!normalized || normalized === parentSessionId) {
1581
+ return;
1582
+ }
1583
+ parentSessionId = normalized;
1584
+ onParentSessionId?.(normalized);
1585
+ };
1586
+ const resolveChildSessions = (invocations, children) => {
1587
+ const claimedSessionIds = new Set(childSessionByCallId.values());
1588
+ for (const invocation of invocations) {
1589
+ if (invocation.childSessionId) {
1590
+ childSessionByCallId.set(invocation.callId, invocation.childSessionId);
1591
+ claimedSessionIds.add(invocation.childSessionId);
1592
+ }
1593
+ }
1594
+ const unmatchedInvocations = invocations.filter(invocation => !childSessionByCallId.has(invocation.callId));
1595
+ const unmatchedChildren = children.filter(child => !claimedSessionIds.has(child.id));
1596
+ for (const invocation of unmatchedInvocations) {
1597
+ const description = invocation.label.replace(/^Subagent:\s*/i, '').toLowerCase();
1598
+ const titleMatch = unmatchedChildren.find(child => child.title?.toLowerCase().includes(description));
1599
+ if (!titleMatch) {
1600
+ continue;
1601
+ }
1602
+ childSessionByCallId.set(invocation.callId, titleMatch.id);
1603
+ claimedSessionIds.add(titleMatch.id);
1604
+ }
1605
+ const stillUnmatchedInvocations = unmatchedInvocations.filter(invocation => !childSessionByCallId.has(invocation.callId));
1606
+ const stillUnmatchedChildren = unmatchedChildren.filter(child => !claimedSessionIds.has(child.id));
1607
+ if (stillUnmatchedInvocations.length === 1 && stillUnmatchedChildren.length === 1) {
1608
+ childSessionByCallId.set(stillUnmatchedInvocations[0].callId, stillUnmatchedChildren[0].id);
1609
+ }
1610
+ };
1611
+ const refresh = () => {
1612
+ if (!reader) {
1613
+ return;
1614
+ }
1615
+ try {
1616
+ if (!parentSessionId && discoverParentSession) {
1617
+ setParentSessionId(reader.findParentSessionSince(spawnTimestamp));
1618
+ }
1619
+ if (!parentSessionId) {
1620
+ return;
1621
+ }
1622
+ const invocations = reader.listTaskInvocations(parentSessionId, spawnTimestamp);
1623
+ const children = reader.listChildSessions(parentSessionId, spawnTimestamp);
1624
+ resolveChildSessions(invocations, children);
1625
+ for (const invocation of invocations) {
1626
+ const childSessionId = childSessionByCallId.get(invocation.callId) ?? invocation.childSessionId;
1627
+ const transcript = childSessionId
1628
+ ? reconstructOpencodeSubagentTranscript(reader.listSessionParts(childSessionId), cwd)
1629
+ : { log: null, response: null };
1630
+ tracker.applySnapshot({
1631
+ callId: invocation.callId,
1632
+ label: invocation.label,
1633
+ status: invocation.status,
1634
+ log: transcript.log,
1635
+ response: transcript.response ?? invocation.output,
1636
+ sessionId: childSessionId ?? null,
1637
+ });
1638
+ }
1639
+ }
1640
+ catch {
1641
+ // OpenCode persistence is a progressive enhancement; stdout remains the fallback.
1642
+ }
1643
+ };
1644
+ const start = () => {
1645
+ if (!reader || timer) {
1646
+ return;
1647
+ }
1648
+ refresh();
1649
+ timer = setInterval(refresh, 150);
1650
+ timer.unref?.();
1651
+ };
1652
+ const stop = () => {
1653
+ if (timer) {
1654
+ clearInterval(timer);
1655
+ timer = null;
1656
+ }
1657
+ reader?.close();
1658
+ };
1659
+ return { refresh, setParentSessionId, start, stop };
1084
1660
  };
1085
1661
  const buildResponseTiming = ({ startTime, firstFinalAnswerTime, isStopped, }) => {
1086
1662
  if (!startTime) {
@@ -1265,6 +1841,21 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1265
1841
  };
1266
1842
  export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, emitSuccessUpdate = true, opencodeAgentMode, runStream, }) => {
1267
1843
  const resolvedModel = resolveAgentModel(tool, model);
1844
+ const subagentTracker = createSubagentRunTracker({
1845
+ parentType: 'ticket',
1846
+ parentId: ticketId,
1847
+ io: io,
1848
+ ...(runStream
1849
+ ? {
1850
+ mainRun: {
1851
+ id: runStream.runId,
1852
+ label: runStream.label,
1853
+ role: runStream.role,
1854
+ runIndex: runStream.runIndex,
1855
+ },
1856
+ }
1857
+ : {}),
1858
+ });
1268
1859
  const writeRunStatus = (status, log, streamingResponse = null, options = {}) => {
1269
1860
  if (runStream) {
1270
1861
  updateAgentRunStatus(runStream.runId, status, log, streamingResponse);
@@ -1285,15 +1876,17 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1285
1876
  if (runStream) {
1286
1877
  finalizeAgentRun(runStream.runId, status, responseText);
1287
1878
  emitAgentRunsUpdated(io, 'ticket', ticketId);
1288
- return;
1879
+ return subagentTracker.finalize(status, log, responseText);
1289
1880
  }
1290
1881
  updateStatus(ticketId, status, log, io, responseText, options);
1882
+ return subagentTracker.finalize(status, log, responseText);
1291
1883
  };
1292
1884
  return new Promise((resolve, reject) => {
1293
1885
  let sessionId = null;
1294
1886
  let spawnTimestamp = null;
1295
1887
  let sessionLookupTimer = null;
1296
1888
  let hasResolvedOpencodeSessionId = Boolean(resumeSessionId);
1889
+ let opencodeSubagentMonitor = null;
1297
1890
  let settled = false;
1298
1891
  let startTime = null;
1299
1892
  let firstFinalAnswerTime = null;
@@ -1305,6 +1898,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1305
1898
  return;
1306
1899
  }
1307
1900
  settled = true;
1901
+ opencodeSubagentMonitor?.stop();
1308
1902
  if (sessionLookupTimer) {
1309
1903
  clearTimeout(sessionLookupTimer);
1310
1904
  sessionLookupTimer = null;
@@ -1362,6 +1956,23 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1362
1956
  activeBuildProcesses.set(ticketId, buildState);
1363
1957
  }
1364
1958
  startTime = Date.now();
1959
+ if (tool === 'opencode' && spawnTimestamp !== null) {
1960
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
1961
+ initialParentSessionId: resumeSessionId,
1962
+ spawnTimestamp,
1963
+ cwd,
1964
+ tracker: subagentTracker,
1965
+ discoverParentSession: false,
1966
+ onParentSessionId: discoveredSessionId => {
1967
+ if (hasResolvedOpencodeSessionId) {
1968
+ return;
1969
+ }
1970
+ hasResolvedOpencodeSessionId = true;
1971
+ writeRunSessionId(discoveredSessionId);
1972
+ },
1973
+ });
1974
+ opencodeSubagentMonitor.start();
1975
+ }
1365
1976
  if (tool === 'claude') {
1366
1977
  const logLines = [];
1367
1978
  const responseLines = [];
@@ -1377,7 +1988,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1377
1988
  remainder = lines.pop() ?? '';
1378
1989
  for (const line of lines) {
1379
1990
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1380
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line, cwd);
1991
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line, cwd);
1992
+ subagentTracker.handle(subagentEvents);
1381
1993
  if (logFragment !== null) {
1382
1994
  logLines.push(logFragment);
1383
1995
  }
@@ -1394,7 +2006,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1394
2006
  progressUpdater.cancel();
1395
2007
  if (remainder.trim()) {
1396
2008
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1397
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder, cwd);
2009
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder, cwd);
2010
+ subagentTracker.handle(subagentEvents);
1398
2011
  if (logFragment !== null) {
1399
2012
  logLines.push(logFragment);
1400
2013
  }
@@ -1408,32 +2021,33 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1408
2021
  const modelError = isModelResolutionError(finalLog);
1409
2022
  const timing = startTime ? { totalMs: Date.now() - startTime } : null;
1410
2023
  if (buildState?.stopRequested) {
1411
- finalizeRun('stopped', finalLog, finalResponse);
1412
- resolveOnce({ response: finalResponse, responseTiming: timing, usageSnapshot });
2024
+ const transcriptRuns = finalizeRun('stopped', finalLog, finalResponse);
2025
+ resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing, usageSnapshot }, transcriptRuns));
1413
2026
  return;
1414
2027
  }
1415
2028
  if (code === 0 && !modelError) {
1416
- finalizeRun('done', finalLog, finalResponse, { emit: emitSuccessUpdate });
1417
- resolveOnce({ response: finalResponse, responseTiming: timing, usageSnapshot });
2029
+ const transcriptRuns = finalizeRun('done', finalLog, finalResponse, { emit: emitSuccessUpdate });
2030
+ resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing, usageSnapshot }, transcriptRuns));
1418
2031
  return;
1419
2032
  }
1420
- finalizeRun('error', finalLog + (errOutput ? '\n\n' + errOutput : ''), finalResponse);
1421
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
2033
+ const transcriptRuns = finalizeRun('error', finalLog + (errOutput ? '\n\n' + errOutput : ''), finalResponse);
2034
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`), transcriptRuns));
1422
2035
  });
1423
2036
  child.on('error', err => {
1424
2037
  progressUpdater.cancel();
1425
2038
  if (buildState?.stopRequested) {
1426
- finalizeRun('stopped', null, null);
1427
- resolveOnce({ response: null, responseTiming: null, usageSnapshot });
2039
+ const transcriptRuns = finalizeRun('stopped', null, null);
2040
+ resolveOnce(withTranscriptRuns({ response: null, responseTiming: null, usageSnapshot }, transcriptRuns));
1428
2041
  return;
1429
2042
  }
1430
- finalizeRun('error', getErrorMessage(err), null);
1431
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2043
+ const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2044
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
1432
2045
  });
1433
2046
  return;
1434
2047
  }
1435
2048
  const logLines = [];
1436
2049
  const responseLines = [];
2050
+ const recoveryTracker = createOpencodeRecoveryTracker();
1437
2051
  let remainder = '';
1438
2052
  const stderrChunks = [];
1439
2053
  const progressUpdater = createThrottledProgressUpdater(() => {
@@ -1459,12 +2073,12 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1459
2073
  catch { /* best-effort timing */ }
1460
2074
  }
1461
2075
  }
1462
- const { logFragment, responseFragment } = parseOpencodeJsonLine(line);
1463
- if (logFragment !== null) {
1464
- logLines.push(logFragment);
1465
- }
1466
- if (responseFragment !== null) {
1467
- responseLines.push(responseFragment);
2076
+ const parsed = parseOpencodeJsonLine(line);
2077
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
2078
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2079
+ subagentTracker.handle(subagentEvents);
2080
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2081
+ firstFinalAnswerTime = null;
1468
2082
  }
1469
2083
  if (logFragment !== null || responseFragment !== null) {
1470
2084
  progressUpdater.schedule();
@@ -1489,31 +2103,35 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1489
2103
  sessionLookupTimer = null;
1490
2104
  }
1491
2105
  if (remainder.trim()) {
1492
- const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder);
1493
- if (logFragment !== null) {
1494
- logLines.push(logFragment);
1495
- }
1496
- if (responseFragment !== null) {
1497
- responseLines.push(responseFragment);
2106
+ const parsed = parseOpencodeJsonLine(remainder);
2107
+ const { subagentEvents, providerSessionId } = parsed;
2108
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2109
+ subagentTracker.handle(subagentEvents);
2110
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2111
+ firstFinalAnswerTime = null;
1498
2112
  }
1499
2113
  }
1500
2114
  const stderrOutput = Buffer.concat(stderrChunks).toString();
2115
+ opencodeSubagentMonitor?.refresh();
1501
2116
  const log = logLines.join('\n\n') || stderrOutput;
1502
- const finalResponse = responseLines.join('\n\n') || stderrOutput;
2117
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
2118
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
2119
+ ? recoveredResponse
2120
+ : responseLines.join('\n\n') || stderrOutput;
1503
2121
  const modelError = isModelResolutionError(log);
1504
2122
  const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested) });
1505
2123
  if (buildState?.stopRequested) {
1506
- finalizeRun('stopped', log, finalResponse);
1507
- resolveOnce({ response: finalResponse, responseTiming: timing });
2124
+ const transcriptRuns = finalizeRun('stopped', log, finalResponse);
2125
+ resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing }, transcriptRuns));
1508
2126
  return;
1509
2127
  }
1510
- if (code === 0 && !modelError) {
1511
- finalizeRun('done', log, finalResponse, { emit: emitSuccessUpdate });
1512
- resolveOnce({ response: finalResponse, responseTiming: timing });
2128
+ if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2129
+ const transcriptRuns = finalizeRun('done', log, finalResponse, { emit: emitSuccessUpdate });
2130
+ resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing }, transcriptRuns));
1513
2131
  return;
1514
2132
  }
1515
- finalizeRun('error', log, finalResponse);
1516
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
2133
+ const transcriptRuns = finalizeRun('error', log, finalResponse);
2134
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`), transcriptRuns));
1517
2135
  });
1518
2136
  child.on('error', err => {
1519
2137
  progressUpdater.cancel();
@@ -1522,21 +2140,27 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1522
2140
  sessionLookupTimer = null;
1523
2141
  }
1524
2142
  if (buildState?.stopRequested) {
1525
- finalizeRun('stopped', null, null);
1526
- resolveOnce({ response: null, responseTiming: null });
2143
+ const transcriptRuns = finalizeRun('stopped', null, null);
2144
+ resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
1527
2145
  return;
1528
2146
  }
1529
- finalizeRun('error', getErrorMessage(err), null);
1530
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2147
+ const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2148
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
1531
2149
  });
1532
2150
  });
1533
2151
  };
1534
2152
  export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', }) => {
1535
2153
  const resolvedModel = resolveAgentModel(tool, model);
2154
+ const subagentTracker = createSubagentRunTracker({
2155
+ parentType: 'chat',
2156
+ parentId: chatSessionId,
2157
+ io,
2158
+ });
1536
2159
  return new Promise((resolve, reject) => {
1537
2160
  let sessionId = null;
1538
2161
  let spawnTimestamp = null;
1539
2162
  let opencodeMetadataRefresher = null;
2163
+ let opencodeSubagentMonitor = null;
1540
2164
  let settled = false;
1541
2165
  let startTime = null;
1542
2166
  let firstFinalAnswerTime = null;
@@ -1552,6 +2176,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1552
2176
  }
1553
2177
  settled = true;
1554
2178
  opencodeMetadataRefresher?.stop();
2179
+ opencodeSubagentMonitor?.stop();
1555
2180
  untrackChatProcess(chatSessionId, chatState);
1556
2181
  handler(value);
1557
2182
  };
@@ -1598,6 +2223,19 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1598
2223
  writeClaudeStdin(child, buildClaudeStdinMessage(prompt, files, cwd));
1599
2224
  }
1600
2225
  startTime = Date.now();
2226
+ if (tool === 'opencode' && spawnTimestamp !== null) {
2227
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
2228
+ initialParentSessionId: resumeSessionId,
2229
+ spawnTimestamp,
2230
+ cwd,
2231
+ tracker: subagentTracker,
2232
+ discoverParentSession: false,
2233
+ onParentSessionId: discoveredSessionId => {
2234
+ updateChatSessionId(chatSessionId, 'opencode', discoveredSessionId, io);
2235
+ },
2236
+ });
2237
+ opencodeSubagentMonitor.start();
2238
+ }
1601
2239
  if (tool === 'claude') {
1602
2240
  const logLines = [];
1603
2241
  const responseLines = [];
@@ -1611,7 +2249,8 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1611
2249
  const lines = text.split('\n');
1612
2250
  remainder = lines.pop() ?? '';
1613
2251
  for (const line of lines) {
1614
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line);
2252
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line);
2253
+ subagentTracker.handle(subagentEvents);
1615
2254
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1616
2255
  if (logFragment !== null) {
1617
2256
  logLines.push(logFragment);
@@ -1629,7 +2268,8 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1629
2268
  progressUpdater.cancel();
1630
2269
  if (remainder.trim()) {
1631
2270
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1632
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder);
2271
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
2272
+ subagentTracker.handle(subagentEvents);
1633
2273
  if (logFragment !== null) {
1634
2274
  logLines.push(logFragment);
1635
2275
  }
@@ -1647,12 +2287,13 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1647
2287
  response: finalResponse,
1648
2288
  });
1649
2289
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1650
- resolveOnce({
2290
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2291
+ resolveOnce(withTranscriptRuns({
1651
2292
  completionStatus: 'stopped',
1652
2293
  finalResponse: preservedOutput.response,
1653
2294
  usageSnapshot,
1654
2295
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1655
- });
2296
+ }, transcriptRuns));
1656
2297
  return;
1657
2298
  }
1658
2299
  const modelError = isModelResolutionError(finalLog);
@@ -1663,16 +2304,18 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1663
2304
  response: finalResponse,
1664
2305
  });
1665
2306
  updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
1666
- resolveOnce({
2307
+ const transcriptRuns = subagentTracker.finalize('done', finalOutput.log, finalOutput.response);
2308
+ resolveOnce(withTranscriptRuns({
1667
2309
  completionStatus: 'done',
1668
2310
  finalResponse: finalOutput.response,
1669
2311
  usageSnapshot,
1670
2312
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1671
- });
2313
+ }, transcriptRuns));
1672
2314
  return;
1673
2315
  }
1674
2316
  updateChatSessionStatus(chatSessionId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1675
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
2317
+ const transcriptRuns = subagentTracker.finalize('error', finalLog, finalResponse);
2318
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`), transcriptRuns));
1676
2319
  });
1677
2320
  child.on('error', err => {
1678
2321
  progressUpdater.cancel();
@@ -1685,21 +2328,24 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1685
2328
  response,
1686
2329
  });
1687
2330
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1688
- resolveOnce({
2331
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2332
+ resolveOnce(withTranscriptRuns({
1689
2333
  completionStatus: 'stopped',
1690
2334
  finalResponse: preservedOutput.response,
1691
2335
  usageSnapshot,
1692
2336
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1693
- });
2337
+ }, transcriptRuns));
1694
2338
  return;
1695
2339
  }
1696
2340
  updateChatSessionStatus(chatSessionId, 'error', getErrorMessage(err), io);
1697
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2341
+ const transcriptRuns = subagentTracker.finalize('error', getErrorMessage(err), null);
2342
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
1698
2343
  });
1699
2344
  return;
1700
2345
  }
1701
2346
  const logLines = [];
1702
2347
  const responseLines = [];
2348
+ const recoveryTracker = createOpencodeRecoveryTracker();
1703
2349
  let remainder = '';
1704
2350
  const stderrChunks = [];
1705
2351
  const progressUpdater = createThrottledProgressUpdater(() => {
@@ -1726,12 +2372,12 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1726
2372
  catch { /* best-effort timing */ }
1727
2373
  }
1728
2374
  }
1729
- const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
1730
- if (logFragment !== null) {
1731
- logLines.push(logFragment);
1732
- }
1733
- if (responseFragment !== null) {
1734
- responseLines.push(responseFragment);
2375
+ const parsed = parseOpencodeJsonLine(line, cwd);
2376
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
2377
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2378
+ subagentTracker.handle(subagentEvents);
2379
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2380
+ firstFinalAnswerTime = null;
1735
2381
  }
1736
2382
  if (logFragment !== null || responseFragment !== null) {
1737
2383
  progressUpdater.schedule();
@@ -1746,17 +2392,20 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1746
2392
  progressUpdater.cancel();
1747
2393
  if (remainder.trim()) {
1748
2394
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1749
- const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder, cwd);
1750
- if (logFragment !== null) {
1751
- logLines.push(logFragment);
1752
- }
1753
- if (responseFragment !== null) {
1754
- responseLines.push(responseFragment);
2395
+ const parsed = parseOpencodeJsonLine(remainder, cwd);
2396
+ const { subagentEvents, providerSessionId } = parsed;
2397
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2398
+ subagentTracker.handle(subagentEvents);
2399
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2400
+ firstFinalAnswerTime = null;
1755
2401
  }
1756
2402
  }
1757
2403
  const stderrOutput = Buffer.concat(stderrChunks).toString();
1758
2404
  const log = logLines.join('\n\n') || stderrOutput;
1759
- const finalResponse = responseLines.join('\n\n') || stderrOutput;
2405
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
2406
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
2407
+ ? recoveredResponse
2408
+ : responseLines.join('\n\n') || stderrOutput;
1760
2409
  const preservedOutput = chatState.stopRequested
1761
2410
  ? getPreservedChatOutput({
1762
2411
  chatSessionId,
@@ -1769,6 +2418,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1769
2418
  ?? preservedOutput?.opencodeSessionId
1770
2419
  ?? getChatSession(chatSessionId)?.opencode_session_id
1771
2420
  ?? resumeSessionId;
2421
+ opencodeSubagentMonitor?.refresh();
1772
2422
  if (chatState.stopRequested) {
1773
2423
  if (resolvedOpencodeSessionId) {
1774
2424
  usageSnapshot = resolveOpencodeUsageSnapshot({
@@ -1778,16 +2428,17 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1778
2428
  });
1779
2429
  }
1780
2430
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
1781
- resolveOnce({
2431
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput?.log ?? null, preservedOutput?.response ?? null);
2432
+ resolveOnce(withTranscriptRuns({
1782
2433
  completionStatus: 'stopped',
1783
2434
  finalResponse: preservedOutput?.response ?? null,
1784
2435
  usageSnapshot,
1785
2436
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1786
- });
2437
+ }, transcriptRuns));
1787
2438
  return;
1788
2439
  }
1789
2440
  const modelError = isModelResolutionError(log);
1790
- if (code === 0 && !modelError) {
2441
+ if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
1791
2442
  const finalOutput = getPreservedChatOutput({
1792
2443
  chatSessionId,
1793
2444
  log,
@@ -1801,16 +2452,18 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1801
2452
  });
1802
2453
  }
1803
2454
  updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
1804
- resolveOnce({
2455
+ const transcriptRuns = subagentTracker.finalize('done', finalOutput.log, finalOutput.response);
2456
+ resolveOnce(withTranscriptRuns({
1805
2457
  completionStatus: 'done',
1806
2458
  finalResponse: finalOutput.response,
1807
2459
  usageSnapshot,
1808
2460
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1809
- });
2461
+ }, transcriptRuns));
1810
2462
  return;
1811
2463
  }
1812
2464
  updateChatSessionStatus(chatSessionId, 'error', log, io, finalResponse);
1813
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
2465
+ const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
2466
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`), transcriptRuns));
1814
2467
  });
1815
2468
  child.on('error', err => {
1816
2469
  progressUpdater.cancel();
@@ -1823,25 +2476,33 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1823
2476
  response,
1824
2477
  });
1825
2478
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1826
- resolveOnce({
2479
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2480
+ resolveOnce(withTranscriptRuns({
1827
2481
  completionStatus: 'stopped',
1828
2482
  finalResponse: preservedOutput.response,
1829
2483
  usageSnapshot,
1830
2484
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1831
- });
2485
+ }, transcriptRuns));
1832
2486
  return;
1833
2487
  }
1834
2488
  updateChatSessionStatus(chatSessionId, 'error', getErrorMessage(err), io);
1835
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2489
+ const transcriptRuns = subagentTracker.finalize('error', getErrorMessage(err), null);
2490
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
1836
2491
  });
1837
2492
  });
1838
2493
  };
1839
2494
  export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, }) => {
1840
2495
  const resolvedModel = resolveAgentModel(tool, model);
2496
+ const subagentTracker = createSubagentRunTracker({
2497
+ parentType: 'spec',
2498
+ parentId: specSessionId,
2499
+ io,
2500
+ });
1841
2501
  return new Promise((resolve, reject) => {
1842
2502
  let sessionId = null;
1843
2503
  let spawnTimestamp = null;
1844
2504
  let opencodeMetadataRefresher = null;
2505
+ let opencodeSubagentMonitor = null;
1845
2506
  let settled = false;
1846
2507
  let startTime = null;
1847
2508
  let firstFinalAnswerTime = null;
@@ -1857,6 +2518,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1857
2518
  }
1858
2519
  settled = true;
1859
2520
  opencodeMetadataRefresher?.stop();
2521
+ opencodeSubagentMonitor?.stop();
1860
2522
  untrackSpecProcess(specSessionId, specState);
1861
2523
  handler(value);
1862
2524
  };
@@ -1903,6 +2565,19 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1903
2565
  writeClaudeStdin(child, buildClaudeStdinMessage(prompt, files, cwd));
1904
2566
  }
1905
2567
  startTime = Date.now();
2568
+ if (tool === 'opencode' && spawnTimestamp !== null) {
2569
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
2570
+ initialParentSessionId: resumeSessionId,
2571
+ spawnTimestamp,
2572
+ cwd,
2573
+ tracker: subagentTracker,
2574
+ discoverParentSession: false,
2575
+ onParentSessionId: discoveredSessionId => {
2576
+ updateSpecSessionIdLocal(specSessionId, 'opencode', discoveredSessionId, io);
2577
+ },
2578
+ });
2579
+ opencodeSubagentMonitor.start();
2580
+ }
1906
2581
  if (tool === 'claude') {
1907
2582
  const logLines = [];
1908
2583
  const responseLines = [];
@@ -1916,7 +2591,8 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1916
2591
  const lines = text.split('\n');
1917
2592
  remainder = lines.pop() ?? '';
1918
2593
  for (const line of lines) {
1919
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line);
2594
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line);
2595
+ subagentTracker.handle(subagentEvents);
1920
2596
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1921
2597
  if (logFragment !== null) {
1922
2598
  logLines.push(logFragment);
@@ -1934,7 +2610,8 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1934
2610
  progressUpdater.cancel();
1935
2611
  if (remainder.trim()) {
1936
2612
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1937
- const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder);
2613
+ const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
2614
+ subagentTracker.handle(subagentEvents);
1938
2615
  if (logFragment !== null) {
1939
2616
  logLines.push(logFragment);
1940
2617
  }
@@ -1952,12 +2629,13 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1952
2629
  response: finalResponse,
1953
2630
  });
1954
2631
  updateSpecSessionStatusLocal(specSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1955
- resolveOnce({
2632
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2633
+ resolveOnce(withTranscriptRuns({
1956
2634
  completionStatus: 'stopped',
1957
2635
  finalResponse: preservedOutput.response,
1958
2636
  usageSnapshot,
1959
2637
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1960
- });
2638
+ }, transcriptRuns));
1961
2639
  return;
1962
2640
  }
1963
2641
  const modelError = isModelResolutionError(finalLog);
@@ -1968,16 +2646,18 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1968
2646
  response: finalResponse,
1969
2647
  });
1970
2648
  updateSpecSessionStatusLocal(specSessionId, 'running', finalOutput.log, io, finalOutput.response);
1971
- resolveOnce({
2649
+ const transcriptRuns = subagentTracker.finalize('done', finalOutput.log, finalOutput.response);
2650
+ resolveOnce(withTranscriptRuns({
1972
2651
  completionStatus: 'done',
1973
2652
  finalResponse: finalOutput.response,
1974
2653
  usageSnapshot,
1975
2654
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1976
- });
2655
+ }, transcriptRuns));
1977
2656
  return;
1978
2657
  }
1979
2658
  updateSpecSessionStatusLocal(specSessionId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1980
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
2659
+ const transcriptRuns = subagentTracker.finalize('error', finalLog, finalResponse);
2660
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`), transcriptRuns));
1981
2661
  });
1982
2662
  child.on('error', err => {
1983
2663
  progressUpdater.cancel();
@@ -1990,21 +2670,24 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1990
2670
  response,
1991
2671
  });
1992
2672
  updateSpecSessionStatusLocal(specSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1993
- resolveOnce({
2673
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2674
+ resolveOnce(withTranscriptRuns({
1994
2675
  completionStatus: 'stopped',
1995
2676
  finalResponse: preservedOutput.response,
1996
2677
  usageSnapshot,
1997
2678
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1998
- });
2679
+ }, transcriptRuns));
1999
2680
  return;
2000
2681
  }
2001
2682
  updateSpecSessionStatusLocal(specSessionId, 'error', getErrorMessage(err), io);
2002
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2683
+ const transcriptRuns = subagentTracker.finalize('error', getErrorMessage(err), null);
2684
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2003
2685
  });
2004
2686
  return;
2005
2687
  }
2006
2688
  const logLines = [];
2007
2689
  const responseLines = [];
2690
+ const recoveryTracker = createOpencodeRecoveryTracker();
2008
2691
  let remainder = '';
2009
2692
  const stderrChunks = [];
2010
2693
  const progressUpdater = createThrottledProgressUpdater(() => {
@@ -2031,12 +2714,12 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2031
2714
  catch { /* best-effort timing */ }
2032
2715
  }
2033
2716
  }
2034
- const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
2035
- if (logFragment !== null) {
2036
- logLines.push(logFragment);
2037
- }
2038
- if (responseFragment !== null) {
2039
- responseLines.push(responseFragment);
2717
+ const parsed = parseOpencodeJsonLine(line, cwd);
2718
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
2719
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2720
+ subagentTracker.handle(subagentEvents);
2721
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2722
+ firstFinalAnswerTime = null;
2040
2723
  }
2041
2724
  if (logFragment !== null || responseFragment !== null) {
2042
2725
  progressUpdater.schedule();
@@ -2049,19 +2732,23 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2049
2732
  }
2050
2733
  child.on('close', code => {
2051
2734
  progressUpdater.cancel();
2735
+ opencodeSubagentMonitor?.refresh();
2052
2736
  if (remainder.trim()) {
2053
2737
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2054
- const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder, cwd);
2055
- if (logFragment !== null) {
2056
- logLines.push(logFragment);
2057
- }
2058
- if (responseFragment !== null) {
2059
- responseLines.push(responseFragment);
2738
+ const parsed = parseOpencodeJsonLine(remainder, cwd);
2739
+ const { subagentEvents, providerSessionId } = parsed;
2740
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2741
+ subagentTracker.handle(subagentEvents);
2742
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2743
+ firstFinalAnswerTime = null;
2060
2744
  }
2061
2745
  }
2062
2746
  const stderrOutput = Buffer.concat(stderrChunks).toString();
2063
2747
  const log = logLines.join('\n\n') || stderrOutput;
2064
- const finalResponse = responseLines.join('\n\n') || stderrOutput;
2748
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
2749
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
2750
+ ? recoveredResponse
2751
+ : responseLines.join('\n\n') || stderrOutput;
2065
2752
  const preservedOutput = specState.stopRequested
2066
2753
  ? getPreservedSpecOutput({
2067
2754
  specSessionId,
@@ -2074,6 +2761,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2074
2761
  ?? preservedOutput?.opencodeSessionId
2075
2762
  ?? getSpecSession(specSessionId)?.opencode_session_id
2076
2763
  ?? resumeSessionId;
2764
+ opencodeSubagentMonitor?.refresh();
2077
2765
  if (specState.stopRequested) {
2078
2766
  if (resolvedOpencodeSessionId) {
2079
2767
  usageSnapshot = resolveOpencodeUsageSnapshot({
@@ -2083,16 +2771,17 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2083
2771
  });
2084
2772
  }
2085
2773
  updateSpecSessionStatusLocal(specSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
2086
- resolveOnce({
2774
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput?.log ?? null, preservedOutput?.response ?? null);
2775
+ resolveOnce(withTranscriptRuns({
2087
2776
  completionStatus: 'stopped',
2088
2777
  finalResponse: preservedOutput?.response ?? null,
2089
2778
  usageSnapshot,
2090
2779
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
2091
- });
2780
+ }, transcriptRuns));
2092
2781
  return;
2093
2782
  }
2094
2783
  const modelError = isModelResolutionError(log);
2095
- if (code === 0 && !modelError) {
2784
+ if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2096
2785
  const finalOutput = getPreservedSpecOutput({
2097
2786
  specSessionId,
2098
2787
  log,
@@ -2106,16 +2795,18 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2106
2795
  });
2107
2796
  }
2108
2797
  updateSpecSessionStatusLocal(specSessionId, 'running', finalOutput.log, io, finalOutput.response);
2109
- resolveOnce({
2798
+ const transcriptRuns = subagentTracker.finalize('done', finalOutput.log, finalOutput.response);
2799
+ resolveOnce(withTranscriptRuns({
2110
2800
  completionStatus: 'done',
2111
2801
  finalResponse: finalOutput.response,
2112
2802
  usageSnapshot,
2113
2803
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
2114
- });
2804
+ }, transcriptRuns));
2115
2805
  return;
2116
2806
  }
2117
2807
  updateSpecSessionStatusLocal(specSessionId, 'error', log, io, finalResponse);
2118
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
2808
+ const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
2809
+ rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`), transcriptRuns));
2119
2810
  });
2120
2811
  child.on('error', err => {
2121
2812
  progressUpdater.cancel();
@@ -2128,16 +2819,18 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2128
2819
  response,
2129
2820
  });
2130
2821
  updateSpecSessionStatusLocal(specSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
2131
- resolveOnce({
2822
+ const transcriptRuns = subagentTracker.finalize('stopped', preservedOutput.log, preservedOutput.response);
2823
+ resolveOnce(withTranscriptRuns({
2132
2824
  completionStatus: 'stopped',
2133
2825
  finalResponse: preservedOutput.response,
2134
2826
  usageSnapshot,
2135
2827
  responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
2136
- });
2828
+ }, transcriptRuns));
2137
2829
  return;
2138
2830
  }
2139
2831
  updateSpecSessionStatusLocal(specSessionId, 'error', getErrorMessage(err), io);
2140
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
2832
+ const transcriptRuns = subagentTracker.finalize('error', getErrorMessage(err), null);
2833
+ rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2141
2834
  });
2142
2835
  });
2143
2836
  };
@@ -2269,6 +2962,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2269
2962
  }
2270
2963
  const logLines = [];
2271
2964
  const responseLines = [];
2965
+ const recoveryTracker = createOpencodeRecoveryTracker();
2272
2966
  let remainder = '';
2273
2967
  const stderrChunks = [];
2274
2968
  const progressUpdater = createThrottledProgressUpdater(() => {
@@ -2295,12 +2989,10 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2295
2989
  catch { /* best-effort timing */ }
2296
2990
  }
2297
2991
  }
2298
- const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
2299
- if (logFragment !== null) {
2300
- logLines.push(logFragment);
2301
- }
2302
- if (responseFragment !== null) {
2303
- responseLines.push(responseFragment);
2992
+ const parsed = parseOpencodeJsonLine(line, cwd);
2993
+ const { logFragment, responseFragment } = parsed;
2994
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2995
+ firstFinalAnswerTime = null;
2304
2996
  }
2305
2997
  if (logFragment !== null || responseFragment !== null) {
2306
2998
  progressUpdater.schedule();
@@ -2312,17 +3004,17 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2312
3004
  progressUpdater.cancel();
2313
3005
  if (remainder.trim()) {
2314
3006
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2315
- const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder, cwd);
2316
- if (logFragment !== null) {
2317
- logLines.push(logFragment);
2318
- }
2319
- if (responseFragment !== null) {
2320
- responseLines.push(responseFragment);
3007
+ const parsed = parseOpencodeJsonLine(remainder, cwd);
3008
+ if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3009
+ firstFinalAnswerTime = null;
2321
3010
  }
2322
3011
  }
2323
3012
  const stderrOutput = Buffer.concat(stderrChunks).toString();
2324
3013
  const log = logLines.join('\n\n') || stderrOutput;
2325
- const finalResponse = responseLines.join('\n\n') || stderrOutput;
3014
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
3015
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3016
+ ? recoveredResponse
3017
+ : responseLines.join('\n\n') || stderrOutput;
2326
3018
  if (reviewState.stopRequested) {
2327
3019
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
2328
3020
  resolveOnce({
@@ -2334,7 +3026,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, tool, prompt, files = [],
2334
3026
  return;
2335
3027
  }
2336
3028
  const modelError = isModelResolutionError(log);
2337
- if (code === 0 && !modelError) {
3029
+ if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2338
3030
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
2339
3031
  resolveOnce({
2340
3032
  completionStatus: 'done',