@straiffi/archon 1.8.13 → 1.8.15

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 (127) hide show
  1. package/dist/client/assets/{ActivityView-DD1Fom-k.js → ActivityView-DpY9GZez.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-aDf_6y6m.js → GitHubActionsDialog-BCr-hjvp.js} +1 -1
  3. package/dist/client/assets/{RepositoryGraphDialog-B4Ij2xmA.js → RepositoryGraphDialog-_hm-x1Dx.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-BBCRKTdr.js → VerificationDialog-CsjDzpE1.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DW3kiEe6.js → architectureDiagram-3BPJPVTR-Dx7MJofM.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-BVWrspTj.js → blockDiagram-GPEHLZMM-CSSJR8F_.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-CDqXjYR1.js → c4Diagram-AAUBKEIU-C4F9969r.js} +1 -1
  8. package/dist/client/assets/channel-DVkhJzgx.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-Db4bg8nj.js → chunk-2J33WTMH-23HW6i-y.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-DqziKs5m.js → chunk-3OPIFGDE-C3ZmRDMb.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-wdN7gQNo.js → chunk-5ZQYHXKU-B_-UQvCg.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-Dj3aKc06.js → chunk-727SXJPM-5Dua6u1B.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-CAlE_C_J.js → chunk-AQP2D5EJ-DcpcaFRO.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-KstW8JRv.js → chunk-CSCIHK7Q-CLCpOw52.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-DrY-9bi9.js → chunk-KSCS5N6A-uOZFe2JI.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-C6fw_Dab.js → chunk-L5ZTLDWV-DNnzSNoX.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-DaZP5hZq.js → chunk-LZXEDZCA-CSA3Xnoh.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-ChYXM7G3.js → chunk-ND2GUHAM-BQR5rD7w.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-JajrDJgw.js → chunk-NZK2D7GU-DyIwcQYQ.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-C4HoCf_q.js → chunk-O5CBEL6O-DfW60w5V.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-BdjAiDOI.js → chunk-WU5MYG2G-CzMMPYdi.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-6oZ93s5p.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DYO_YnkU.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-CpXupRCy.js → dagre-BM42HDAG-Ba7Pz-ZI.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-DyopuXBv.js → diagram-2AECGRRQ-BxYfgpZ6.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-C8lS8Vro.js → diagram-5GNKFQAL-Bc4-O-UJ.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-CsyC_xgK.js → diagram-KO2AKTUF-Ds1Lmrzm.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-DWsQdA71.js → diagram-LMA3HP47-VFQabxKS.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-YkNa_Tt6.js → diagram-OG6HWLK6-Baff59Zc.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-VmMz5G_L.js → erDiagram-TEJ5UH35-Da5zHjOK.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-DZAu6IJl.js → flowDiagram-I6XJVG4X-pdGMAuog.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CHJRz_2M.js → ganttDiagram-6RSMTGT7-DjJGX0nm.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BfA1qYTR.js → gitGraphDiagram-PVQCEYII-kErlwtcT.js} +1 -1
  34. package/dist/client/assets/index-CS4TzG41.css +2 -0
  35. package/dist/client/assets/{index-B4x8ZZ8N.js → index-D5TEfELZ.js} +107 -107
  36. package/dist/client/assets/{infoDiagram-5YYISTIA-CyqqEYvx.js → infoDiagram-5YYISTIA-Bx78HhDi.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DnXMvxPm.js → ishikawaDiagram-YF4QCWOH-BP0YQCMl.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-JHISSGLW-DBumlehf.js → journeyDiagram-JHISSGLW-DUgrCOuD.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-UN3LZRKU-D-QvfQhh.js → kanban-definition-UN3LZRKU-K0Zc5_M8.js} +1 -1
  40. package/dist/client/assets/{line-BgwSCyaP.js → line-Dtg-krjk.js} +1 -1
  41. package/dist/client/assets/{mermaid-parser.core-qDA1puIK.js → mermaid-parser.core-Dw5BI77W.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-CFM-fuJi.js → mermaid.core-BE-qlWNj.js} +3 -3
  43. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B93RAITk.js → mindmap-definition-RKZ34NQL-1Piz4H9I.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-4H26LBE5-C9arDpCE.js → pieDiagram-4H26LBE5-zlWdJ7lL.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Tma_RsUu.js → quadrantDiagram-W4KKPZXB-DRQY8Adp.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-4Y6WPE33-C6GCxpqT.js → requirementDiagram-4Y6WPE33-B8_GJFxl.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CY8ukFyG.js → sankeyDiagram-5OEKKPKP-BaI0Njo8.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B6MbhJm7.js → sequenceDiagram-3UESZ5HK-DjqAXbRF.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-AJRCARHV-Bjy1dBVk.js → stateDiagram-AJRCARHV-Ovr_3LeO.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-9pmeAorV.js +1 -0
  51. package/dist/client/assets/{timeline-definition-PNZ67QCA-KrbaHcGf.js → timeline-definition-PNZ67QCA-DkczYKeZ.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-CIIHVFJN-DQ-5gbga.js → vennDiagram-CIIHVFJN-BZyo4ESi.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cwpu-hvf.js → wardleyDiagram-YWT4CUSO-BEISrECX.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DoP4HxQE.js → xychartDiagram-2RQKCTM6-CyNzGK7f.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/server/db.js +2 -0
  57. package/dist/server/db.js.map +1 -1
  58. package/dist/server/index.js +37 -47
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/lib/agent.js +231 -181
  61. package/dist/server/lib/agent.js.map +1 -1
  62. package/dist/server/lib/agentProvider.js +74 -0
  63. package/dist/server/lib/agentProvider.js.map +1 -0
  64. package/dist/server/lib/agentRuns.js +21 -9
  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/automationResumeDecision.js +4 -3
  69. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  70. package/dist/server/lib/chatOperations.js +30 -1
  71. package/dist/server/lib/chatOperations.js.map +1 -1
  72. package/dist/server/lib/claudeAgentProvider.js +48 -0
  73. package/dist/server/lib/claudeAgentProvider.js.map +1 -0
  74. package/dist/server/lib/executionEnvironment.js +20 -0
  75. package/dist/server/lib/executionEnvironment.js.map +1 -0
  76. package/dist/server/lib/git.js +32 -1
  77. package/dist/server/lib/git.js.map +1 -1
  78. package/dist/server/lib/hostExecutionEnvironment.js +77 -0
  79. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -0
  80. package/dist/server/lib/lightweightPrompt.js +18 -18
  81. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  82. package/dist/server/lib/opencodeAgentProvider.js +139 -0
  83. package/dist/server/lib/opencodeAgentProvider.js.map +1 -0
  84. package/dist/server/lib/opencodeExecutionReliability.js +52 -0
  85. package/dist/server/lib/opencodeExecutionReliability.js.map +1 -0
  86. package/dist/server/lib/opencodeSessionOperations.js +358 -0
  87. package/dist/server/lib/opencodeSessionOperations.js.map +1 -0
  88. package/dist/server/lib/opencodeTaskSessions.js +13 -225
  89. package/dist/server/lib/opencodeTaskSessions.js.map +1 -1
  90. package/dist/server/lib/projectAutoConfig.js +23 -19
  91. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  92. package/dist/server/lib/reviewFindings.js +15 -0
  93. package/dist/server/lib/reviewFindings.js.map +1 -1
  94. package/dist/server/lib/specArtifact.js +19 -0
  95. package/dist/server/lib/specArtifact.js.map +1 -1
  96. package/dist/server/lib/specBundleGroups.js +151 -0
  97. package/dist/server/lib/specBundleGroups.js.map +1 -0
  98. package/dist/server/lib/specExport.js +1 -0
  99. package/dist/server/lib/specExport.js.map +1 -1
  100. package/dist/server/lib/specFormatting.js +13 -0
  101. package/dist/server/lib/specFormatting.js.map +1 -1
  102. package/dist/server/lib/specReviewValidation.js +22 -1
  103. package/dist/server/lib/specReviewValidation.js.map +1 -1
  104. package/dist/server/lib/specReviews.js +1 -1
  105. package/dist/server/lib/specSections.js +8 -1
  106. package/dist/server/lib/specSections.js.map +1 -1
  107. package/dist/server/lib/specTicketMaterialization.js +119 -0
  108. package/dist/server/lib/specTicketMaterialization.js.map +1 -0
  109. package/dist/server/lib/specs.js +27 -5
  110. package/dist/server/lib/specs.js.map +1 -1
  111. package/dist/server/lib/types.js.map +1 -1
  112. package/dist/server/workers/followUp.js +35 -10
  113. package/dist/server/workers/followUp.js.map +1 -1
  114. package/dist/server/workers/plan.js +28 -23
  115. package/dist/server/workers/plan.js.map +1 -1
  116. package/dist/server/workers/review.js +62 -24
  117. package/dist/server/workers/review.js.map +1 -1
  118. package/dist/server/workers/specReview.js +7 -1
  119. package/dist/server/workers/specReview.js.map +1 -1
  120. package/dist/server/workers/specSession.js +68 -36
  121. package/dist/server/workers/specSession.js.map +1 -1
  122. package/package.json +1 -1
  123. package/dist/client/assets/channel-C14KP1Bm.js +0 -1
  124. package/dist/client/assets/classDiagram-4FO5ZUOK-CR20Gg9e.js +0 -1
  125. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B-N-i15d.js +0 -1
  126. package/dist/client/assets/index-CCQwWUKF.css +0 -2
  127. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-asf2fsPC.js +0 -1
@@ -2,73 +2,30 @@ 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 { markRetryableOpenCodeDatabaseLock, RetryableOpenCodeDatabaseLockError, retryOpenCodeDatabaseLocks } from './opencodeExecutionReliability.js';
12
+ import { claudeAgentProvider } from './claudeAgentProvider.js';
13
+ import { buildOpencodeSpecReviewInvocation, createOpencodeExitError, createOpencodeRecoveryTracker, interpretOpencodeRecoveryEvent, isSuccessfulOpencodeExit, opencodeAgentProvider, } from './opencodeAgentProvider.js';
10
14
  import { appendClaudeHeadlessPlanPrompt, buildClaudeHeadlessPlanArgs } from './planModeQuestions.js';
11
15
  import { formatGenericToolCall } from './genericToolCall.js';
12
16
  import { getSpecSession, updateSpecSessionResumeId, updateSpecSessionStatus, updateSpecSessionTitle, updateSpecSessionUsageSnapshot } from './specs.js';
13
17
  import { updateSpecReviewRunStatus, updateSpecReviewRunStreaming, updateSpecReviewRunUsage } from './specReviews.js';
14
18
  import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForExecutionRun, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
15
- import { resolveOpencodeDbPath } from './runtimePaths.js';
16
19
  import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
17
20
  import { getTicket } from './tickets.js';
18
21
  import { createTranscriptRunSnapshots } from './transcriptRuns.js';
19
22
  import { createWorkspaceProcessEnvironment } from './workspaceProcessEnvironment.js';
20
- import { OpencodeModelResolutionError, parseOpencodeError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
23
+ import { OpencodeModelResolutionError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
21
24
  import { extractLeadingOpencodeJsonObjects } from './opencodePromptOutput.js';
22
25
  import { assertAgentSpawnPermit, AgentSpawnPermitRevokedError } from './agentSpawnPermits.js';
23
26
  const withTranscriptRuns = (value, transcriptRuns) => {
24
27
  return transcriptRuns ? { ...value, transcriptRuns } : value;
25
28
  };
26
- const createOpencodeRecoveryTracker = () => {
27
- let sawError = false;
28
- let latestEvent = null;
29
- let recoveredFinalAnswerFragments = [];
30
- let latestError = null;
31
- let modelResolutionError = null;
32
- return {
33
- consume: events => {
34
- let shouldResetResponse = false;
35
- for (const event of events ?? []) {
36
- latestEvent = event.type;
37
- if (event.type === 'error') {
38
- sawError = true;
39
- latestError = event.error;
40
- if (event.error?.isModelResolutionError) {
41
- modelResolutionError = event.error;
42
- }
43
- recoveredFinalAnswerFragments = [];
44
- shouldResetResponse = true;
45
- continue;
46
- }
47
- if (sawError) {
48
- recoveredFinalAnswerFragments.push(event.text);
49
- }
50
- }
51
- return shouldResetResponse;
52
- },
53
- hasRecoveredFinalAnswer: () => (sawError
54
- && latestEvent === 'final_answer'
55
- && recoveredFinalAnswerFragments.length > 0),
56
- hasUnrecoveredError: () => latestEvent === 'error',
57
- getModelResolutionError: () => modelResolutionError,
58
- getUnrecoveredError: () => latestEvent === 'error' ? latestError : null,
59
- getRecoveredResponse: () => recoveredFinalAnswerFragments.join('\n\n'),
60
- };
61
- };
62
- const isSuccessfulOpencodeExit = (code, stderrModelError, recoveryTracker) => (!stderrModelError
63
- && !recoveryTracker.getModelResolutionError()
64
- && !recoveryTracker.hasUnrecoveredError()
65
- && (code === 0 || recoveryTracker.hasRecoveredFinalAnswer()));
66
- const createOpencodeExitError = (message, stderrModelError, recoveryTracker) => {
67
- const structuredError = recoveryTracker.getModelResolutionError() ?? recoveryTracker.getUnrecoveredError();
68
- return structuredError?.isModelResolutionError || stderrModelError
69
- ? new OpencodeModelResolutionError(message)
70
- : new Error(message);
71
- };
72
29
  export class AgentContinuationRejectedError extends Error {
73
30
  constructor(message) {
74
31
  super(message);
@@ -91,9 +48,9 @@ export const getAgentErrorTranscriptRuns = (error) => {
91
48
  const transcriptRuns = error.transcriptRuns;
92
49
  return Array.isArray(transcriptRuns) && transcriptRuns.length > 0 ? transcriptRuns : null;
93
50
  };
94
- const OPENCODE_PLACEHOLDER_TITLE_PATTERN = /^New session\b/i;
95
51
  const OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS = 1500;
96
52
  const AGENT_PROGRESS_UPDATE_INTERVAL_MS = 100;
53
+ const OPENCODE_SUBAGENT_POLL_INTERVAL_MS = 750;
97
54
  const resolveAgentModel = (tool, model) => {
98
55
  if (tool === 'claude') {
99
56
  return normalizeClaudeModelId(model) || null;
@@ -157,13 +114,6 @@ const buildClaudeArgs = (mode, model, resumeSessionId, sessionId) => {
157
114
  sessionId,
158
115
  ];
159
116
  };
160
- const OPENCODE_SESSION_BY_ID_QUERY = 'SELECT id, title FROM session WHERE id = ? LIMIT 1';
161
- const OPENCODE_SESSION_BY_CREATED_AT_QUERY = 'SELECT id, title FROM session WHERE time_created > ? ORDER BY time_created DESC LIMIT 1';
162
- 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";
163
- 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";
164
- 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";
165
- 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";
166
- const OPENCODE_SESSION_COST_QUERY = 'SELECT cost FROM session WHERE id = ?';
167
117
  const GET_PERSISTED_CHAT_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM chat_sessions WHERE id = ?';
168
118
  const GET_PERSISTED_SPEC_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM spec_sessions WHERE id = ?';
169
119
  export const EMPTY_CHAT_USAGE_SNAPSHOT = {
@@ -327,9 +277,6 @@ export const mergeUsageFromJsonLine = (current, line) => {
327
277
  }
328
278
  return next;
329
279
  };
330
- const hasUsageSnapshot = (value) => {
331
- return Object.values(value).some(candidate => candidate !== null && candidate !== undefined);
332
- };
333
280
  const hasMatchingUsageSnapshot = (current, next) => {
334
281
  return current.usage_snapshot_context_window === next.usage_snapshot_context_window
335
282
  && current.usage_snapshot_max_output_tokens === next.usage_snapshot_max_output_tokens
@@ -500,67 +447,12 @@ const createThrottledProgressUpdater = (flushUpdate) => {
500
447
  },
501
448
  };
502
449
  };
503
- const openOpencodeDatabase = () => new Database(resolveOpencodeDbPath(), { readonly: true });
504
- const readOpencodeSession = (statement, value) => {
505
- const opencodeDb = openOpencodeDatabase();
506
- try {
507
- const row = opencodeDb.prepare(statement).get(value);
508
- if (!row?.id) {
509
- return null;
510
- }
511
- const normalizedTitle = typeof row.title === 'string'
512
- ? row.title.trim() || null
513
- : null;
514
- const title = normalizedTitle && !OPENCODE_PLACEHOLDER_TITLE_PATTERN.test(normalizedTitle)
515
- ? normalizedTitle
516
- : null;
517
- return {
518
- id: row.id,
519
- title,
520
- };
521
- }
522
- finally {
523
- opencodeDb.close();
524
- }
525
- };
526
450
  export const readOpencodeUsageSnapshot = ({ sessionId, spawnTimestamp = null, }) => {
527
- const opencodeDb = openOpencodeDatabase();
528
- try {
529
- const costRow = opencodeDb.prepare(OPENCODE_SESSION_COST_QUERY).get(sessionId);
530
- const sessionCost = typeof costRow?.cost === 'number' ? { usage_snapshot_cost: costRow.cost } : {};
531
- const completedAssistantMessage = (spawnTimestamp === null
532
- ? opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY).get(sessionId)
533
- : opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
534
- if (typeof completedAssistantMessage?.data === 'string') {
535
- try {
536
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(completedAssistantMessage.data));
537
- if (hasUsageSnapshot(parsedUsage)) {
538
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
539
- }
540
- }
541
- catch {
542
- // Fall through to the step-finish lookup below.
543
- }
544
- }
545
- const latestStepFinish = (spawnTimestamp === null
546
- ? opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_QUERY).get(sessionId)
547
- : opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
548
- if (typeof latestStepFinish?.data === 'string') {
549
- try {
550
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(latestStepFinish.data));
551
- if (hasUsageSnapshot(parsedUsage)) {
552
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
553
- }
554
- }
555
- catch {
556
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
557
- }
558
- }
559
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
560
- }
561
- finally {
562
- opencodeDb.close();
451
+ const snapshot = hostOpencodeSessionOperations.readUsageSnapshot(sessionId, spawnTimestamp);
452
+ if (!snapshot) {
453
+ throw new Error('OpenCode usage snapshot read failed');
563
454
  }
455
+ return snapshot;
564
456
  };
565
457
  const resolveOpencodeUsageSnapshot = ({ current, sessionId, spawnTimestamp = null, }) => {
566
458
  try {
@@ -655,12 +547,12 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
655
547
  };
656
548
  const readCurrentSession = () => {
657
549
  if (knownSessionId) {
658
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
550
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
659
551
  }
660
552
  if (spawnTimestamp === null) {
661
553
  return null;
662
554
  }
663
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
555
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
664
556
  };
665
557
  const refresh = () => {
666
558
  if (stopped) {
@@ -723,12 +615,12 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
723
615
  };
724
616
  const readCurrentSession = () => {
725
617
  if (knownSessionId) {
726
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
618
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
727
619
  }
728
620
  if (spawnTimestamp === null) {
729
621
  return null;
730
622
  }
731
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
623
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
732
624
  };
733
625
  const refresh = () => {
734
626
  if (stopped) {
@@ -1094,15 +986,14 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1094
986
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1095
987
  const text = event.part.text.trim();
1096
988
  const phase = event.part.metadata?.openai?.phase;
989
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, text, phase);
1097
990
  return {
1098
991
  logFragment: text || null,
1099
992
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
1100
993
  subagentEvents: [],
1101
994
  providerSessionId,
1102
995
  modelResolutionError: false,
1103
- recoveryEvents: text && phase === 'final_answer'
1104
- ? [{ type: 'final_answer', text }]
1105
- : [],
996
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1106
997
  };
1107
998
  }
1108
999
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -1186,14 +1077,15 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1186
1077
  };
1187
1078
  }
1188
1079
  if (event.type === 'error') {
1189
- const parsedError = parseOpencodeError(event.error, event.part?.text);
1080
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, event.part?.text, undefined);
1081
+ const parsedError = recoveryEvent?.type === 'error' ? recoveryEvent.error : null;
1190
1082
  const text = parsedError?.message ?? '';
1191
1083
  return {
1192
1084
  logFragment: text ? `**Error:** ${text}` : null,
1193
1085
  responseFragment: null,
1194
1086
  subagentEvents: [],
1195
1087
  providerSessionId,
1196
- recoveryEvents: [{ type: 'error', error: parsedError }],
1088
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1197
1089
  };
1198
1090
  }
1199
1091
  return emptyParsedStreamLine();
@@ -1596,7 +1488,7 @@ export const readOpencodeSessionTranscript = ({ sessionId, since, cwd, }) => {
1596
1488
  reader.close();
1597
1489
  }
1598
1490
  };
1599
- export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, taskSessionReader, discoverParentSession = true, }) => {
1491
+ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, taskSessionReader, discoverParentSession = true, parentDiscoveryDelayMs = 0, }) => {
1600
1492
  const reader = taskSessionReader === undefined ? createOpencodeTaskSessionReader() : taskSessionReader;
1601
1493
  let parentSessionId = initialParentSessionId;
1602
1494
  let timer = null;
@@ -1639,7 +1531,7 @@ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTim
1639
1531
  return;
1640
1532
  }
