@straiffi/archon 1.8.12 → 1.8.14

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 (139) hide show
  1. package/dist/client/assets/{ActivityView-CEETnHri.js → ActivityView-DYDYyqIN.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-CjBW65OR.js → GitHubActionsDialog-jhi4GOyV.js} +1 -1
  3. package/dist/client/assets/{RepositoryGraphDialog-F3C0n89W.js → RepositoryGraphDialog-BZV0vHu6.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-DD1CLydA.js → VerificationDialog-Sc6e8AKH.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-COl_GdZ6.js → architectureDiagram-3BPJPVTR-DFbki8mi.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-qojIlH-q.js → blockDiagram-GPEHLZMM-BZFehG8P.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-DFtG4fMg.js → c4Diagram-AAUBKEIU-FVDXRu1t.js} +1 -1
  8. package/dist/client/assets/channel-D1kOpezG.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-TJkOJXbn.js → chunk-2J33WTMH-4tuVZaT0.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-CELQ603z.js → chunk-3OPIFGDE-DSn14S35.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-BgPEJ16b.js → chunk-5ZQYHXKU-Cwx1KNjg.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-Biph-aPf.js → chunk-727SXJPM-Odza4hes.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-CPVToZH6.js → chunk-AQP2D5EJ-BmVoQT7M.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-BHO9oB4A.js → chunk-CSCIHK7Q-Bg7Ktx0B.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-B8vatrko.js → chunk-KSCS5N6A-2tnhar2x.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-DvzBHR4-.js → chunk-L5ZTLDWV-dAyg8qMJ.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-Bm8cj-JK.js → chunk-LZXEDZCA-0gElxrlR.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-BPQZvPl9.js → chunk-ND2GUHAM-Bu-8Lz91.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-DEQVA6Ya.js → chunk-NZK2D7GU-CInZFdzd.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-hPGqWilt.js → chunk-O5CBEL6O-rAzpvdNR.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-D0VvFXkQ.js → chunk-WU5MYG2G-DOOw2vq7.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-C253nwny.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DFgDA23g.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-rmJlMuw_.js → dagre-BM42HDAG-Dgua1iuh.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-BqejYr09.js → diagram-2AECGRRQ-odacwlwu.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-DGSf1BNT.js → diagram-5GNKFQAL-BiKjFmJ1.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-e_TRdqvx.js → diagram-KO2AKTUF-CmYcvFri.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-CTSnwgUq.js → diagram-LMA3HP47-ncx0RBuX.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-mUn9Y0aC.js → diagram-OG6HWLK6-DbchKIzw.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-TmdWUwfA.js → erDiagram-TEJ5UH35-CMnpvRyQ.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-CoE0KTIp.js → flowDiagram-I6XJVG4X-Bi02_PK5.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-FkX1d1D7.js → ganttDiagram-6RSMTGT7-BiS992US.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CXze3DV2.js → gitGraphDiagram-PVQCEYII-Cc1hrTsT.js} +1 -1
  34. package/dist/client/assets/index-B1gdJKwK.js +275 -0
  35. package/dist/client/assets/index-CCQwWUKF.css +2 -0
  36. package/dist/client/assets/{infoDiagram-5YYISTIA-CO0CivOy.js → infoDiagram-5YYISTIA-BKVfqJCe.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-pEp8gScG.js → ishikawaDiagram-YF4QCWOH-B-IbFOty.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-JHISSGLW-BAnBAX-u.js → journeyDiagram-JHISSGLW-DAZBgAQW.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-UN3LZRKU-Cqe-m5fR.js → kanban-definition-UN3LZRKU-Bkr_fXev.js} +1 -1
  40. package/dist/client/assets/{line-CKXoUXdX.js → line-CROuuXDO.js} +1 -1
  41. package/dist/client/assets/{mermaid-parser.core-7XZHbu9R.js → mermaid-parser.core-DrBw_Rsc.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-CsF9gbdO.js → mermaid.core-pPd6OYYj.js} +3 -3
  43. package/dist/client/assets/{mindmap-definition-RKZ34NQL-88fK1Uq_.js → mindmap-definition-RKZ34NQL-BXSd3TZU.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-4H26LBE5-DHFlSAwL.js → pieDiagram-4H26LBE5-DoT7PIZC.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D5WyQ5EU.js → quadrantDiagram-W4KKPZXB-ycZkLowm.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DGkMfp_N.js → requirementDiagram-4Y6WPE33-BPkO1yLu.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-loJAHDiT.js → sankeyDiagram-5OEKKPKP-D7AvhhCw.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-xgcwtjyW.js → sequenceDiagram-3UESZ5HK-C982swnH.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-AJRCARHV-BB95DmQX.js → stateDiagram-AJRCARHV-xvJ3u8lc.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BKQG8NCn.js +1 -0
  51. package/dist/client/assets/{timeline-definition-PNZ67QCA-OCHTEVfL.js → timeline-definition-PNZ67QCA-D89qjzb5.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-CIIHVFJN-Bo_7YNy2.js → vennDiagram-CIIHVFJN-CoKeC__u.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DquywRz0.js → wardleyDiagram-YWT4CUSO-DKhjzvSE.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BLNrbCwd.js → xychartDiagram-2RQKCTM6-DFn9ass1.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/server/db.js +77 -2
  57. package/dist/server/db.js.map +1 -1
  58. package/dist/server/index.js +90 -17
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/lib/agent.js +205 -242
  61. package/dist/server/lib/agent.js.map +1 -1
  62. package/dist/server/lib/agentProvider.js +64 -0
  63. package/dist/server/lib/agentProvider.js.map +1 -0
  64. package/dist/server/lib/agentRuns.js +1 -0
  65. package/dist/server/lib/agentRuns.js.map +1 -1
  66. package/dist/server/lib/automationProgression.js +6 -2
  67. package/dist/server/lib/automationProgression.js.map +1 -1
  68. package/dist/server/lib/automationReconciliation.js +20 -0
  69. package/dist/server/lib/automationReconciliation.js.map +1 -1
  70. package/dist/server/lib/automationResumeDecision.js +6 -5
  71. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  72. package/dist/server/lib/buildCompletionContinuation.js +1 -1
  73. package/dist/server/lib/buildCompletionContinuation.js.map +1 -1
  74. package/dist/server/lib/claudeAgentProvider.js +48 -0
  75. package/dist/server/lib/claudeAgentProvider.js.map +1 -0
  76. package/dist/server/lib/executionEnvironment.js +20 -0
  77. package/dist/server/lib/executionEnvironment.js.map +1 -0
  78. package/dist/server/lib/executionReconciliation.js +43 -4
  79. package/dist/server/lib/executionReconciliation.js.map +1 -1
  80. package/dist/server/lib/executionRetry.js +16 -9
  81. package/dist/server/lib/executionRetry.js.map +1 -1
  82. package/dist/server/lib/executionRetryPredicates.js +12 -3
  83. package/dist/server/lib/executionRetryPredicates.js.map +1 -1
  84. package/dist/server/lib/git.js +4 -4
  85. package/dist/server/lib/git.js.map +1 -1
  86. package/dist/server/lib/hostExecutionEnvironment.js +77 -0
  87. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -0
  88. package/dist/server/lib/lightweightPrompt.js +17 -54
  89. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  90. package/dist/server/lib/opencodeAgentProvider.js +139 -0
  91. package/dist/server/lib/opencodeAgentProvider.js.map +1 -0
  92. package/dist/server/lib/opencodeErrors.js +35 -0
  93. package/dist/server/lib/opencodeErrors.js.map +1 -0
  94. package/dist/server/lib/opencodePromptOutput.js +112 -0
  95. package/dist/server/lib/opencodePromptOutput.js.map +1 -0
  96. package/dist/server/lib/opencodeSessionOperations.js +348 -0
  97. package/dist/server/lib/opencodeSessionOperations.js.map +1 -0
  98. package/dist/server/lib/opencodeTaskSessions.js +13 -225
  99. package/dist/server/lib/opencodeTaskSessions.js.map +1 -1
  100. package/dist/server/lib/postBuildCheckRuns.js +16 -12
  101. package/dist/server/lib/postBuildCheckRuns.js.map +1 -1
  102. package/dist/server/lib/postBuildChecks.js +45 -43
  103. package/dist/server/lib/postBuildChecks.js.map +1 -1
  104. package/dist/server/lib/previewProxy.js +36 -4
  105. package/dist/server/lib/previewProxy.js.map +1 -1
  106. package/dist/server/lib/projectAutoConfig.js +27 -97
  107. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  108. package/dist/server/lib/projects.js +34 -6
  109. package/dist/server/lib/projects.js.map +1 -1
  110. package/dist/server/lib/reviewContext.js +10 -5
  111. package/dist/server/lib/reviewContext.js.map +1 -1
  112. package/dist/server/lib/reviewFindings.js +201 -27
  113. package/dist/server/lib/reviewFindings.js.map +1 -1
  114. package/dist/server/lib/reviewerConfig.js +23 -0
  115. package/dist/server/lib/reviewerConfig.js.map +1 -0
  116. package/dist/server/lib/specReviewOperations.js +64 -9
  117. package/dist/server/lib/specReviewOperations.js.map +1 -1
  118. package/dist/server/lib/ticketFollowUpOperations.js +28 -0
  119. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  120. package/dist/server/lib/tickets.js +15 -0
  121. package/dist/server/lib/tickets.js.map +1 -1
  122. package/dist/server/lib/types.js.map +1 -1
  123. package/dist/server/workers/build.js +8 -1
  124. package/dist/server/workers/build.js.map +1 -1
  125. package/dist/server/workers/followUp.js +15 -6
  126. package/dist/server/workers/followUp.js.map +1 -1
  127. package/dist/server/workers/plan.js +5 -21
  128. package/dist/server/workers/plan.js.map +1 -1
  129. package/dist/server/workers/review.js +176 -70
  130. package/dist/server/workers/review.js.map +1 -1
  131. package/dist/server/workers/specReview.js +26 -11
  132. package/dist/server/workers/specReview.js.map +1 -1
  133. package/package.json +1 -1
  134. package/dist/client/assets/channel-DJLjk5--.js +0 -1
  135. package/dist/client/assets/classDiagram-4FO5ZUOK-aHXCyXE_.js +0 -1
  136. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CRPsGis_.js +0 -1
  137. package/dist/client/assets/index-B9S66NLn.css +0 -2
  138. package/dist/client/assets/index-CHsj9z-M.js +0 -275
  139. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DKhxPOLT.js +0 -1
@@ -2,56 +2,29 @@ import { spawn } from 'child_process';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { readFileSync } from 'fs';
4
4
  import { extname, isAbsolute, join, relative, resolve, sep } from 'path';
5
- import Database from 'better-sqlite3';
6
5
  import db from '../db.js';
7
6
  import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
8
7
  import { normalizeClaudeModelId, normalizeModelId } from './models.js';
9
8
  import { createOpencodeTaskSessionReader, parseOpencodeTaskEnvelope } from './opencodeTaskSessions.js';
9
+ import { hostOpencodeSessionOperations } from './opencodeSessionOperations.js';
10
+ import { hostExecutionEnvironment } from './hostExecutionEnvironment.js';
11
+ import { claudeAgentProvider } from './claudeAgentProvider.js';
12
+ import { buildOpencodeSpecReviewInvocation, createOpencodeExitError, createOpencodeRecoveryTracker, interpretOpencodeRecoveryEvent, isSuccessfulOpencodeExit, opencodeAgentProvider, } from './opencodeAgentProvider.js';
10
13
  import { appendClaudeHeadlessPlanPrompt, buildClaudeHeadlessPlanArgs } from './planModeQuestions.js';
11
14
  import { formatGenericToolCall } from './genericToolCall.js';
12
15
  import { getSpecSession, updateSpecSessionResumeId, updateSpecSessionStatus, updateSpecSessionTitle, updateSpecSessionUsageSnapshot } from './specs.js';
13
16
  import { updateSpecReviewRunStatus, updateSpecReviewRunStreaming, updateSpecReviewRunUsage } from './specReviews.js';
14
17
  import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForExecutionRun, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
15
- import { resolveOpencodeDbPath } from './runtimePaths.js';
16
18
  import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
17
19
  import { getTicket } from './tickets.js';
18
20
  import { createTranscriptRunSnapshots } from './transcriptRuns.js';
19
21
  import { createWorkspaceProcessEnvironment } from './workspaceProcessEnvironment.js';
22
+ import { OpencodeModelResolutionError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
23
+ import { extractLeadingOpencodeJsonObjects } from './opencodePromptOutput.js';
20
24
  import { assertAgentSpawnPermit, AgentSpawnPermitRevokedError } from './agentSpawnPermits.js';
21
25
  const withTranscriptRuns = (value, transcriptRuns) => {
22
26
  return transcriptRuns ? { ...value, transcriptRuns } : value;
23
27
  };
24
- const createOpencodeRecoveryTracker = () => {
25
- let sawError = false;
26
- let latestEvent = null;
27
- let recoveredFinalAnswerFragments = [];
28
- return {
29
- consume: events => {
30
- let shouldResetResponse = false;
31
- for (const event of events ?? []) {
32
- latestEvent = event.type;
33
- if (event.type === 'error') {
34
- sawError = true;
35
- recoveredFinalAnswerFragments = [];
36
- shouldResetResponse = true;
37
- continue;
38
- }
39
- if (sawError) {
40
- recoveredFinalAnswerFragments.push(event.text);
41
- }
42
- }
43
- return shouldResetResponse;
44
- },
45
- hasRecoveredFinalAnswer: () => (sawError
46
- && latestEvent === 'final_answer'
47
- && recoveredFinalAnswerFragments.length > 0),
48
- hasUnrecoveredError: () => latestEvent === 'error',
49
- getRecoveredResponse: () => recoveredFinalAnswerFragments.join('\n\n'),
50
- };
51
- };
52
- const isSuccessfulOpencodeExit = (code, modelError, recoveryTracker) => (!modelError
53
- && !recoveryTracker.hasUnrecoveredError()
54
- && (code === 0 || recoveryTracker.hasRecoveredFinalAnswer()));
55
28
  export class AgentContinuationRejectedError extends Error {
56
29
  constructor(message) {
57
30
  super(message);
@@ -74,7 +47,6 @@ export const getAgentErrorTranscriptRuns = (error) => {
74
47
  const transcriptRuns = error.transcriptRuns;
75
48
  return Array.isArray(transcriptRuns) && transcriptRuns.length > 0 ? transcriptRuns : null;
76
49
  };
77
- const OPENCODE_PLACEHOLDER_TITLE_PATTERN = /^New session\b/i;
78
50
  const OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS = 1500;
79
51
  const AGENT_PROGRESS_UPDATE_INTERVAL_MS = 100;
80
52
  const resolveAgentModel = (tool, model) => {
@@ -140,13 +112,6 @@ const buildClaudeArgs = (mode, model, resumeSessionId, sessionId) => {
140
112
  sessionId,
141
113
  ];
142
114
  };
143
- const OPENCODE_SESSION_BY_ID_QUERY = 'SELECT id, title FROM session WHERE id = ? LIMIT 1';
144
- const OPENCODE_SESSION_BY_CREATED_AT_QUERY = 'SELECT id, title FROM session WHERE time_created > ? ORDER BY time_created DESC LIMIT 1';
145
- const OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY = "SELECT data FROM message WHERE session_id = ? AND json_extract(data, '$.role') = 'assistant' AND json_extract(data, '$.time.completed') IS NOT NULL ORDER BY time_created DESC LIMIT 1";
146
- const OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY = "SELECT data FROM message WHERE session_id = ? AND time_created >= ? AND json_extract(data, '$.role') = 'assistant' AND json_extract(data, '$.time.completed') IS NOT NULL ORDER BY time_created DESC LIMIT 1";
147
- const OPENCODE_STEP_FINISH_USAGE_QUERY = "SELECT data FROM part WHERE session_id = ? AND json_extract(data, '$.type') = 'step-finish' ORDER BY time_created DESC LIMIT 1";
148
- const OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY = "SELECT data FROM part WHERE session_id = ? AND time_created >= ? AND json_extract(data, '$.type') = 'step-finish' ORDER BY time_created DESC LIMIT 1";
149
- const OPENCODE_SESSION_COST_QUERY = 'SELECT cost FROM session WHERE id = ?';
150
115
  const GET_PERSISTED_CHAT_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM chat_sessions WHERE id = ?';
151
116
  const GET_PERSISTED_SPEC_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM spec_sessions WHERE id = ?';
152
117
  export const EMPTY_CHAT_USAGE_SNAPSHOT = {
@@ -284,7 +249,7 @@ export const mergeUsageFromJsonLine = (current, line) => {
284
249
  if (!trimmedLine.startsWith('{')) {
285
250
  return current;
286
251
  }
287
- const { jsonObjects } = extractLeadingJsonObjects(trimmedLine);
252
+ const { jsonObjects } = extractLeadingOpencodeJsonObjects(trimmedLine);
288
253
  let next = current;
289
254
  for (const jsonObject of jsonObjects) {
290
255
  try {
@@ -310,9 +275,6 @@ export const mergeUsageFromJsonLine = (current, line) => {
310
275
  }
311
276
  return next;
312
277
  };
313
- const hasUsageSnapshot = (value) => {
314
- return Object.values(value).some(candidate => candidate !== null && candidate !== undefined);
315
- };
316
278
  const hasMatchingUsageSnapshot = (current, next) => {
317
279
  return current.usage_snapshot_context_window === next.usage_snapshot_context_window
318
280
  && current.usage_snapshot_max_output_tokens === next.usage_snapshot_max_output_tokens
@@ -483,67 +445,12 @@ const createThrottledProgressUpdater = (flushUpdate) => {
483
445
  },
484
446
  };
485
447
  };
486
- const openOpencodeDatabase = () => new Database(resolveOpencodeDbPath(), { readonly: true });
487
- const readOpencodeSession = (statement, value) => {
488
- const opencodeDb = openOpencodeDatabase();
489
- try {
490
- const row = opencodeDb.prepare(statement).get(value);
491
- if (!row?.id) {
492
- return null;
493
- }
494
- const normalizedTitle = typeof row.title === 'string'
495
- ? row.title.trim() || null
496
- : null;
497
- const title = normalizedTitle && !OPENCODE_PLACEHOLDER_TITLE_PATTERN.test(normalizedTitle)
498
- ? normalizedTitle
499
- : null;
500
- return {
501
- id: row.id,
502
- title,
503
- };
504
- }
505
- finally {
506
- opencodeDb.close();
507
- }
508
- };
509
448
  export const readOpencodeUsageSnapshot = ({ sessionId, spawnTimestamp = null, }) => {
510
- const opencodeDb = openOpencodeDatabase();
511
- try {
512
- const costRow = opencodeDb.prepare(OPENCODE_SESSION_COST_QUERY).get(sessionId);
513
- const sessionCost = typeof costRow?.cost === 'number' ? { usage_snapshot_cost: costRow.cost } : {};
514
- const completedAssistantMessage = (spawnTimestamp === null
515
- ? opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY).get(sessionId)
516
- : opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
517
- if (typeof completedAssistantMessage?.data === 'string') {
518
- try {
519
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(completedAssistantMessage.data));
520
- if (hasUsageSnapshot(parsedUsage)) {
521
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
522
- }
523
- }
524
- catch {
525
- // Fall through to the step-finish lookup below.
526
- }
527
- }
528
- const latestStepFinish = (spawnTimestamp === null
529
- ? opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_QUERY).get(sessionId)
530
- : opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
531
- if (typeof latestStepFinish?.data === 'string') {
532
- try {
533
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(latestStepFinish.data));
534
- if (hasUsageSnapshot(parsedUsage)) {
535
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
536
- }
537
- }
538
- catch {
539
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
540
- }
541
- }
542
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
543
- }
544
- finally {
545
- opencodeDb.close();
449
+ const snapshot = hostOpencodeSessionOperations.readUsageSnapshot(sessionId, spawnTimestamp);
450
+ if (!snapshot) {
451
+ throw new Error('OpenCode usage snapshot read failed');
546
452
  }
453
+ return snapshot;
547
454
  };
548
455
  const resolveOpencodeUsageSnapshot = ({ current, sessionId, spawnTimestamp = null, }) => {
549
456
  try {
@@ -638,12 +545,12 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
638
545
  };
639
546
  const readCurrentSession = () => {
640
547
  if (knownSessionId) {
641
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
548
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
642
549
  }
643
550
  if (spawnTimestamp === null) {
644
551
  return null;
645
552
  }
646
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
553
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
647
554
  };
648
555
  const refresh = () => {
649
556
  if (stopped) {
@@ -706,12 +613,12 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
706
613
  };
707
614
  const readCurrentSession = () => {
708
615
  if (knownSessionId) {
709
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
616
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
710
617
  }
711
618
  if (spawnTimestamp === null) {
712
619
  return null;
713
620
  }
714
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
621
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
715
622
  };
716
623
  const refresh = () => {
717
624
  if (stopped) {
@@ -956,6 +863,7 @@ const emptyParsedStreamLine = () => ({
956
863
  responseFragment: null,
957
864
  subagentEvents: [],
958
865
  providerSessionId: null,
866
+ modelResolutionError: false,
959
867
  });
960
868
  const isExecutionCapableParsedStreamLine = (parsed) => (parsed.logFragment !== null
961
869
  || parsed.responseFragment !== null
@@ -1067,62 +975,6 @@ const formatOpencodeStepTitle = (event) => {
1067
975
  const title = event.part?.title?.trim();
1068
976
  return title || null;
1069
977
  };
1070
- const extractLeadingJsonObject = (line) => {
1071
- if (!line.startsWith('{')) {
1072
- return null;
1073
- }
1074
- let depth = 0;
1075
- let inString = false;
1076
- let escaping = false;
1077
- for (let index = 0; index < line.length; index += 1) {
1078
- const char = line[index];
1079
- if (inString) {
1080
- if (escaping) {
1081
- escaping = false;
1082
- continue;
1083
- }
1084
- if (char === '\\') {
1085
- escaping = true;
1086
- continue;
1087
- }
1088
- if (char === '"') {
1089
- inString = false;
1090
- }
1091
- continue;
1092
- }
1093
- if (char === '"') {
1094
- inString = true;
1095
- continue;
1096
- }
1097
- if (char === '{') {
1098
- depth += 1;
1099
- continue;
1100
- }
1101
- if (char === '}') {
1102
- depth -= 1;
1103
- if (depth === 0) {
1104
- return line.slice(0, index + 1);
1105
- }
1106
- }
1107
- }
1108
- return null;
1109
- };
1110
- const extractLeadingJsonObjects = (line) => {
1111
- const jsonObjects = [];
1112
- let remaining = line.trim();
1113
- while (remaining.startsWith('{')) {
1114
- const jsonObject = extractLeadingJsonObject(remaining);
1115
- if (!jsonObject) {
1116
- break;
1117
- }
1118
- jsonObjects.push(jsonObject);
1119
- remaining = remaining.slice(jsonObject.length).trim();
1120
- }
1121
- return {
1122
- jsonObjects,
1123
- remainder: remaining,
1124
- };
1125
- };
1126
978
  const parseOpencodeJsonEvent = (event, cwd) => {
1127
979
  const providerSessionId = event.sessionID?.trim()
1128
980
  || event.sessionId?.trim()
@@ -1132,14 +984,14 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1132
984
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1133
985
  const text = event.part.text.trim();
1134
986
  const phase = event.part.metadata?.openai?.phase;
987
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, text, phase);
1135
988
  return {
1136
989
  logFragment: text || null,
1137
990
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
1138
991
  subagentEvents: [],
1139
992
  providerSessionId,
1140
- recoveryEvents: text && phase === 'final_answer'
1141
- ? [{ type: 'final_answer', text }]
1142
- : [],
993
+ modelResolutionError: false,
994
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1143
995
  };
1144
996
  }
1145
997
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -1149,6 +1001,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1149
1001
  responseFragment: null,
1150
1002
  subagentEvents: [],
1151
1003
  providerSessionId,
1004
+ modelResolutionError: false,
1152
1005
  };
1153
1006
  }
1154
1007
  if (event.type === 'tool' || event.type === 'tool_use' || event.type === 'tool_result') {
@@ -1188,6 +1041,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1188
1041
  : []),
1189
1042
  ],
1190
1043
  providerSessionId,
1044
+ modelResolutionError: false,
1191
1045
  };
1192
1046
  }
1193
1047
  if (!isToolInvocation) {
@@ -1199,6 +1053,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1199
1053
  responseFragment: null,
1200
1054
  subagentEvents: [],
1201
1055
  providerSessionId,
1056
+ modelResolutionError: false,
1202
1057
  };
1203
1058
  }
1204
1059
  if (event.type === 'step_start') {
@@ -1207,6 +1062,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1207
1062
  responseFragment: null,
1208
1063
  subagentEvents: [],
1209
1064
  providerSessionId,
1065
+ modelResolutionError: false,
1210
1066
  };
1211
1067
  }
1212
1068
  if (event.type === 'step_finish') {
@@ -1215,23 +1071,19 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1215
1071
  responseFragment: null,
1216
1072
  subagentEvents: [],
1217
1073
  providerSessionId,
1074
+ modelResolutionError: false,
1218
1075
  };
1219
1076
  }
1220
1077
  if (event.type === 'error') {
1221
- const errorText = typeof event.error === 'string'
1222
- ? event.error.trim()
1223
- : event.error && typeof event.error === 'object'
1224
- ? (event.error.data?.message?.trim()
1225
- || event.error.message?.trim()
1226
- || JSON.stringify(event.error))
1227
- : '';
1228
- const text = event.part?.text?.trim() || errorText || '';
1078
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, event.part?.text, undefined);
1079
+ const parsedError = recoveryEvent?.type === 'error' ? recoveryEvent.error : null;
1080
+ const text = parsedError?.message ?? '';
1229
1081
  return {
1230
1082
  logFragment: text ? `**Error:** ${text}` : null,
1231
1083
  responseFragment: null,
1232
1084
  subagentEvents: [],
1233
1085
  providerSessionId,
1234
- recoveryEvents: [{ type: 'error' }],
1086
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1235
1087
  };
1236
1088
  }
1237
1089
  return emptyParsedStreamLine();
@@ -1241,7 +1093,7 @@ const parseOpencodeJsonLine = (line, cwd) => {
1241
1093
  if (!trimmed) {
1242
1094
  return emptyParsedStreamLine();
1243
1095
  }
1244
- const { jsonObjects, remainder } = extractLeadingJsonObjects(trimmed);
1096
+ const { jsonObjects, remainder } = extractLeadingOpencodeJsonObjects(trimmed);
1245
1097
  if (jsonObjects.length === 0) {
1246
1098
  if (trimmed.startsWith('{')) {
1247
1099
  return emptyParsedStreamLine();
@@ -1259,13 +1111,14 @@ const parseOpencodeJsonLine = (line, cwd) => {
1259
1111
  }],
1260
1112
  };
1261
1113
  }
1262
- return { logFragment: trimmed, responseFragment: trimmed, subagentEvents: [], providerSessionId: null };
1114
+ return { logFragment: trimmed, responseFragment: trimmed, subagentEvents: [], providerSessionId: null, modelResolutionError: false };
1263
1115
  }