1641
1533
  try {
1642
- if (!parentSessionId && discoverParentSession) {
1534
+ if (!parentSessionId && discoverParentSession && Date.now() >= spawnTimestamp + parentDiscoveryDelayMs) {
1643
1535
  setParentSessionId(reader.findParentSessionSince(spawnTimestamp));
1644
1536
  }
1645
1537
  if (!parentSessionId) {
@@ -1672,7 +1564,7 @@ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTim
1672
1564
  return;
1673
1565
  }
1674
1566
  refresh();
1675
- timer = setInterval(refresh, 150);
1567
+ timer = setInterval(refresh, OPENCODE_SUBAGENT_POLL_INTERVAL_MS);
1676
1568
  timer.unref?.();
1677
1569
  };
1678
1570
  const stop = () => {
@@ -1724,6 +1616,16 @@ const killProcess = (proc, signal) => {
1724
1616
  catch {
1725
1617
  }
1726
1618
  };
1619
+ const interruptProcess = (state, signal) => {
1620
+ if (!isProcessAlive(state.child)) {
1621
+ return;
1622
+ }
1623
+ if (state.interrupt) {
1624
+ state.interrupt(signal);
1625
+ return;
1626
+ }
1627
+ killProcess(state.child, signal);
1628
+ };
1727
1629
  const waitForProcessExit = async (proc, timeoutMs) => {
1728
1630
  const deadline = Date.now() + timeoutMs;
1729
1631
  await new Promise(resolve => {
@@ -1757,6 +1659,7 @@ const untrackChatProcess = (chatSessionId, state) => {
1757
1659
  export const registerChatProcessPlaceholder = (chatSessionId) => {
1758
1660
  const state = {
1759
1661
  child: null,
1662
+ interrupt: null,
1760
1663
  researchChildren: new Set(),
1761
1664
  stopRequested: false,
1762
1665
  metadataRefresher: null,
@@ -1765,7 +1668,7 @@ export const registerChatProcessPlaceholder = (chatSessionId) => {
1765
1668
  return state;
1766
1669
  };
1767
1670
  export const registerBuildProcessPlaceholder = (ticketId) => {
1768
- const state = { child: null, stopRequested: false, sessionLookupTimer: null };
1671
+ const state = { child: null, interrupt: null, stopRequested: false, sessionLookupTimer: null };
1769
1672
  activeBuildProcesses.set(ticketId, state);
1770
1673
  return state;
1771
1674
  };
@@ -1796,6 +1699,7 @@ const untrackSpecProcess = (specSessionId, state) => {
1796
1699
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1797
1700
  const state = {
1798
1701
  child: null,
1702
+ interrupt: null,
1799
1703
  researchChildren: new Set(),
1800
1704
  stopRequested: false,
1801
1705
  metadataRefresher: null,
@@ -1819,6 +1723,7 @@ const untrackSpecReviewProcess = (specReviewRunId, state) => {
1819
1723
  export const registerSpecReviewProcessPlaceholder = (specReviewRunId) => {
1820
1724
  const state = {
1821
1725
  child: null,
1726
+ interrupt: null,
1822
1727
  researchChildren: new Set(),
1823
1728
  stopRequested: false,
1824
1729
  metadataRefresher: null,
@@ -1864,10 +1769,20 @@ export const stopBuildAgent = async (ticketId) => {
1864
1769
  if (!state.child) {
1865
1770
  return true;
1866
1771
  }
1867
- killProcess(state.child, 'SIGTERM');
1772
+ if (state.interrupt) {
1773
+ state.interrupt('SIGTERM');
1774
+ }
1775
+ else {
1776
+ killProcess(state.child, 'SIGTERM');
1777
+ }
1868
1778
  await waitForProcessExit(state.child, 5000);
1869
1779
  if (isProcessAlive(state.child)) {
1870
- killProcess(state.child, 'SIGKILL');
1780
+ if (state.interrupt) {
1781
+ state.interrupt('SIGKILL');
1782
+ }
1783
+ else {
1784
+ killProcess(state.child, 'SIGKILL');
1785
+ }
1871
1786
  await waitForProcessExit(state.child, 1000);
1872
1787
  }
1873
1788
  return true;
@@ -1943,15 +1858,15 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1943
1858
  if (!state.child) {
1944
1859
  return true;
1945
1860
  }
1946
- killProcess(state.child, 'SIGTERM');
1861
+ interruptProcess(state, 'SIGTERM');
1947
1862
  await waitForProcessExit(state.child, 5000);
1948
1863
  if (isProcessAlive(state.child)) {
1949
- killProcess(state.child, 'SIGKILL');
1864
+ interruptProcess(state, 'SIGKILL');
1950
1865
  await waitForProcessExit(state.child, 1000);
1951
1866
  }
1952
1867
  return true;
1953
1868
  };
1954
- 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, }) => {
1869
+ const runAgentOnce = ({ 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, }) => {
1955
1870
  const resolvedModel = resolveAgentModel(tool, model);
1956
1871
  const subagentTracker = createSubagentRunTracker({
1957
1872
  parentType: 'ticket',
@@ -2049,7 +1964,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2049
1964
  return hasResolvedOpencodeSessionId;
2050
1965
  }
2051
1966
  try {
2052
- const session = readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
1967
+ const session = hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
2053
1968
  if (session?.id) {
2054
1969
  bindResolvedOpencodeSession(session.id);
2055
1970
  }
@@ -2059,15 +1974,19 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2059
1974
  }
2060
1975
  return hasResolvedOpencodeSessionId;
2061
1976
  };
2062
- const [cmd, args] = tool === 'claude'
2063
- ? (() => {
2064
- sessionId = resumeSessionId || randomUUID();
2065
- return [
2066
- 'claude',
2067
- buildClaudeArgs(opencodeAgentMode, resolvedModel, resumeSessionId, sessionId),
2068
- ];
2069
- })()
2070
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1977
+ const agentProvider = tool === 'claude'
1978
+ ? claudeAgentProvider
1979
+ : opencodeAgentProvider;
1980
+ const invocation = agentProvider.buildStreamingInvocation({
1981
+ cwd,
1982
+ prompt,
1983
+ model: resolvedModel,
1984
+ variant,
1985
+ mode: opencodeAgentMode,
1986
+ resumeSessionId,
1987
+ sessionId: randomUUID(),
1988
+ });
1989
+ sessionId = invocation.sessionId;
2071
1990
  writeRunStatus('running', null);
2072
1991
  if (tool === 'claude' && sessionId) {
2073
1992
  writeRunSessionId(sessionId);
@@ -2076,6 +1995,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2076
1995
  spawnTimestamp = Date.now();
2077
1996
  }
2078
1997
  let child;
1998
+ let interrupt;
1999
+ let placementFailure;
2079
2000
  try {
2080
2001
  if (buildState?.stopRequested) {
2081
2002
  throw new AgentSpawnPermitRevokedError();
@@ -2083,28 +2004,37 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2083
2004
  if (spawnPermitId && executionContext) {
2084
2005
  assertAgentSpawnPermit(spawnPermitId, executionContext.runId);
2085
2006
  }
2086
- child = spawn(cmd, args, {
2007
+ const handle = executionProvider.execute({
2008
+ target: {
2009
+ kind: 'worktree',
2010
+ id: ticketId,
2011
+ workspacePath: cwd,
2012
+ revision: 'working-tree',
2013
+ },
2014
+ command: invocation.command,
2015
+ args: invocation.args,
2087
2016
  cwd,
2088
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2017
+ stdio: invocation.stdio,
2089
2018
  env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
2090
2019
  detached: process.platform !== 'win32',
2091
2020
  });
2021
+ child = handle.child;
2022
+ interrupt = handle.interrupt;
2023
+ placementFailure = handle.placementFailure;
2092
2024
  }
2093
2025
  catch (error) {
2094
- const resolvedError = error instanceof Error ? error : new Error(String(error));
2026
+ const resolvedError = agentProvider.classifyExecutionFailure(error);
2095
2027
  const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2096
2028
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2097
2029
  return;
2098
2030
  }
2099
2031
  reviewChild = child;
2100
- if (tool === 'claude') {
2101
- const claudePrompt = opencodeAgentMode === 'plan'
2102
- ? appendClaudeHeadlessPlanPrompt(prompt)
2103
- : prompt;
2104
- writeClaudeStdin(child, buildClaudeStdinMessage(claudePrompt, [], cwd));
2032
+ if (invocation.stdin !== null) {
2033
+ writeClaudeStdin(child, invocation.stdin);
2105
2034
  }
2106
2035
  if (trackBuildProcess && buildState) {
2107
2036
  buildState.child = child;
2037
+ buildState.interrupt = interrupt;
2108
2038
  activeBuildProcesses.set(ticketId, buildState);
2109
2039
  }
2110
2040
  if (reviewProcessState) {
@@ -2113,6 +2043,14 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2113
2043
  killProcess(child, 'SIGTERM');
2114
2044
  }
2115
2045
  }
2046
+ void placementFailure.then(failure => {
2047
+ if (!failure || settled) {
2048
+ return;
2049
+ }
2050
+ const resolvedError = agentProvider.classifyExecutionFailure(failure);
2051
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2052
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2053
+ });
2116
2054
  startTime = Date.now();
2117
2055
  if (tool === 'opencode' && spawnTimestamp !== null) {
2118
2056
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
@@ -2207,8 +2145,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2207
2145
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null, usageSnapshot }, transcriptRuns));
2208
2146
  return;
2209
2147
  }
2210
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2211
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2148
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2149
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2150
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2212
2151
  });
2213
2152
  return;
2214
2153
  }
@@ -2307,6 +2246,13 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2307
2246
  const error = resumeSessionId && !executionCapableEventObserved && isTypedPreExecutionContinuationRejection(failureMessage)
2308
2247
  ? new AgentContinuationRejectedError(failureMessage)
2309
2248
  : createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
2249
+ const retryableError = !executionCapableEventObserved && !sessionId
2250
+ ? markRetryableOpenCodeDatabaseLock(error)
2251
+ : error;
2252
+ if (retryableError instanceof RetryableOpenCodeDatabaseLockError) {
2253
+ rejectOnce(retryableError);
2254
+ return;
2255
+ }
2310
2256
  const transcriptRuns = finalizeRun('error', log, finalResponse);
2311
2257
  rejectOnce(attachTranscriptRunsToError(error, transcriptRuns));
2312
2258
  });
@@ -2321,12 +2267,17 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2321
2267
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
2322
2268
  return;
2323
2269
  }
2324
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2325
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2270
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2271
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2272
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2326
2273
  });
2327
2274
  });
2328
2275
  };
2329
- export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, }) => {
2276
+ export const runAgent = (options) => {
2277
+ const run = () => runAgentOnce(options);
2278
+ return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
2279
+ };
2280
+ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, }) => {
2330
2281
  const resolvedModel = resolveAgentModel(tool, model);
2331
2282
  const subagentTracker = createSubagentRunTracker({
2332
2283
  parentType: 'chat',
@@ -2693,11 +2644,18 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2693
2644
  }, transcriptRuns));
2694
2645
  return;
2695
2646
  }
2696
- updateChatSessionStatus(chatSessionId, 'error', log, io, finalResponse);
2697
2647
  const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
2698
2648
  const error = resumeSessionId && !executionCapableEventObserved && isTypedPreExecutionContinuationRejection(failureMessage)
2699
2649
  ? new AgentContinuationRejectedError(failureMessage)
2700
2650
  : createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
2651
+ const retryableError = !executionCapableEventObserved
2652
+ ? markRetryableOpenCodeDatabaseLock(error)
2653
+ : error;
2654
+ if (retryableError instanceof RetryableOpenCodeDatabaseLockError) {
2655
+ rejectOnce(retryableError);
2656
+ return;
2657
+ }
2658
+ updateChatSessionStatus(chatSessionId, 'error', log, io, finalResponse);
2701
2659
  const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
2702
2660
  rejectOnce(attachTranscriptRunsToError(error, transcriptRuns));
2703
2661
  });
@@ -2727,7 +2685,11 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
2727
2685
  });
2728
2686
  });
2729
2687
  };
2730
- export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, executionContext, mainRun, }) => {
2688
+ export const runChatAgent = (options) => {
2689
+ const run = () => runChatAgentOnce(options);
2690
+ return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
2691
+ };
2692
+ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, executionContext, mainRun, }) => {
2731
2693
  const resolvedModel = resolveAgentModel(tool, model);
2732
2694
  const subagentTracker = createSubagentRunTracker({
2733
2695
  parentType: 'spec',
@@ -3067,10 +3029,18 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
3067
3029
  }, transcriptRuns));