1264
1116
  const logFragments = [];
1265
1117
  const responseFragments = [];
1266
1118
  const subagentEvents = [];
1267
1119
  const recoveryEvents = [];
1268
1120
  let providerSessionId = null;
1121
+ let modelResolutionError = false;
1269
1122
  for (const jsonObject of jsonObjects) {
1270
1123
  let event;
1271
1124
  try {
@@ -1278,6 +1131,7 @@ const parseOpencodeJsonLine = (line, cwd) => {
1278
1131
  const { logFragment, responseFragment } = parsedEvent;
1279
1132
  subagentEvents.push(...parsedEvent.subagentEvents);
1280
1133
  recoveryEvents.push(...(parsedEvent.recoveryEvents ?? []));
1134
+ modelResolutionError ||= Boolean(parsedEvent.modelResolutionError);
1281
1135
  providerSessionId = providerSessionId ?? parsedEvent.providerSessionId;
1282
1136
  if (logFragment !== null) {
1283
1137
  logFragments.push(logFragment);
@@ -1308,6 +1162,7 @@ const parseOpencodeJsonLine = (line, cwd) => {
1308
1162
  responseFragment: responseFragments.length > 0 ? responseFragments.join('\n\n') : null,
1309
1163
  subagentEvents,
1310
1164
  providerSessionId,
1165
+ modelResolutionError,
1311
1166
  recoveryEvents,
1312
1167
  };
1313
1168
  }
@@ -1731,12 +1586,7 @@ const buildResponseTiming = ({ startTime, firstFinalAnswerTime, isStopped, }) =>
1731
1586
  const responseMs = Date.now() - firstFinalAnswerTime;
1732
1587
  return { totalMs, thinkingMs, responseMs };
1733
1588
  };
1734
- const isModelResolutionError = (message) => {
1735
- if (typeof message !== 'string') {
1736
- return false;
1737
- }
1738
- return /ProviderModelNotFoundError|Model not found/i.test(message);
1739
- };
1589
+ const isLegacyModelResolutionError = (message) => (typeof message === 'string' && /ProviderModelNotFoundError|Model not found/i.test(message));
1740
1590
  const isProcessAlive = (proc) => {
1741
1591
  if (!proc) {
1742
1592
  return false;
@@ -1764,6 +1614,16 @@ const killProcess = (proc, signal) => {
1764
1614
  catch {
1765
1615
  }
1766
1616
  };
1617
+ const interruptProcess = (state, signal) => {
1618
+ if (!isProcessAlive(state.child)) {
1619
+ return;
1620
+ }
1621
+ if (state.interrupt) {
1622
+ state.interrupt(signal);
1623
+ return;
1624
+ }
1625
+ killProcess(state.child, signal);
1626
+ };
1767
1627
  const waitForProcessExit = async (proc, timeoutMs) => {
1768
1628
  const deadline = Date.now() + timeoutMs;
1769
1629
  await new Promise(resolve => {
@@ -1797,6 +1657,7 @@ const untrackChatProcess = (chatSessionId, state) => {
1797
1657
  export const registerChatProcessPlaceholder = (chatSessionId) => {
1798
1658
  const state = {
1799
1659
  child: null,
1660
+ interrupt: null,
1800
1661
  researchChildren: new Set(),
1801
1662
  stopRequested: false,
1802
1663
  metadataRefresher: null,
@@ -1805,7 +1666,7 @@ export const registerChatProcessPlaceholder = (chatSessionId) => {
1805
1666
  return state;
1806
1667
  };
1807
1668
  export const registerBuildProcessPlaceholder = (ticketId) => {
1808
- const state = { child: null, stopRequested: false, sessionLookupTimer: null };
1669
+ const state = { child: null, interrupt: null, stopRequested: false, sessionLookupTimer: null };
1809
1670
  activeBuildProcesses.set(ticketId, state);
1810
1671
  return state;
1811
1672
  };
@@ -1836,6 +1697,7 @@ const untrackSpecProcess = (specSessionId, state) => {
1836
1697
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1837
1698
  const state = {
1838
1699
  child: null,
1700
+ interrupt: null,
1839
1701
  researchChildren: new Set(),
1840
1702
  stopRequested: false,
1841
1703
  metadataRefresher: null,
@@ -1859,6 +1721,7 @@ const untrackSpecReviewProcess = (specReviewRunId, state) => {
1859
1721
  export const registerSpecReviewProcessPlaceholder = (specReviewRunId) => {
1860
1722
  const state = {
1861
1723
  child: null,
1724
+ interrupt: null,
1862
1725
  researchChildren: new Set(),
1863
1726
  stopRequested: false,
1864
1727
  metadataRefresher: null,
@@ -1904,10 +1767,20 @@ export const stopBuildAgent = async (ticketId) => {
1904
1767
  if (!state.child) {
1905
1768
  return true;
1906
1769
  }
1907
- killProcess(state.child, 'SIGTERM');
1770
+ if (state.interrupt) {
1771
+ state.interrupt('SIGTERM');
1772
+ }
1773
+ else {
1774
+ killProcess(state.child, 'SIGTERM');
1775
+ }
1908
1776
  await waitForProcessExit(state.child, 5000);
1909
1777
  if (isProcessAlive(state.child)) {
1910
- killProcess(state.child, 'SIGKILL');
1778
+ if (state.interrupt) {
1779
+ state.interrupt('SIGKILL');
1780
+ }
1781
+ else {
1782
+ killProcess(state.child, 'SIGKILL');
1783
+ }
1911
1784
  await waitForProcessExit(state.child, 1000);
1912
1785
  }
1913
1786
  return true;
@@ -1983,15 +1856,15 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1983
1856
  if (!state.child) {
1984
1857
  return true;
1985
1858
  }
1986
- killProcess(state.child, 'SIGTERM');
1859
+ interruptProcess(state, 'SIGTERM');
1987
1860
  await waitForProcessExit(state.child, 5000);
1988
1861
  if (isProcessAlive(state.child)) {
1989
- killProcess(state.child, 'SIGKILL');
1862
+ interruptProcess(state, 'SIGKILL');
1990
1863
  await waitForProcessExit(state.child, 1000);
1991
1864
  }
1992
1865
  return true;
1993
1866
  };
1994
- export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, }) => {
1867
+ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, executionProvider = hostExecutionEnvironment, }) => {
1995
1868
  const resolvedModel = resolveAgentModel(tool, model);
1996
1869
  const subagentTracker = createSubagentRunTracker({
1997
1870
  parentType: 'ticket',
@@ -2089,7 +1962,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2089
1962
  return hasResolvedOpencodeSessionId;
2090
1963
  }
2091
1964
  try {
2092
- const session = readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
1965
+ const session = hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
2093
1966
  if (session?.id) {
2094
1967
  bindResolvedOpencodeSession(session.id);
2095
1968
  }
@@ -2099,15 +1972,19 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2099
1972
  }
2100
1973
  return hasResolvedOpencodeSessionId;
2101
1974
  };
2102
- const [cmd, args] = tool === 'claude'
2103
- ? (() => {
2104
- sessionId = resumeSessionId || randomUUID();
2105
- return [
2106
- 'claude',
2107
- buildClaudeArgs(opencodeAgentMode, resolvedModel, resumeSessionId, sessionId),
2108
- ];
2109
- })()
2110
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1975
+ const agentProvider = tool === 'claude'
1976
+ ? claudeAgentProvider
1977
+ : opencodeAgentProvider;
1978
+ const invocation = agentProvider.buildStreamingInvocation({
1979
+ cwd,
1980
+ prompt,
1981
+ model: resolvedModel,
1982
+ variant,
1983
+ mode: opencodeAgentMode,
1984
+ resumeSessionId,
1985
+ sessionId: randomUUID(),
1986
+ });
1987
+ sessionId = invocation.sessionId;
2111
1988
  writeRunStatus('running', null);
2112
1989
  if (tool === 'claude' && sessionId) {
2113
1990
  writeRunSessionId(sessionId);
@@ -2116,6 +1993,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2116
1993
  spawnTimestamp = Date.now();
2117
1994
  }
2118
1995
  let child;
1996
+ let interrupt;
1997
+ let placementFailure;
2119
1998
  try {
2120
1999
  if (buildState?.stopRequested) {
2121
2000
  throw new AgentSpawnPermitRevokedError();
@@ -2123,28 +2002,37 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2123
2002
  if (spawnPermitId && executionContext) {
2124
2003
  assertAgentSpawnPermit(spawnPermitId, executionContext.runId);
2125
2004
  }
2126
- child = spawn(cmd, args, {
2005
+ const handle = executionProvider.execute({
2006
+ target: {
2007
+ kind: 'worktree',
2008
+ id: ticketId,
2009
+ workspacePath: cwd,
2010
+ revision: 'working-tree',
2011
+ },
2012
+ command: invocation.command,
2013
+ args: invocation.args,
2127
2014
  cwd,
2128
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2015
+ stdio: invocation.stdio,
2129
2016
  env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
2130
2017
  detached: process.platform !== 'win32',
2131
2018
  });
2019
+ child = handle.child;
2020
+ interrupt = handle.interrupt;
2021
+ placementFailure = handle.placementFailure;
2132
2022
  }
2133
2023
  catch (error) {
2134
- const resolvedError = error instanceof Error ? error : new Error(String(error));
2024
+ const resolvedError = agentProvider.classifyExecutionFailure(error);
2135
2025
  const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2136
2026
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2137
2027
  return;
2138
2028
  }
2139
2029
  reviewChild = child;
2140
- if (tool === 'claude') {
2141
- const claudePrompt = opencodeAgentMode === 'plan'
2142
- ? appendClaudeHeadlessPlanPrompt(prompt)
2143
- : prompt;
2144
- writeClaudeStdin(child, buildClaudeStdinMessage(claudePrompt, [], cwd));
2030
+ if (invocation.stdin !== null) {
2031
+ writeClaudeStdin(child, invocation.stdin);
2145
2032
  }
2146
2033
  if (trackBuildProcess && buildState) {
2147
2034
  buildState.child = child;
2035
+ buildState.interrupt = interrupt;
2148
2036
  activeBuildProcesses.set(ticketId, buildState);
2149
2037
  }
2150
2038
  if (reviewProcessState) {
@@ -2153,6 +2041,14 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2153
2041
  killProcess(child, 'SIGTERM');
2154
2042
  }
2155
2043
  }
2044
+ void placementFailure.then(failure => {
2045
+ if (!failure || settled) {
2046
+ return;
2047
+ }
2048
+ const resolvedError = agentProvider.classifyExecutionFailure(failure);
2049
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2050
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2051
+ });
2156
2052
  startTime = Date.now();
2157
2053
  if (tool === 'opencode' && spawnTimestamp !== null) {
2158
2054
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
@@ -2221,7 +2117,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2221
2117
  const errOutput = Buffer.concat(stderrChunks).toString();
2222
2118
  const finalLog = logLines.join('\n\n') || errOutput;
2223
2119
  const finalResponse = responseLines.join('\n\n') || errOutput;
2224
- const modelError = isModelResolutionError(finalLog);
2120
+ const modelError = isLegacyModelResolutionError(errOutput);
2225
2121
  const timing = startTime ? { totalMs: Date.now() - startTime } : null;
2226
2122
  if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2227
2123
  const transcriptRuns = finalizeRun('stopped', finalLog, finalResponse);
@@ -2247,8 +2143,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2247
2143
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null, usageSnapshot }, transcriptRuns));
2248
2144
  return;
2249
2145
  }
2250
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2251
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2146
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2147
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2148
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2252
2149
  });
2253
2150
  return;
2254
2151
  }
@@ -2331,14 +2228,14 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2331
2228
  const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
2332
2229
  ? recoveredResponse
2333
2230
  : responseLines.join('\n\n') || stderrOutput;
2334
- const modelError = isModelResolutionError(log);
2231
+ const stderrModelError = parseOpencodeStderrModelResolutionError(stderrOutput);
2335
2232
  const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested || reviewProcessState?.stopRequested) });
2336
2233
  if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2337
2234
  const transcriptRuns = finalizeRun('stopped', log, finalResponse);
2338
2235
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing }, transcriptRuns));
2339
2236
  return;
2340
2237
  }
2341
- if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2238
+ if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker)) {
2342
2239
  const transcriptRuns = finalizeRun('done', log, finalResponse, { emit: emitSuccessUpdate });
2343
2240
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing }, transcriptRuns));
2344
2241
  return;