3068
3030
  return;
3069
3031
  }
3032
+ const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
3033
+ const error = createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
3034
+ const retryableError = logLines.length === 0 && responseLines.length === 0
3035
+ ? markRetryableOpenCodeDatabaseLock(error)
3036
+ : error;
3037
+ if (retryableError instanceof RetryableOpenCodeDatabaseLockError) {
3038
+ rejectOnce(retryableError);
3039
+ return;
3040
+ }
3070
3041
  updateSpecSessionStatusLocal(specSessionId, 'error', log, io, finalResponse);
3071
3042
  const transcriptRuns = subagentTracker.finalize('error', log, finalResponse);
3072
- const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
3073
- rejectOnce(attachTranscriptRunsToError(createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker), transcriptRuns));
3043
+ rejectOnce(attachTranscriptRunsToError(error, transcriptRuns));
3074
3044
  });
3075
3045
  child.on('error', err => {
3076
3046
  progressUpdater.cancel();
@@ -3098,6 +3068,10 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
3098
3068
  });
3099
3069
  });
3100
3070
  };
3071
+ export const runSpecAgent = (options) => {
3072
+ const run = () => runSpecAgentOnce(options);
3073
+ return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
3074
+ };
3101
3075
  /**
3102
3076
  * Focused, decoupled runner for a one-shot Spec Review.
3103
3077
  *
@@ -3108,7 +3082,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
3108
3082
  * updateSpecReviewRunStatus / updateSpecReviewRunStreaming / updateSpecReviewRunUsage —
3109
3083
  * the spec_sessions row is never touched by this path.
3110
3084
  */
3111
- export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, executionContext, }) => {
3085
+ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, executionContext, }) => {
3112
3086
  const registeredState = activeSpecReviewProcesses.get(specReviewRunId);
3113
3087
  if (registeredState?.stopRequested) {
3114
3088
  return Promise.resolve({
@@ -3127,6 +3101,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3127
3101
  });
3128
3102
  return new Promise((resolve, reject) => {
3129
3103
  let spawnTimestamp = null;
3104
+ let opencodeSessionId = null;
3130
3105
  let opencodeSubagentMonitor = null;
3131
3106
  let settled = false;
3132
3107
  let startTime = null;
@@ -3134,6 +3109,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3134
3109
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
3135
3110
  const reviewState = registeredState ?? {
3136
3111
  child: null,
3112
+ interrupt: null,
3137
3113
  researchChildren: new Set(),
3138
3114
  stopRequested: false,
3139
3115
  metadataRefresher: null,
@@ -3149,21 +3125,56 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3149
3125
  };
3150
3126
  const resolveOnce = settle(resolve);
3151
3127
  const rejectOnce = settle(reject);
3152
- const [cmd, args] = tool === 'claude'
3153
- ? [
3154
- 'claude',
3155
- buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3156
- ]
3157
- : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
3128
+ const [cmd, args] = [
3129
+ 'claude',
3130
+ buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3131
+ ];
3158
3132
  updateSpecReviewRunStreaming(specReviewRunId, null);
3159
- const child = spawn(cmd, args, {
3160
- cwd,
3161
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
3162
- env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
3163
- detached: process.platform !== 'win32',
3164
- });
3133
+ const env = createWorkspaceProcessEnvironment(cwd, { PWD: cwd });
3134
+ let child;
3135
+ if (tool === 'opencode') {
3136
+ const invocation = buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant });
3137
+ try {
3138
+ const handle = hostExecutionEnvironment.execute({
3139
+ target: {
3140
+ kind: 'repository',
3141
+ id: cwd,
3142
+ workspacePath: cwd,
3143
+ revision: 'working-tree',
3144
+ },
3145
+ command: invocation.command,
3146
+ args: invocation.args,
3147
+ cwd,
3148
+ stdio: invocation.stdio,
3149
+ env,
3150
+ detached: process.platform !== 'win32',
3151
+ });
3152
+ child = handle.child;
3153
+ reviewState.interrupt = handle.interrupt;
3154
+ void handle.placementFailure.then(failure => {
3155
+ if (failure) {
3156
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(failure));
3157
+ }
3158
+ });
3159
+ }
3160
+ catch (error) {
3161
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(error));
3162
+ return;
3163
+ }
3164
+ }
3165
+ else {
3166
+ child = spawn(cmd, args, {
3167
+ cwd,
3168
+ stdio: ['pipe', 'pipe', 'pipe'],
3169
+ env,
3170
+ detached: process.platform !== 'win32',
3171
+ });
3172
+ }
3165
3173
  reviewState.child = child;