@@ -2346,7 +2243,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2346
2243
  const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
2347
2244
  const error = resumeSessionId && !executionCapableEventObserved && isTypedPreExecutionContinuationRejection(failureMessage)
2348
2245
  ? new AgentContinuationRejectedError(failureMessage)
2349
- : new Error(failureMessage);
2246
+ : createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
2350
2247
  const transcriptRuns = finalizeRun('error', log, finalResponse);
2351
2248
  rejectOnce(attachTranscriptRunsToError(error, transcriptRuns));
2352
2249
  });
@@ -2361,8 +2258,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2361
2258
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
2362
2259
  return;
2363
2260
  }
2364
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2365
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2261
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2262
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2263
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2366
2264
  });
2367
2265
  });
2368
2266
  };
@@ -2555,7 +2453,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2555
2453
  }, transcriptRuns));
2556
2454
  return;
2557
2455
  }
2558
- const modelError = isModelResolutionError(finalLog);
2456
+ const modelError = isLegacyModelResolutionError(errOutput);
2559
2457
  if (code === 0 && !modelError) {
2560
2458
  const finalOutput = getPreservedChatOutput({
2561
2459
  chatSessionId,
@@ -2709,8 +2607,8 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2709
2607
  }, transcriptRuns));
2710
2608
  return;