3166
3174
  activeSpecReviewProcesses.set(specReviewRunId, reviewState);
3175
+ if (reviewState.stopRequested) {
3176
+ interruptProcess(reviewState, 'SIGTERM');
3177
+ }
3167
3178
  if (tool === 'claude') {
3168
3179
  writeClaudeStdin(child, buildClaudeStdinMessage(appendClaudeHeadlessPlanPrompt(prompt), files, cwd));
3169
3180
  }
@@ -3175,6 +3186,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3175
3186
  spawnTimestamp,
3176
3187
  cwd,
3177
3188
  tracker: subagentTracker,
3189
+ parentDiscoveryDelayMs: OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS,
3178
3190
  });
3179
3191
  opencodeSubagentMonitor.start();
3180
3192
  }
@@ -3287,6 +3299,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3287
3299
  const parsed = parseOpencodeJsonLine(line, cwd);
3288
3300
  const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
3289
3301
  opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3302
+ opencodeSessionId = providerSessionId ?? opencodeSessionId;
3290
3303
  subagentTracker.handle(subagentEvents);
3291
3304
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3292
3305
  firstFinalAnswerTime = null;
@@ -3304,6 +3317,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3304
3317
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3305
3318
  const parsed = parseOpencodeJsonLine(remainder, cwd);
3306
3319
  opencodeSubagentMonitor?.setParentSessionId(parsed.providerSessionId);
3320
+ opencodeSessionId = parsed.providerSessionId ?? opencodeSessionId;
3307
3321
  subagentTracker.handle(parsed.subagentEvents);
3308
3322
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3309
3323
  firstFinalAnswerTime = null;
@@ -3315,6 +3329,13 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3315
3329
  const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3316
3330
  ? recoveredResponse
3317
3331
  : responseLines.join('\n\n') || stderrOutput;
3332
+ if (opencodeSessionId && spawnTimestamp !== null) {
3333
+ usageSnapshot = resolveOpencodeUsageSnapshot({
3334
+ current: usageSnapshot,
3335
+ sessionId: opencodeSessionId,
3336
+ spawnTimestamp,
3337
+ });
3338
+ }
3318
3339
  if (reviewState.stopRequested) {
3319
3340
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3320
3341
  subagentTracker.finalize('stopped', log || null, finalResponse || null);
@@ -3338,15 +3359,44 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3338
3359
  });
3339
3360
  return;
3340
3361
  }
3341
- subagentTracker.finalize('error', log || null, finalResponse || null);
3342
3362
  const failureMessage = `Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`;
3343
- rejectOnce(createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker));
3363
+ const error = createOpencodeExitError(failureMessage, stderrModelError, recoveryTracker);
3364
+ const retryableError = logLines.length === 0 && responseLines.length === 0 && !opencodeSessionId
3365
+ ? markRetryableOpenCodeDatabaseLock(error)
3366
+ : error;
3367
+ if (retryableError instanceof RetryableOpenCodeDatabaseLockError) {
3368
+ rejectOnce(retryableError);
3369
+ return;
3370
+ }
3371
+ subagentTracker.finalize('error', log || null, finalResponse || null);
3372
+ rejectOnce(error);
3344
3373
  });