2711
2609
  }
2712
- const modelError = isModelResolutionError(log);
2713
- if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2610
+ const stderrModelError = parseOpencodeStderrModelResolutionError(stderrOutput);
2611
+ if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker)) {
2714
2612
  const finalOutput = getPreservedChatOutput({
2715
2613
  chatSessionId,
2716
2614
  log,
@@ -2737,7 +2635,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2737
2635
  const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
2738
2636
  const error = resumeSessionId && !executionCapableEventObserved && isTypedPreExecutionContinuationRejection(failureMessage)
2739
2637
  ? new AgentContinuationRejectedError(failureMessage)
2740
- : new Error(failureMessage);
2638
+ : createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
2741
2639
  const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
2742
2640
  rejectOnce(attachTranscriptRunsToError(error, transcriptRuns));
2743
2641
  });
@@ -2938,7 +2836,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
2938
2836
  }, transcriptRuns));
2939
2837
  return;
2940
2838
  }
2941
- const modelError = isModelResolutionError(finalLog);
2839
+ const modelError = isLegacyModelResolutionError(errOutput);
2942
2840
  if (code === 0 && !modelError) {
2943
2841
  const finalOutput = getPreservedSpecOutput({
2944
2842
  specSessionId,
@@ -3083,8 +2981,8 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
3083
2981
  }, transcriptRuns));