3345
3374
  child.on('error', err => {
3346
3375
  progressUpdater.cancel();
3347
- subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3348
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3376
+ if (reviewState.stopRequested) {
3377
+ const processError = getErrorMessage(err);
3378
+ const finalLog = logLines.join('\n\n') || processError;
3379
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3380
+ ? recoveryTracker.getRecoveredResponse()
3381
+ : responseLines.join('\n\n') || null;
3382
+ updateSpecReviewRunStreaming(specReviewRunId, finalResponse);
3383
+ subagentTracker.finalize('stopped', finalLog || null, finalResponse);
3384
+ resolveOnce({
3385
+ completionStatus: 'stopped',
3386
+ finalResponse,
3387
+ usageSnapshot,
3388
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
3389
+ });
3390
+ return;
3391
+ }
3392
+ const failure = opencodeAgentProvider.classifyExecutionFailure(err);
3393
+ subagentTracker.finalize('error', failure.message, null);
3394
+ rejectOnce(failure);
3349
3395
  });
3350
3396
  });
3351
3397
  };
3398
+ export const runSpecReviewAgent = (options) => {
3399
+ const run = () => runSpecReviewAgentOnce(options);
3400
+ return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
3401
+ };
3352
3402
  //# sourceMappingURL=agent.js.map