3084
2982
  return;
3085
2983
  }
3086
- const modelError = isModelResolutionError(log);
3087
- if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
2984
+ const stderrModelError = parseOpencodeStderrModelResolutionError(stderrOutput);
2985
+ if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker)) {
3088
2986
  const finalOutput = getPreservedSpecOutput({
3089
2987
  specSessionId,
3090
2988
  log,
@@ -3109,7 +3007,8 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
3109
3007
  }
3110
3008
  updateSpecSessionStatusLocal(specSessionId, 'error', log, io, finalResponse);
3111
3009
  const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
3112
- rejectOnce(attachTranscriptRunsToError(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`), transcriptRuns));
3010
+ const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
3011
+ rejectOnce(attachTranscriptRunsToError(createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker), transcriptRuns));
3113
3012
  });
3114
3013
  child.on('error', err => {
3115
3014
  progressUpdater.cancel();
@@ -3166,6 +3065,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3166
3065
  });
3167
3066
  return new Promise((resolve, reject) => {
3168
3067
  let spawnTimestamp = null;
3068
+ let opencodeSessionId = null;
3169
3069
  let opencodeSubagentMonitor = null;
3170
3070
  let settled = false;
3171
3071
  let startTime = null;
@@ -3173,6 +3073,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3173
3073
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
3174
3074
  const reviewState = registeredState ?? {
3175
3075
  child: null,
3076
+ interrupt: null,
3176
3077
  researchChildren: new Set(),
3177
3078
  stopRequested: false,
3178
3079
  metadataRefresher: null,
@@ -3188,21 +3089,56 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3188
3089
  };
3189
3090
  const resolveOnce = settle(resolve);
3190
3091
  const rejectOnce = settle(reject);
3191
- const [cmd, args] = tool === 'claude'
3192
- ? [
3193
- 'claude',
3194
- buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3195
- ]
3196
- : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
3092
+ const [cmd, args] = [
3093
+ 'claude',
3094
+ buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3095
+ ];
3197
3096
  updateSpecReviewRunStreaming(specReviewRunId, null);
3198
- const child = spawn(cmd, args, {
3199
- cwd,
3200
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
3201
- env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
3202
- detached: process.platform !== 'win32',
3203
- });
3097
+ const env = createWorkspaceProcessEnvironment(cwd, { PWD: cwd });
3098
+ let child;
3099
+ if (tool === 'opencode') {
3100
+ const invocation = buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant });
3101
+ try {
3102
+ const handle = hostExecutionEnvironment.execute({
3103
+ target: {
3104
+ kind: 'repository',
3105
+ id: cwd,
3106
+ workspacePath: cwd,
3107
+ revision: 'working-tree',
3108
+ },
3109
+ command: invocation.command,
3110
+ args: invocation.args,
3111
+ cwd,
3112
+ stdio: invocation.stdio,
3113
+ env,
3114
+ detached: process.platform !== 'win32',
3115
+ });
3116
+ child = handle.child;
3117
+ reviewState.interrupt = handle.interrupt;
3118
+ void handle.placementFailure.then(failure => {
3119
+ if (failure) {
3120
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(failure));
3121
+ }
3122
+ });
3123
+ }
3124
+ catch (error) {
3125
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(error));
3126
+ return;
3127
+ }
3128
+ }
3129
+ else {
3130
+ child = spawn(cmd, args, {
3131
+ cwd,
3132
+ stdio: ['pipe', 'pipe', 'pipe'],
3133
+ env,
3134
+ detached: process.platform !== 'win32',
3135
+ });
3136
+ }
3204
3137
  reviewState.child = child;
3205
3138
  activeSpecReviewProcesses.set(specReviewRunId, reviewState);
3139
+ if (reviewState.stopRequested) {
3140
+ interruptProcess(reviewState, 'SIGTERM');
3141
+ }
3206
3142
  if (tool === 'claude') {
3207
3143
  writeClaudeStdin(child, buildClaudeStdinMessage(appendClaudeHeadlessPlanPrompt(prompt), files, cwd));
3208
3144
  }
@@ -3272,7 +3208,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3272
3208
  });
3273
3209
  return;
3274
3210
  }
3275
- const modelError = isModelResolutionError(finalLog);
3211
+ const modelError = isLegacyModelResolutionError(errOutput);
3276
3212
  if (code === 0 && !modelError) {
3277
3213
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3278
3214
  subagentTracker.finalize('done', finalLog || null, finalResponse || null);
@@ -3326,6 +3262,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3326
3262
  const parsed = parseOpencodeJsonLine(line, cwd);
3327
3263
  const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
3328
3264
  opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3265
+ opencodeSessionId = providerSessionId ?? opencodeSessionId;
3329
3266
  subagentTracker.handle(subagentEvents);
3330
3267
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3331
3268
  firstFinalAnswerTime = null;
@@ -3343,6 +3280,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3343
3280
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3344
3281
  const parsed = parseOpencodeJsonLine(remainder, cwd);
3345
3282
  opencodeSubagentMonitor?.setParentSessionId(parsed.providerSessionId);
3283
+ opencodeSessionId = parsed.providerSessionId ?? opencodeSessionId;
3346
3284
  subagentTracker.handle(parsed.subagentEvents);
3347
3285
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3348
3286
  firstFinalAnswerTime = null;
@@ -3354,6 +3292,13 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3354
3292
  const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3355
3293
  ? recoveredResponse
3356
3294
  : responseLines.join('\n\n') || stderrOutput;
3295
+ if (opencodeSessionId && spawnTimestamp !== null) {
3296
+ usageSnapshot = resolveOpencodeUsageSnapshot({
3297
+ current: usageSnapshot,
3298
+ sessionId: opencodeSessionId,
3299
+ spawnTimestamp,
3300
+ });
3301
+ }
3357
3302
  if (reviewState.stopRequested) {
3358
3303
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3359
3304
  subagentTracker.finalize('stopped', log || null, finalResponse || null);
@@ -3365,8 +3310,8 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3365
3310
  });
3366
3311
  return;
3367
3312
  }
3368
- const modelError = isModelResolutionError(log);
3369
- if (isSuccessfulOpencodeExit(code, modelError, recoveryTracker)) {
3313
+ const stderrModelError = parseOpencodeStderrModelResolutionError(stderrOutput);
3314
+ if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker)) {
3370
3315
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3371
3316
  subagentTracker.finalize('done', log || null, finalResponse || null);
3372
3317
  resolveOnce({
@@ -3378,12 +3323,30 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3378
3323
  return;
3379
3324
  }
3380
3325
  subagentTracker.finalize('error', log || null, finalResponse || null);
3381
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
3326
+ const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
3327
+ rejectOnce(createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker));
3382
3328
  });
3383
3329
  child.on('error', err => {
3384
3330
  progressUpdater.cancel();
3385
- subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3386
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3331
+ if (reviewState.stopRequested) {
3332
+ const processError = getErrorMessage(err);
3333
+ const finalLog = logLines.join('\n\n') || processError;
3334
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3335
+ ? recoveryTracker.getRecoveredResponse()
3336
+ : responseLines.join('\n\n') || null;
3337
+ updateSpecReviewRunStreaming(specReviewRunId, finalResponse);
3338
+ subagentTracker.finalize('stopped', finalLog || null, finalResponse);
3339
+ resolveOnce({
3340
+ completionStatus: 'stopped',
3341
+ finalResponse,
3342
+ usageSnapshot,
3343
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
3344
+ });
3345
+ return;
3346
+ }
3347
+ const failure = opencodeAgentProvider.classifyExecutionFailure(err);
3348
+ subagentTracker.finalize('error', failure.message, null);
3349
+ rejectOnce(failure);
3387
3350
  });
3388
3351
  });
3389
3352
  };