@straiffi/archon 1.11.4 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +8 -0
  2. package/dist/client/assets/{ActivityView-Cj-JRM9h.js → ActivityView-D9dXuvls.js} +1 -1
  3. package/dist/client/assets/{GitHubActionsDialog-Bz7x-EXk.js → GitHubActionsDialog-DFB9FNX0.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-C1g5teUk.js → VerificationDialog-CCpZtj3u.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Czgp4_s_.js → architectureDiagram-3BPJPVTR-C5QNVdbR.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-D_eKxSQA.js → blockDiagram-GPEHLZMM-ygrKiT2B.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-D6nMuDM5.js → c4Diagram-AAUBKEIU-p6pvarzH.js} +1 -1
  8. package/dist/client/assets/channel-CjCFIS-2.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-B6Mn8gmT.js → chunk-2J33WTMH-BEZXyYSy.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-BBoS7imT.js → chunk-3OPIFGDE-CxmfjZmF.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-CdhOo0Gr.js → chunk-5ZQYHXKU-B2X60oxE.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-Dq8Odwy5.js → chunk-727SXJPM-P03pDtRv.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-DsKFdfMh.js → chunk-AQP2D5EJ-DE0MLi-X.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-4Ol5HlMd.js → chunk-CSCIHK7Q-C3QVx-Wf.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-Ao7uFEEm.js → chunk-KSCS5N6A-BvajGF5R.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-C9Gzx6GF.js → chunk-L5ZTLDWV-HtpQP1op.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-Ceca_Ske.js → chunk-LZXEDZCA-CWfF3VMY.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-Cs2auQCc.js → chunk-ND2GUHAM-kEAduwJl.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-fssKuUqc.js → chunk-NZK2D7GU-DNG4qBtm.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-BdRwmtR1.js → chunk-O5CBEL6O-CS4J6DbD.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-DnWsSIYg.js → chunk-WU5MYG2G-B2-mpLQ2.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-DQ1lcUTi.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tiWbaJIg.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-B71wyESx.js → dagre-BM42HDAG-CNK_Lkn1.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-D7Xht5lG.js → diagram-2AECGRRQ-BGKodAdj.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-BIL2jzaH.js → diagram-5GNKFQAL-O7IcwCBh.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-CKLHtkTQ.js → diagram-KO2AKTUF-BzuzpIm4.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-CoTDrm1f.js → diagram-LMA3HP47-C6xFuQYj.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-a6qcioel.js → diagram-OG6HWLK6-hp2uvo99.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-CUTe5tSP.js → erDiagram-TEJ5UH35-CiiZ8B7W.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-CSDL4PU-.js → flowDiagram-I6XJVG4X-CMXv6xN7.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Ck4JCO9N.js → ganttDiagram-6RSMTGT7-EP8OVqmy.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-D8h23S3l.js → gitGraphDiagram-PVQCEYII-Dacq4hgw.js} +1 -1
  34. package/dist/client/assets/index-a8IeMnRR.css +2 -0
  35. package/dist/client/assets/index-uGt4qt5t.js +287 -0
  36. package/dist/client/assets/{infoDiagram-5YYISTIA-Bq1wj1vH.js → infoDiagram-5YYISTIA-DpTIfB1a.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-C6ry_1gD.js → ishikawaDiagram-YF4QCWOH-DOcryUZC.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-JHISSGLW-BCoSw_Ye.js → journeyDiagram-JHISSGLW-DgEfwhed.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-UN3LZRKU-CvHdWXxq.js → kanban-definition-UN3LZRKU-Bg9Kz1IT.js} +1 -1
  40. package/dist/client/assets/{line-DDPHAHbi.js → line-DGt3zplT.js} +1 -1
  41. package/dist/client/assets/{mermaid-parser.core-B7v3eJbR.js → mermaid-parser.core-CeZgcMLs.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-DoJjZ2zS.js → mermaid.core-Bp6d14EP.js} +3 -3
  43. package/dist/client/assets/{mindmap-definition-RKZ34NQL-D8uwnlrG.js → mindmap-definition-RKZ34NQL-CyJdo6SE.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-4H26LBE5-B2zLClZg.js → pieDiagram-4H26LBE5-elm12Uf5.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DGGYHTHx.js → quadrantDiagram-W4KKPZXB-BXuIBixh.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-4Y6WPE33-JfCBqWur.js → requirementDiagram-4Y6WPE33-C5rIaCf4.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D9nUF-4P.js → sankeyDiagram-5OEKKPKP-CIYPcDTl.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DGnNkDzT.js → sequenceDiagram-3UESZ5HK-CX_go9-1.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-AJRCARHV-CFZlnYcA.js → stateDiagram-AJRCARHV-CYbsPFSQ.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-QZ_UdR0J.js +1 -0
  51. package/dist/client/assets/{timeline-definition-PNZ67QCA-C-KYYKMX.js → timeline-definition-PNZ67QCA-B3pBx_YB.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-CIIHVFJN-C3MGIjT1.js → vennDiagram-CIIHVFJN-B1Z6hLI-.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CUD7E4c3.js → wardleyDiagram-YWT4CUSO-BvJEPN2P.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CM2Q4-TE.js → xychartDiagram-2RQKCTM6-CKavkWIt.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/server/db.js +63 -4
  57. package/dist/server/db.js.map +1 -1
  58. package/dist/server/index.js +55 -0
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/lib/agent.js +277 -299
  61. package/dist/server/lib/agent.js.map +1 -1
  62. package/dist/server/lib/agentOutputLanguage.js +11 -0
  63. package/dist/server/lib/agentOutputLanguage.js.map +1 -0
  64. package/dist/server/lib/agentProvider.js.map +1 -1
  65. package/dist/server/lib/agentRuns.js +68 -4
  66. package/dist/server/lib/agentRuns.js.map +1 -1
  67. package/dist/server/lib/appLocale.js +19 -0
  68. package/dist/server/lib/appLocale.js.map +1 -0
  69. package/dist/server/lib/buildExecutionEnvironment.js +92 -1
  70. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  71. package/dist/server/lib/chatOperations.js +10 -3
  72. package/dist/server/lib/chatOperations.js.map +1 -1
  73. package/dist/server/lib/conversationResume.js +56 -3
  74. package/dist/server/lib/conversationResume.js.map +1 -1
  75. package/dist/server/lib/dockerSandboxOpencodeSessionOperations.js +81 -25
  76. package/dist/server/lib/dockerSandboxOpencodeSessionOperations.js.map +1 -1
  77. package/dist/server/lib/dockerSandboxReleaseProfiles.js +36 -7
  78. package/dist/server/lib/dockerSandboxReleaseProfiles.js.map +1 -1
  79. package/dist/server/lib/executionEnvironmentRoutes.js +71 -4
  80. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  81. package/dist/server/lib/executionEnvironmentValidation.js +199 -19
  82. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  83. package/dist/server/lib/executionEnvironmentValidationEvidence.js +140 -0
  84. package/dist/server/lib/executionEnvironmentValidationEvidence.js.map +1 -0
  85. package/dist/server/lib/executionEnvironmentValidationOperations.js +13 -0
  86. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  87. package/dist/server/lib/executionRetryContext.js +13 -0
  88. package/dist/server/lib/executionRetryContext.js.map +1 -1
  89. package/dist/server/lib/figmaResources.js +9 -2
  90. package/dist/server/lib/figmaResources.js.map +1 -1
  91. package/dist/server/lib/hostExecutionEnvironment.js +14 -2
  92. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -1
  93. package/dist/server/lib/i18n/index.js +3 -2
  94. package/dist/server/lib/i18n/index.js.map +1 -1
  95. package/dist/server/lib/i18n/resources.js +0 -1
  96. package/dist/server/lib/i18n/resources.js.map +1 -1
  97. package/dist/server/lib/lightweightPrompt.js +4 -0
  98. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  99. package/dist/server/lib/models.js +58 -18
  100. package/dist/server/lib/models.js.map +1 -1
  101. package/dist/server/lib/opencodeAgentProvider.js +54 -48
  102. package/dist/server/lib/opencodeAgentProvider.js.map +1 -1
  103. package/dist/server/lib/opencodeCli.js +244 -0
  104. package/dist/server/lib/opencodeCli.js.map +1 -0
  105. package/dist/server/lib/opencodePromptOutput.js +11 -65
  106. package/dist/server/lib/opencodePromptOutput.js.map +1 -1
  107. package/dist/server/lib/opencodeSessionOperations.js +44 -28
  108. package/dist/server/lib/opencodeSessionOperations.js.map +1 -1
  109. package/dist/server/lib/opencodeSessionProvenance.js +23 -0
  110. package/dist/server/lib/opencodeSessionProvenance.js.map +1 -0
  111. package/dist/server/lib/opencodeStream.js +263 -0
  112. package/dist/server/lib/opencodeStream.js.map +1 -0
  113. package/dist/server/lib/opencodeTaskSessions.js +3 -4
  114. package/dist/server/lib/opencodeTaskSessions.js.map +1 -1
  115. package/dist/server/lib/opencodeV2SessionOperations.js +453 -0
  116. package/dist/server/lib/opencodeV2SessionOperations.js.map +1 -0
  117. package/dist/server/lib/run.js +27 -41
  118. package/dist/server/lib/run.js.map +1 -1
  119. package/dist/server/lib/runProcessIdentity.js +10 -2
  120. package/dist/server/lib/runProcessIdentity.js.map +1 -1
  121. package/dist/server/lib/runProcesses.js +52 -19
  122. package/dist/server/lib/runProcesses.js.map +1 -1
  123. package/dist/server/lib/runServiceStop.js +62 -0
  124. package/dist/server/lib/runServiceStop.js.map +1 -0
  125. package/dist/server/lib/runServices.js +63 -4
  126. package/dist/server/lib/runServices.js.map +1 -1
  127. package/dist/server/lib/runSessionLifecycle.js +25 -0
  128. package/dist/server/lib/runSessionLifecycle.js.map +1 -1
  129. package/dist/server/lib/runtimePaths.js +7 -4
  130. package/dist/server/lib/runtimePaths.js.map +1 -1
  131. package/dist/server/lib/serviceRunLogs.js +179 -0
  132. package/dist/server/lib/serviceRunLogs.js.map +1 -0
  133. package/dist/server/lib/specOperations.js +7 -2
  134. package/dist/server/lib/specOperations.js.map +1 -1
  135. package/dist/server/lib/specReviewContext.js +3 -2
  136. package/dist/server/lib/specReviewContext.js.map +1 -1
  137. package/dist/server/lib/toolUpgrade.js +54 -5
  138. package/dist/server/lib/toolUpgrade.js.map +1 -1
  139. package/dist/server/lib/types.js.map +1 -1
  140. package/dist/server/workers/build.js +47 -9
  141. package/dist/server/workers/build.js.map +1 -1
  142. package/dist/server/workers/chat.js +74 -10
  143. package/dist/server/workers/chat.js.map +1 -1
  144. package/dist/server/workers/followUp.js +59 -22
  145. package/dist/server/workers/followUp.js.map +1 -1
  146. package/dist/server/workers/plan.js +109 -169
  147. package/dist/server/workers/plan.js.map +1 -1
  148. package/dist/server/workers/planFollowUp.js +27 -7
  149. package/dist/server/workers/planFollowUp.js.map +1 -1
  150. package/dist/server/workers/review.js +43 -10
  151. package/dist/server/workers/review.js.map +1 -1
  152. package/dist/server/workers/specReview.js +1 -0
  153. package/dist/server/workers/specReview.js.map +1 -1
  154. package/dist/server/workers/specSession.js +42 -5
  155. package/dist/server/workers/specSession.js.map +1 -1
  156. package/dist/shared/i18n/locales.js +12 -0
  157. package/dist/shared/i18n/locales.js.map +1 -0
  158. package/package.json +1 -1
  159. package/dist/client/assets/channel-BsoUrvI0.js +0 -1
  160. package/dist/client/assets/classDiagram-4FO5ZUOK-D79OdDcf.js +0 -1
  161. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CyG7JUxO.js +0 -1
  162. package/dist/client/assets/index-Ckj_syqH.js +0 -287
  163. package/dist/client/assets/index-RM0FaS4q.css +0 -2
  164. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BB3Eu4sn.js +0 -1
@@ -6,6 +6,7 @@ import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
6
6
  import { normalizeAgentModelId, requireExecutableAgent } from './agentCatalog.js';
7
7
  import { createOpencodeTaskSessionReader, parseOpencodeTaskEnvelope } from './opencodeTaskSessions.js';
8
8
  import { hostOpencodeSessionOperations } from './opencodeSessionOperations.js';
9
+ import { getOpencodeCliCompatibility, inspectOpencodeRelease, requireSupportedOpencodeCompatibility } from './opencodeCli.js';
9
10
  import { hostExecutionEnvironment } from './hostExecutionEnvironment.js';
10
11
  import { markRetryableOpenCodeDatabaseLock, RetryableOpenCodeDatabaseLockError, retryOpenCodeDatabaseLocks } from './opencodeExecutionReliability.js';
11
12
  import { claudeAgentProvider } from './claudeAgentProvider.js';
@@ -22,7 +23,7 @@ import { getTicket } from './tickets.js';
22
23
  import { createTranscriptRunSnapshots } from './transcriptRuns.js';
23
24
  import { createHostLauncherEnvironment, createLegacyHostRuntimeEnvironment, createWorkspaceProcessEnvironment, } from './workspaceProcessEnvironment.js';
24
25
  import { OpencodeModelResolutionError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
25
- import { extractLeadingOpencodeJsonObjects } from './opencodePromptOutput.js';
26
+ import { decodeOpencodeStreamEvent, extractLeadingOpencodeJsonObjects, extractOpencodeStreamUsage, parseOpencodeStreamLine } from './opencodeStream.js';
26
27
  import { assertAgentSpawnPermit, AgentSpawnPermitRevokedError } from './agentSpawnPermits.js';
27
28
  import { buildClaudeStdinMessage } from './claudeStdin.js';
28
29
  import { enrichPromptWithPreview } from './agentPreview.js';
@@ -67,6 +68,23 @@ const getStreamingAgentProvider = (tool) => {
67
68
  }
68
69
  return opencodeAgentProvider;
69
70
  };
71
+ const resolveOpencodeCompatibility = (tool, executionProvider, runtimeIdentity) => {
72
+ if (tool !== 'opencode') {
73
+ return null;
74
+ }
75
+ if (executionProvider.kind !== 'docker_sandbox') {
76
+ const compatibility = requireSupportedOpencodeCompatibility(getOpencodeCliCompatibility({ force: true }));
77
+ if (runtimeIdentity && (compatibility.runtimeIdentity.version !== runtimeIdentity.version
78
+ || compatibility.runtimeIdentity.major !== runtimeIdentity.major)) {
79
+ throw new Error('The selected host OpenCode runtime no longer matches its resolved compatibility evidence.');
80
+ }
81
+ return compatibility;
82
+ }
83
+ if (!runtimeIdentity) {
84
+ throw new Error('The selected OpenCode execution environment has no validated runtime compatibility.');
85
+ }
86
+ return requireSupportedOpencodeCompatibility(inspectOpencodeRelease(runtimeIdentity.version));
87
+ };
70
88
  const getAgentStderr = (tool, chunks) => {
71
89
  const stderr = Buffer.concat(chunks).toString();
72
90
  return tool === 'codex' ? redactCodexDiagnostic(stderr) : stderr;
@@ -113,30 +131,6 @@ export const EMPTY_CHAT_USAGE_SNAPSHOT = {
113
131
  usage_snapshot_reasoning_tokens: null,
114
132
  usage_snapshot_cost: null,
115
133
  };
116
- const toFiniteNumber = (value) => {
117
- return typeof value === 'number' && Number.isFinite(value)
118
- ? value
119
- : null;
120
- };
121
- const getValueAtPath = (value, path) => {
122
- let current = value;
123
- for (const segment of path) {
124
- if (!current || typeof current !== 'object' || !(segment in current)) {
125
- return null;
126
- }
127
- current = current[segment];
128
- }
129
- return current;
130
- };
131
- const getNumberAtPaths = (value, paths) => {
132
- for (const path of paths) {
133
- const candidate = toFiniteNumber(getValueAtPath(value, path));
134
- if (candidate !== null) {
135
- return candidate;
136
- }
137
- }
138
- return null;
139
- };
140
134
  const mergeChatUsageSnapshot = (current, next) => {
141
135
  return {
142
136
  usage_snapshot_context_window: next.usage_snapshot_context_window ?? current.usage_snapshot_context_window,
@@ -149,105 +143,8 @@ const mergeChatUsageSnapshot = (current, next) => {
149
143
  usage_snapshot_cost: next.usage_snapshot_cost ?? current.usage_snapshot_cost,
150
144
  };
151
145
  };
152
- const extractChatUsageSnapshot = (value) => {
153
- if (!value || typeof value !== 'object') {
154
- return {};
155
- }
156
- return {
157
- usage_snapshot_context_window: getNumberAtPaths(value, [
158
- ['contextWindow'],
159
- ['context_window'],
160
- ['result', 'contextWindow'],
161
- ['result', 'context_window'],
162
- ['limit', 'context'],
163
- ['limits', 'context'],
164
- ]),
165
- usage_snapshot_max_output_tokens: getNumberAtPaths(value, [
166
- ['maxOutputTokens'],
167
- ['max_output_tokens'],
168
- ['result', 'maxOutputTokens'],
169
- ['result', 'max_output_tokens'],
170
- ['limit', 'output'],
171
- ['limits', 'output'],
172
- ]),
173
- usage_snapshot_total_tokens: getNumberAtPaths(value, [
174
- ['tokens', 'total'],
175
- ['usage', 'tokens', 'total'],
176
- ['usage', 'total'],
177
- ['usage', 'totalTokens'],
178
- ['result', 'tokens', 'total'],
179
- ['result', 'usage', 'tokens', 'total'],
180
- ['result', 'usage', 'total'],
181
- ['result', 'usage', 'totalTokens'],
182
- ]),
183
- usage_snapshot_input_tokens: getNumberAtPaths(value, [
184
- ['message', 'usage', 'input_tokens'],
185
- ['usage', 'input_tokens'],
186
- ['tokens', 'input'],
187
- ['usage', 'tokens', 'input'],
188
- ['usage', 'input'],
189
- ['usage', 'inputTokens'],
190
- ['result', 'tokens', 'input'],
191
- ['result', 'usage', 'tokens', 'input'],
192
- ['result', 'usage', 'input'],
193
- ['result', 'usage', 'inputTokens'],
194
- ]),
195
- usage_snapshot_output_tokens: getNumberAtPaths(value, [
196
- ['message', 'usage', 'output_tokens'],
197
- ['usage', 'output_tokens'],
198
- ['tokens', 'output'],
199
- ['usage', 'tokens', 'output'],
200
- ['usage', 'output'],
201
- ['usage', 'outputTokens'],
202
- ['result', 'tokens', 'output'],
203
- ['result', 'usage', 'tokens', 'output'],
204
- ['result', 'usage', 'output'],
205
- ['result', 'usage', 'outputTokens'],
206
- ]),
207
- usage_snapshot_cache_read_tokens: getNumberAtPaths(value, [
208
- ['message', 'usage', 'cache_read_input_tokens'],
209
- ['usage', 'cache_read_input_tokens'],
210
- ['tokens', 'cache', 'read'],
211
- ['usage', 'tokens', 'cacheRead'],
212
- ['usage', 'cacheRead'],
213
- ['usage', 'cacheReadTokens'],
214
- ['result', 'tokens', 'cache', 'read'],
215
- ['result', 'usage', 'tokens', 'cacheRead'],
216
- ['result', 'usage', 'cacheRead'],
217
- ['result', 'usage', 'cacheReadTokens'],
218
- ]),
219
- usage_snapshot_reasoning_tokens: getNumberAtPaths(value, [
220
- ['tokens', 'reasoning'],
221
- ['usage', 'tokens', 'reasoning'],
222
- ['usage', 'reasoning'],
223
- ['usage', 'reasoningTokens'],
224
- ['result', 'tokens', 'reasoning'],
225
- ['result', 'usage', 'tokens', 'reasoning'],
226
- ['result', 'usage', 'reasoning'],
227
- ['result', 'usage', 'reasoningTokens'],
228
- ]),
229
- usage_snapshot_cost: getNumberAtPaths(value, [
230
- ['total_cost_usd'],
231
- ['cost'],
232
- ['result', 'cost'],
233
- ]),
234
- };
235
- };
236
- export const mergeUsageFromJsonLine = (current, line) => {
237
- const trimmedLine = line.trim();
238
- if (!trimmedLine.startsWith('{')) {
239
- return current;
240
- }
241
- const { jsonObjects } = extractLeadingOpencodeJsonObjects(trimmedLine);
146
+ const completeUsageTotal = (current) => {
242
147
  let next = current;
243
- for (const jsonObject of jsonObjects) {
244
- try {
245
- next = mergeChatUsageSnapshot(next, extractChatUsageSnapshot(JSON.parse(jsonObject)));
246
- }
247
- catch {
248
- continue;
249
- }
250
- }
251
148
  if (next.usage_snapshot_total_tokens === null) {
252
149
  const components = [
253
150
  next.usage_snapshot_input_tokens,
@@ -264,6 +161,43 @@ export const mergeUsageFromJsonLine = (current, line) => {
264
161
  }
265
162
  return next;
266
163
  };
164
+ const mergeStreamUsage = (current, usage) => mergeChatUsageSnapshot(current, {
165
+ usage_snapshot_context_window: usage.contextWindow,
166
+ usage_snapshot_max_output_tokens: usage.maxOutputTokens,
167
+ usage_snapshot_total_tokens: usage.totalTokens,
168
+ usage_snapshot_input_tokens: usage.inputTokens,
169
+ usage_snapshot_output_tokens: usage.outputTokens,
170
+ usage_snapshot_cache_read_tokens: usage.cacheReadTokens,
171
+ usage_snapshot_reasoning_tokens: usage.reasoningTokens,
172
+ usage_snapshot_cost: usage.cost,
173
+ });
174
+ export const mergeUsageFromJsonLine = (current, line) => {
175
+ const trimmedLine = line.trim();
176
+ if (!trimmedLine.startsWith('{')) {
177
+ return current;
178
+ }
179
+ let next = current;
180
+ for (const event of parseOpencodeStreamLine(trimmedLine).events) {
181
+ next = mergeStreamUsage(next, event.usage);
182
+ }
183
+ return completeUsageTotal(next);
184
+ };
185
+ const mergeClaudeUsageFromJsonLine = (current, line) => {
186
+ const { jsonObjects } = extractLeadingOpencodeJsonObjects(line);
187
+ let next = current;
188
+ for (const jsonObject of jsonObjects) {
189
+ try {
190
+ const event = JSON.parse(jsonObject);
191
+ if (event.type === 'assistant' || event.type === 'result') {
192
+ next = mergeStreamUsage(next, extractOpencodeStreamUsage(event));
193
+ }
194
+ }
195
+ catch {
196
+ // Stream parsing handles malformed output separately.
197
+ }
198
+ }
199
+ return completeUsageTotal(next);
200
+ };
267
201
  const hasMatchingUsageSnapshot = (current, next) => {
268
202
  return current.usage_snapshot_context_window === next.usage_snapshot_context_window
269
203
  && current.usage_snapshot_max_output_tokens === next.usage_snapshot_max_output_tokens
@@ -525,7 +459,7 @@ const updateChatSessionOpencodeMetadata = (chatSessionId, sessionId, title, io)
525
459
  io.emit('chat-session:updated', chatSession);
526
460
  }
527
461
  };
528
- const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, initialSessionId = null, sessionOperations = hostOpencodeSessionOperations, }) => {
462
+ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, cwd, initialSessionId = null, sessionOperations, }) => {
529
463
  let lookupTimer = null;
530
464
  let stopped = false;
531
465
  let knownSessionId = initialSessionId;
@@ -545,7 +479,19 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
545
479
  if (spawnTimestamp === null) {
546
480
  return null;
547
481
  }
548
- return sessionOperations.findLatestSessionAfter(spawnTimestamp);
482
+ return sessionOperations.findLatestSessionAfter(spawnTimestamp, { directory: cwd });
483
+ };
484
+ const pinSessionId = (sessionId) => {
485
+ const normalized = sessionId?.trim();
486
+ if (!normalized) {
487
+ return;
488
+ }
489
+ knownSessionId = normalized;
490
+ if (normalized !== lastAppliedSessionId) {
491
+ updateChatSessionOpencodeMetadata(chatSessionId, normalized, null, io);
492
+ lastAppliedSessionId = normalized;
493
+ lastAppliedTitle = null;
494
+ }
549
495
  };
550
496
  const refresh = async () => {
551
497
  if (stopped) {
@@ -586,6 +532,7 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
586
532
  }, OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS);
587
533
  };
588
534
  return {
535
+ pinSessionId,
589
536
  refresh,
590
537
  scheduleRetry,
591
538
  stop: () => {
@@ -594,7 +541,7 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
594
541
  },
595
542
  };
596
543
  };
597
- const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp, initialSessionId = null, }) => {
544
+ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp, cwd, initialSessionId = null, sessionOperations, }) => {
598
545
  let lookupTimer = null;
599
546
  let stopped = false;
600
547
  let knownSessionId = initialSessionId;
@@ -609,12 +556,24 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
609
556
  };
610
557
  const readCurrentSession = async () => {
611
558
  if (knownSessionId) {
612
- return hostOpencodeSessionOperations.findSessionById(knownSessionId);
559
+ return sessionOperations.findSessionById(knownSessionId);
613
560
  }
614
561
  if (spawnTimestamp === null) {
615
562
  return null;
616
563
  }
617
- return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
564
+ return sessionOperations.findLatestSessionAfter(spawnTimestamp, { directory: cwd });
565
+ };
566
+ const pinSessionId = (sessionId) => {
567
+ const normalized = sessionId?.trim();
568
+ if (!normalized) {
569
+ return;
570
+ }
571
+ knownSessionId = normalized;
572
+ if (normalized !== lastAppliedSessionId) {
573
+ updateSpecSessionOpencodeMetadata(specSessionId, normalized, null, io);
574
+ lastAppliedSessionId = normalized;
575
+ lastAppliedTitle = null;
576
+ }
618
577
  };
619
578
  const refresh = async () => {
620
579
  if (stopped) {
@@ -655,6 +614,7 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
655
614
  }, OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS);
656
615
  };
657
616
  return {
617
+ pinSessionId,
658
618
  refresh,
659
619
  scheduleRetry,
660
620
  stop: () => {
@@ -977,60 +937,40 @@ const parseClaudeStreamJsonLine = (line, cwd) => {
977
937
  }
978
938
  return emptyParsedStreamLine();
979
939
  };
980
- const formatOpencodeToolUse = (event, cwd) => {
981
- const toolName = event.part?.tool;
982
- if (!toolName) {
983
- return null;
984
- }
985
- return formatToolCall(toolName, event.part?.state?.input, cwd);
986
- };
987
- const formatOpencodeStepTitle = (event) => {
988
- const title = event.part?.title?.trim();
989
- return title || null;
990
- };
991
940
  const parseOpencodeJsonEvent = (event, cwd) => {
992
- const providerSessionId = event.sessionID?.trim()
993
- || event.sessionId?.trim()
994
- || event.part?.sessionID?.trim()
995
- || event.part?.sessionId?.trim()
996
- || null;
941
+ const providerSessionId = event.sessionId;
997
942
  if (event.type === 'subagent_snapshot') {
998
- const sessionId = typeof event.part?.state?.metadata?.sessionId === 'string'
999
- ? event.part.state.metadata.sessionId
1000
- : null;
1001
943
  return {
1002
944
  ...emptyParsedStreamLine(),
1003
945
  providerSessionId,
1004
946
  subagentEvents: [{
1005
947
  kind: 'snapshot',
1006
- callId: event.part?.callID ?? null,
1007
- label: event.part?.title?.trim() || 'Subagent',
1008
- sessionId,
1009
- status: getSubagentCompletionStatus(event.part?.state?.status),
1010
- log: event.part?.state?.log ?? null,
1011
- response: event.part?.state?.response ?? null,
948
+ callId: event.tool.callId,
949
+ label: event.tool.title || 'Subagent',
950
+ sessionId: event.tool.childSessionId,
951
+ status: getSubagentCompletionStatus(event.tool.status),
952
+ log: event.tool.log,
953
+ response: event.tool.response,
1012
954
  }],
1013
955
  };
1014
956
  }
1015
- if (event.type === 'plan' && event.part?.type === 'text' && event.part.text) {
957
+ if (event.type === 'plan' && event.text) {
1016
958
  return {
1017
- logFragment: event.part.text,
959
+ logFragment: event.text,
1018
960
  responseFragment: null,
1019
961
  subagentEvents: [],
1020
962
  providerSessionId,
1021
963
  modelResolutionError: false,
1022
- concatenateFragments: event.part.metadata?.codex?.delta === true,
1023
- replaceFragment: event.part.metadata?.codex?.snapshot === true,
1024
- fragmentGroup: event.part.metadata?.codex?.fragmentGroup
1025
- ?? event.part.metadata?.codex?.itemId
1026
- ?? null,
964
+ concatenateFragments: event.metadata.delta,
965
+ replaceFragment: event.metadata.snapshot,
966
+ fragmentGroup: event.metadata.fragmentGroup ?? event.metadata.itemId,
1027
967
  };
1028
968
  }
1029
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1030
- const concatenateFragments = event.part.metadata?.codex?.delta === true;
1031
- const text = concatenateFragments ? event.part.text : event.part.text.trim();
1032
- const phase = event.part.metadata?.openai?.phase;
1033
- const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, text, phase);
969
+ if (event.type === 'text' && event.text) {
970
+ const concatenateFragments = event.metadata.delta;
971
+ const text = concatenateFragments ? event.text : event.text.trim();
972
+ const phase = event.metadata.phase;
973
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, undefined, text, phase);
1034
974
  return {
1035
975
  logFragment: text || null,
1036
976
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
@@ -1039,14 +979,12 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1039
979
  modelResolutionError: false,
1040
980
  recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1041
981
  concatenateFragments,
1042
- fragmentGroup: event.part.metadata?.codex?.fragmentGroup
1043
- ?? event.part.metadata?.codex?.itemId
1044
- ?? null,
982
+ fragmentGroup: event.metadata.fragmentGroup ?? event.metadata.itemId,
1045
983
  };
1046
984
  }
1047
- if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
1048
- const concatenateFragments = event.part.metadata?.codex?.delta === true;
1049
- const text = concatenateFragments ? event.part.text ?? '' : event.part.text?.trim() ?? '';
985
+ if (event.type === 'reasoning') {
986
+ const concatenateFragments = event.metadata.delta;
987
+ const text = concatenateFragments ? event.text : event.text.trim();
1050
988
  return {
1051
989
  logFragment: text || null,
1052
990
  responseFragment: null,
@@ -1054,26 +992,22 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1054
992
  providerSessionId,
1055
993
  modelResolutionError: false,
1056
994
  concatenateFragments,
1057
- fragmentGroup: event.part.metadata?.codex?.fragmentGroup
1058
- ?? event.part.metadata?.codex?.itemId
1059
- ?? null,
995
+ fragmentGroup: event.metadata.fragmentGroup ?? event.metadata.itemId,
1060
996
  };
1061
997
  }
1062
- if (event.type === 'tool' || event.type === 'tool_use' || event.type === 'tool_result') {
1063
- const isToolInvocation = event.type === 'tool' || event.type === 'tool_use';
1064
- const toolName = event.part?.tool;
998
+ if (event.type === 'tool_use' || event.type === 'tool_result') {
999
+ const isToolInvocation = event.type === 'tool_use';
1000
+ const toolName = event.tool.name;
1065
1001
  if (toolName && isSubagentTool(toolName)) {
1066
- const description = getSubagentDescription(event.part?.state?.input, toolName);
1067
- const rawOutput = event.part?.state?.output?.trim();
1002
+ const description = getSubagentDescription(event.tool.input, toolName);
1003
+ const rawOutput = event.tool.output?.trim();
1068
1004
  const envelope = parseOpencodeTaskEnvelope(rawOutput);
1069
1005
  const output = envelope ? envelope.result : rawOutput;
1070
- const taskStatus = event.part?.state?.status?.toLowerCase() ?? envelope?.state?.toLowerCase();
1006
+ const taskStatus = event.tool.status?.toLowerCase() ?? envelope?.state?.toLowerCase();
1071
1007
  const isTerminal = Boolean(taskStatus && taskStatus !== 'running' && taskStatus !== 'pending');
1072
1008
  const outputKind = isTerminal ? 'complete' : 'fragment';
1073
1009
  const completionStatus = getSubagentCompletionStatus(taskStatus);
1074
- const childSessionId = envelope?.childSessionId
1075
- ?? (typeof event.part?.state?.metadata?.sessionID === 'string' ? event.part.state.metadata.sessionID : null)
1076
- ?? (typeof event.part?.state?.metadata?.sessionId === 'string' ? event.part.state.metadata.sessionId : null);
1010
+ const childSessionId = envelope?.childSessionId ?? event.tool.childSessionId;
1077
1011
  return {
1078
1012
  logFragment: isToolInvocation ? `Subagent: ${description}` : null,
1079
1013
  responseFragment: null,
@@ -1081,7 +1015,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1081
1015
  ...(isToolInvocation
1082
1016
  ? [{
1083
1017
  kind: 'start',
1084
- callId: event.part?.callID ?? null,
1018
+ callId: event.tool.callId,
1085
1019
  label: `Subagent: ${description}`,
1086
1020
  sessionId: childSessionId,
1087
1021
  }]
@@ -1089,7 +1023,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1089
1023
  ...(output || isTerminal
1090
1024
  ? [{
1091
1025
  kind: outputKind,
1092
- callId: event.part?.callID ?? null,
1026
+ callId: event.tool.callId,
1093
1027
  label: `Subagent: ${description}`,
1094
1028
  ...(output ? { text: output } : {}),
1095
1029
  sessionId: childSessionId,
@@ -1104,7 +1038,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1104
1038
  if (!isToolInvocation) {
1105
1039
  return { ...emptyParsedStreamLine(), providerSessionId };
1106
1040
  }
1107
- const toolFragment = formatOpencodeToolUse(event, cwd);
1041
+ const toolFragment = formatToolCall(toolName, event.tool.input, cwd);
1108
1042
  return {
1109
1043
  logFragment: toolFragment,
1110
1044
  responseFragment: null,
@@ -1115,7 +1049,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1115
1049
  }
1116
1050
  if (event.type === 'step_start') {
1117
1051
  return {
1118
- logFragment: formatOpencodeStepTitle(event),
1052
+ logFragment: event.title,
1119
1053
  responseFragment: null,
1120
1054
  subagentEvents: [],
1121
1055
  providerSessionId,
@@ -1132,7 +1066,7 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1132
1066
  };
1133
1067
  }
1134
1068
  if (event.type === 'error') {
1135
- const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, event.part?.text, undefined);
1069
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, event.fallbackText ?? undefined, undefined);
1136
1070
  const parsedError = recoveryEvent?.type === 'error' ? recoveryEvent.error : null;
1137
1071
  const text = parsedError?.message ?? '';
1138
1072
  return {
@@ -1143,15 +1077,16 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1143
1077
  recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1144
1078
  };
1145
1079
  }
1146
- return emptyParsedStreamLine();
1080
+ return { ...emptyParsedStreamLine(), providerSessionId };
1147
1081
  };
1148
1082
  const parseOpencodeJsonLine = (line, cwd) => {
1149
1083
  const trimmed = line.trim();
1150
1084
  if (!trimmed) {
1151
1085
  return emptyParsedStreamLine();
1152
1086
  }
1153
- const { jsonObjects, remainder } = extractLeadingOpencodeJsonObjects(trimmed);
1154
- if (jsonObjects.length === 0) {
1087
+ const normalizedLine = parseOpencodeStreamLine(trimmed);
1088
+ const { events, remainder } = normalizedLine;
1089
+ if (!normalizedLine.structured) {
1155
1090
  if (trimmed.startsWith('{')) {
1156
1091
  return emptyParsedStreamLine();
1157
1092
  }
@@ -1179,21 +1114,14 @@ const parseOpencodeJsonLine = (line, cwd) => {
1179
1114
  let concatenateFragments;
1180
1115
  let replaceFragment;
1181
1116
  let fragmentGroup;
1182
- for (const jsonObject of jsonObjects) {
1183
- let event;
1184
- try {
1185
- event = JSON.parse(jsonObject);
1186
- }
1187
- catch {
1188
- continue;
1189
- }
1117
+ for (const event of events) {
1190
1118
  const parsedEvent = parseOpencodeJsonEvent(event, cwd);
1191
1119
  const { logFragment, responseFragment } = parsedEvent;
1192
1120
  subagentEvents.push(...parsedEvent.subagentEvents);
1193
1121
  recoveryEvents.push(...(parsedEvent.recoveryEvents ?? []));
1194
1122
  modelResolutionError ||= Boolean(parsedEvent.modelResolutionError);
1195
1123
  providerSessionId = providerSessionId ?? parsedEvent.providerSessionId;
1196
- if (jsonObjects.length === 1) {
1124
+ if (events.length === 1) {
1197
1125
  concatenateFragments = parsedEvent.concatenateFragments;
1198
1126
  replaceFragment = parsedEvent.replaceFragment;
1199
1127
  fragmentGroup = parsedEvent.fragmentGroup;
@@ -1236,6 +1164,7 @@ const parseOpencodeJsonLine = (line, cwd) => {
1236
1164
  }
1237
1165
  return emptyParsedStreamLine();
1238
1166
  };
1167
+ const hasOpencodeFinalAnswer = (line) => parseOpencodeStreamLine(line).events.some(event => (event.type === 'text' && Boolean(event.text) && (!event.metadata.phase || event.metadata.phase === 'final_answer')));
1239
1168
  const appendTranscriptText = (current, fragment) => {
1240
1169
  const trimmed = fragment.trim();
1241
1170
  if (!trimmed) {
@@ -1289,7 +1218,7 @@ const appendOpencodeTranscriptLine = ({ parsed, logLines, responseLines, recover
1289
1218
  }
1290
1219
  return shouldResetResponse;
1291
1220
  };
1292
- export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, executionContext, deferMainFinalization = false, }) => {
1221
+ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, executionContext, deferMainFinalization = false, opencodeRuntimeIdentity = null, }) => {
1293
1222
  const trackedRuns = [];
1294
1223
  let nextRunIndex = Math.max(executionContext ? getNextAgentRunIndex(executionContext.runId) : getNextAgentRunIndex(parentType, parentId), mainRun ? mainRun.runIndex + 1 : 1);
1295
1224
  const emitUpdate = () => {
@@ -1335,7 +1264,7 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1335
1264
  return;
1336
1265
  }
1337
1266
  run.sessionId = normalized;
1338
- updateAgentRunSessionId(run.id, normalized);
1267
+ updateAgentRunSessionId(run.id, normalized, opencodeRuntimeIdentity);
1339
1268
  };
1340
1269
  const updateLabel = (run, label) => {
1341
1270
  if (!label?.trim()) {
@@ -1507,6 +1436,8 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1507
1436
  response_text: run.response || null,
1508
1437
  failure_message: persistedRun?.failure_message ?? null,
1509
1438
  session_id: run.sessionId,
1439
+ opencode_session_runtime_major: persistedRun?.opencode_session_runtime_major ?? null,
1440
+ opencode_session_runtime_version: persistedRun?.opencode_session_runtime_version ?? null,
1510
1441
  completed_at: persistedRun?.completed_at ?? timestamp,
1511
1442
  created_at: persistedRun?.created_at ?? run.createdAt,
1512
1443
  updated_at: persistedRun?.updated_at ?? timestamp,
@@ -1538,6 +1469,8 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1538
1469
  response_text: parentResponse,
1539
1470
  failure_message: persistedMainRun?.failure_message ?? null,
1540
1471
  session_id: persistedMainRun?.session_id ?? null,
1472
+ opencode_session_runtime_major: persistedMainRun?.opencode_session_runtime_major ?? null,
1473
+ opencode_session_runtime_version: persistedMainRun?.opencode_session_runtime_version ?? null,
1541
1474
  completed_at: persistedMainRun?.completed_at ?? timestamp,
1542
1475
  created_at: persistedMainRun?.created_at ?? timestamp,
1543
1476
  updated_at: persistedMainRun?.updated_at ?? timestamp,
@@ -1565,6 +1498,8 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1565
1498
  response_text: parentResponse,
1566
1499
  failure_message: null,
1567
1500
  session_id: null,
1501
+ opencode_session_runtime_major: null,
1502
+ opencode_session_runtime_version: null,
1568
1503
  completed_at: timestamp,
1569
1504
  created_at: timestamp,
1570
1505
  updated_at: timestamp,
@@ -1575,7 +1510,7 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1575
1510
  const setMainSessionId = (sessionId) => {
1576
1511
  const normalized = sessionId?.trim();
1577
1512
  if (mainRun && normalized) {
1578
- updateAgentRunSessionId(mainRun.id, normalized);
1513
+ updateAgentRunSessionId(mainRun.id, normalized, opencodeRuntimeIdentity);
1579
1514
  }
1580
1515
  };
1581
1516
  return { applySnapshot, handle, finalize, hasRunningSubagents, setMainSessionId, updateMain };
@@ -1598,7 +1533,7 @@ export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1598
1533
  : partType === 'step-finish'
1599
1534
  ? 'step_finish'
1600
1535
  : partType;
1601
- const parsed = parseOpencodeJsonEvent({ type: eventType, part }, cwd);
1536
+ const parsed = parseOpencodeJsonEvent(decodeOpencodeStreamEvent({ type: eventType, part }), cwd);
1602
1537
  if (parsed.logFragment && parsed.logFragment !== parsed.responseFragment) {
1603
1538
  log = appendTranscriptText(log, parsed.logFragment);
1604
1539
  }
@@ -1620,7 +1555,7 @@ export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1620
1555
  return { finalResponse: finalResponse || null, log: log || null, response: response || null };
1621
1556
  };
1622
1557
  export const readOpencodeSessionTranscript = async ({ sessionId, since, cwd, sessionOperations = hostOpencodeSessionOperations, }) => {
1623
- const reader = createOpencodeTaskSessionReader(sessionOperations);
1558
+ const reader = createOpencodeTaskSessionReader(sessionOperations, { directory: cwd });
1624
1559
  if (!reader) {
1625
1560
  return null;
1626
1561
  }
@@ -1635,7 +1570,9 @@ export const readOpencodeSessionTranscript = async ({ sessionId, since, cwd, ses
1635
1570
  }
1636
1571
  };
1637
1572
  export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, onActivity, taskSessionReader, discoverParentSession = true, parentDiscoveryDelayMs = 0, }) => {
1638
- const reader = taskSessionReader === undefined ? createOpencodeTaskSessionReader() : taskSessionReader;
1573
+ const reader = taskSessionReader === undefined
1574
+ ? createOpencodeTaskSessionReader(hostOpencodeSessionOperations, { directory: cwd })
1575
+ : taskSessionReader;
1639
1576
  let parentSessionId = initialParentSessionId;
1640
1577
  let timer = null;
1641
1578
  const childSessionByCallId = new Map();
@@ -2079,8 +2016,10 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
2079
2016
  }
2080
2017
  return true;
2081
2018
  };
2082
- const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null, skills = [], resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
2019
+ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null, skills = [], resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), opencodeRuntimeIdentity = null, }) => {
2083
2020
  const resolvedModel = resolveAgentModel(tool, model);
2021
+ const resolvedOpencodeCompatibility = resolveOpencodeCompatibility(tool, executionProvider, opencodeRuntimeIdentity);
2022
+ const resolvedOpencodeRuntimeIdentity = resolvedOpencodeCompatibility?.runtimeIdentity ?? null;
2084
2023
  const opencodeSessionOperations = executionProvider.sessionOperations?.opencode ?? null;
2085
2024
  const subagentTracker = createSubagentRunTracker({
2086
2025
  parentType: 'ticket',
@@ -2097,6 +2036,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2097
2036
  }
2098
2037
  : {}),
2099
2038
  executionContext,
2039
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
2100
2040
  });
2101
2041
  const emitRunUpdate = () => {
2102
2042
  const emitter = io;
@@ -2116,7 +2056,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2116
2056
  };
2117
2057
  const writeRunSessionId = (sessionId) => {
2118
2058
  if (runStream) {
2119
- updateAgentRunSessionId(runStream.runId, sessionId);
2059
+ updateAgentRunSessionId(runStream.runId, sessionId, resolvedOpencodeRuntimeIdentity);
2120
2060
  emitRunUpdate();
2121
2061
  if (persistSessionIdToTicket) {
2122
2062
  updateSessionId(ticketId, sessionId, sessionField, io);
@@ -2201,7 +2141,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2201
2141
  return hasResolvedOpencodeSessionId;
2202
2142
  }
2203
2143
  try {
2204
- const session = await opencodeSessionOperations?.findLatestSessionAfter(spawnTimestamp, cwd);
2144
+ const session = await opencodeSessionOperations?.findLatestSessionAfter(spawnTimestamp, { directory: cwd });
2205
2145
  if (!hasResolvedOpencodeSessionId && session?.id) {
2206
2146
  bindResolvedOpencodeSession(session.id);
2207
2147
  }
@@ -2221,6 +2161,8 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2221
2161
  mode: opencodeAgentMode,
2222
2162
  resumeSessionId,
2223
2163
  sessionId: randomUUID(),
2164
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
2165
+ opencodeCompatibility: resolvedOpencodeCompatibility,
2224
2166
  };
2225
2167
  const invocation = agentProvider.buildStreamingInvocation(streamingRequest);
2226
2168
  sessionId = invocation.sessionId;
@@ -2316,7 +2258,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2316
2258
  startTime = Date.now();
2317
2259
  if (tool === 'opencode' && spawnTimestamp !== null) {
2318
2260
  const taskSessionReader = opencodeSessionOperations
2319
- ? createOpencodeTaskSessionReader(opencodeSessionOperations)
2261
+ ? createOpencodeTaskSessionReader(opencodeSessionOperations, { directory: cwd })
2320
2262
  : null;
2321
2263
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
2322
2264
  initialParentSessionId: resumeSessionId,
@@ -2349,7 +2291,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2349
2291
  const lines = text.split('\n');
2350
2292
  remainder = lines.pop() ?? '';
2351
2293
  for (const line of lines) {
2352
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2294
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, line);
2353
2295
  const parsed = parseClaudeStreamJsonLine(line, cwd);
2354
2296
  const { logFragment, responseFragment, subagentEvents } = parsed;
2355
2297
  if (isExecutionCapableParsedStreamLine(parsed)) {
@@ -2375,7 +2317,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2375
2317
  return;
2376
2318
  }
2377
2319
  if (remainder.trim()) {
2378
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2320
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, remainder);
2379
2321
  const parsed = parseClaudeStreamJsonLine(remainder, cwd);
2380
2322
  const { logFragment, responseFragment, subagentEvents } = parsed;
2381
2323
  if (isExecutionCapableParsedStreamLine(parsed)) {
@@ -2464,18 +2406,8 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2464
2406
  for (const line of lines) {
2465
2407
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2466
2408
  if (firstFinalAnswerTime === null && startTime !== null) {
2467
- const trimmedLine = line.trim();
2468
- if (trimmedLine.startsWith('{')) {
2469
- try {
2470
- const event = JSON.parse(trimmedLine);
2471
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
2472
- const phase = event.part.metadata?.openai?.phase;
2473
- if (!phase || phase === 'final_answer') {
2474
- firstFinalAnswerTime = Date.now();
2475
- }
2476
- }
2477
- }
2478
- catch { /* best-effort timing */ }
2409
+ if (hasOpencodeFinalAnswer(line)) {
2410
+ firstFinalAnswerTime = Date.now();
2479
2411
  }
2480
2412
  }
2481
2413
  const parsed = parseOpencodeJsonLine(line);
@@ -2670,8 +2602,10 @@ export const runAgent = (options) => {
2670
2602
  const enrichedPrompt = enrichPromptWithPreview(options.prompt, options.executionContext, options.executionTarget);
2671
2603
  return typeof enrichedPrompt === 'string' ? launch(enrichedPrompt) : enrichedPrompt.then(launch);
2672
2604
  };
2673
- const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
2605
+ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), opencodeRuntimeIdentity = null, }) => {
2674
2606
  const resolvedModel = resolveAgentModel(tool, model);
2607
+ const resolvedOpencodeCompatibility = resolveOpencodeCompatibility(tool, executionProvider, opencodeRuntimeIdentity);
2608
+ const resolvedOpencodeRuntimeIdentity = resolvedOpencodeCompatibility?.runtimeIdentity ?? null;
2675
2609
  const opencodeSessionOperations = executionProvider.sessionOperations?.opencode ?? null;
2676
2610
  const subagentTracker = createSubagentRunTracker({
2677
2611
  parentType: 'chat',
@@ -2680,6 +2614,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2680
2614
  executionContext,
2681
2615
  mainRun,
2682
2616
  deferMainFinalization: true,
2617
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
2683
2618
  });
2684
2619
  return new Promise((resolve, reject) => {
2685
2620
  let sessionId = null;
@@ -2725,6 +2660,8 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2725
2660
  mode: opencodeAgentMode,
2726
2661
  resumeSessionId,
2727
2662
  sessionId: randomUUID(),
2663
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
2664
+ opencodeCompatibility: resolvedOpencodeCompatibility,
2728
2665
  };
2729
2666
  const invocation = agentProvider.buildStreamingInvocation(streamingRequest);
2730
2667
  sessionId = invocation.sessionId;
@@ -2739,15 +2676,18 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2739
2676
  subagentTracker.setMainSessionId(resumeSessionId);
2740
2677
  }
2741
2678
  spawnTimestamp = Date.now();
2742
- opencodeMetadataRefresher = createOpencodeMetadataRefresher({
2743
- chatSessionId,
2744
- io,
2745
- spawnTimestamp,
2746
- initialSessionId: resumeSessionId,
2747
- ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
2748
- });
2749
- chatState.metadataRefresher = opencodeMetadataRefresher;
2750
- if (resumeSessionId) {
2679
+ if (opencodeSessionOperations) {
2680
+ opencodeMetadataRefresher = createOpencodeMetadataRefresher({
2681
+ chatSessionId,
2682
+ io,
2683
+ spawnTimestamp,
2684
+ cwd,
2685
+ initialSessionId: resumeSessionId,
2686
+ sessionOperations: opencodeSessionOperations,
2687
+ });
2688
+ chatState.metadataRefresher = opencodeMetadataRefresher;
2689
+ }
2690
+ if (resumeSessionId && opencodeMetadataRefresher) {
2751
2691
  const metadataRefresher = opencodeMetadataRefresher;
2752
2692
  void metadataRefresher.refresh().then(result => {
2753
2693
  if (!result.hasResolvedTitle) {
@@ -2834,7 +2774,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2834
2774
  cwd,
2835
2775
  tracker: subagentTracker,
2836
2776
  taskSessionReader: opencodeSessionOperations
2837
- ? createOpencodeTaskSessionReader(opencodeSessionOperations)
2777
+ ? createOpencodeTaskSessionReader(opencodeSessionOperations, { directory: cwd })
2838
2778
  : null,
2839
2779
  discoverParentSession: false,
2840
2780
  onParentSessionId: discoveredSessionId => {
@@ -2866,7 +2806,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2866
2806
  executionCapableEventObserved = true;
2867
2807
  }
2868
2808
  subagentTracker.handle(subagentEvents);
2869
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2809
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, line);
2870
2810
  if (logFragment !== null) {
2871
2811
  logLines.push(logFragment);
2872
2812
  }
@@ -2886,7 +2826,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2886
2826
  return;
2887
2827
  }
2888
2828
  if (remainder.trim()) {
2889
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2829
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, remainder);
2890
2830
  const parsed = parseClaudeStreamJsonLine(remainder);
2891
2831
  const { logFragment, responseFragment, subagentEvents } = parsed;
2892
2832
  if (isExecutionCapableParsedStreamLine(parsed)) {
@@ -2989,18 +2929,8 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2989
2929
  for (const line of lines) {
2990
2930
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
2991
2931
  if (firstFinalAnswerTime === null && startTime !== null) {
2992
- const trimmedLine = line.trim();
2993
- if (trimmedLine.startsWith('{')) {
2994
- try {
2995
- const event = JSON.parse(trimmedLine);
2996
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
2997
- const phase = event.part.metadata?.openai?.phase;
2998
- if (!phase || phase === 'final_answer') {
2999
- firstFinalAnswerTime = Date.now();
3000
- }
3001
- }
3002
- }
3003
- catch { /* best-effort timing */ }
2932
+ if (hasOpencodeFinalAnswer(line)) {
2933
+ firstFinalAnswerTime = Date.now();
3004
2934
  }
3005
2935
  }
3006
2936
  const parsed = parseOpencodeJsonLine(line, cwd);
@@ -3008,7 +2938,16 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
3008
2938
  if (isExecutionCapableParsedStreamLine(parsed)) {
3009
2939
  executionCapableEventObserved = true;
3010
2940
  }
3011
- opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2941
+ if (providerSessionId) {
2942
+ if (opencodeMetadataRefresher) {
2943
+ opencodeMetadataRefresher.pinSessionId(providerSessionId);
2944
+ }
2945
+ else {
2946
+ updateChatSessionId(chatSessionId, 'opencode', providerSessionId, io);
2947
+ }
2948
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2949
+ subagentTracker.setMainSessionId(providerSessionId);
2950
+ }
3012
2951
  subagentTracker.handle(subagentEvents);
3013
2952
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3014
2953
  firstFinalAnswerTime = null;
@@ -3035,7 +2974,16 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
3035
2974
  if (isExecutionCapableParsedStreamLine(parsed)) {
3036
2975
  executionCapableEventObserved = true;
3037
2976
  }
3038
- opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2977
+ if (providerSessionId) {
2978
+ if (opencodeMetadataRefresher) {
2979
+ opencodeMetadataRefresher.pinSessionId(providerSessionId);
2980
+ }
2981
+ else {
2982
+ updateChatSessionId(chatSessionId, 'opencode', providerSessionId, io);
2983
+ }
2984
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
2985
+ subagentTracker.setMainSessionId(providerSessionId);
2986
+ }
3039
2987
  subagentTracker.handle(subagentEvents);
3040
2988
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3041
2989
  firstFinalAnswerTime = null;
@@ -3061,12 +3009,12 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
3061
3009
  ?? resumeSessionId;
3062
3010
  await opencodeSubagentMonitor?.refresh();
3063
3011
  if (chatState.stopRequested) {
3064
- if (resolvedOpencodeSessionId) {
3012
+ if (resolvedOpencodeSessionId && opencodeSessionOperations) {
3065
3013
  usageSnapshot = await resolveOpencodeUsageSnapshot({
3066
3014
  current: usageSnapshot,
3067
3015
  sessionId: resolvedOpencodeSessionId,
3068
3016
  spawnTimestamp,
3069
- ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
3017
+ sessionOperations: opencodeSessionOperations,
3070
3018
  });
3071
3019
  }
3072
3020
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
@@ -3086,12 +3034,12 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
3086
3034
  log,
3087
3035
  response: finalResponse,
3088
3036
  });
3089
- if (resolvedOpencodeSessionId && spawnTimestamp !== null) {
3037
+ if (resolvedOpencodeSessionId && spawnTimestamp !== null && opencodeSessionOperations) {
3090
3038
  usageSnapshot = await resolveOpencodeUsageSnapshot({
3091
3039
  current: usageSnapshot,
3092
3040
  sessionId: resolvedOpencodeSessionId,
3093
3041
  spawnTimestamp,
3094
- ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
3042
+ sessionOperations: opencodeSessionOperations,
3095
3043
  });
3096
3044
  }
3097
3045
  updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
@@ -3155,8 +3103,11 @@ export const runChatAgent = (options) => {
3155
3103
  const enrichedPrompt = enrichPromptWithPreview(options.prompt, options.executionContext, options.executionTarget);
3156
3104
  return typeof enrichedPrompt === 'string' ? launch(enrichedPrompt) : enrichedPrompt.then(launch);
3157
3105
  };
3158
- const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], resumeSessionId = null, executionContext, mainRun, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
3106
+ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], resumeSessionId = null, executionContext, mainRun, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), opencodeRuntimeIdentity = null, }) => {
3159
3107
  const resolvedModel = resolveAgentModel(tool, model);
3108
+ const resolvedOpencodeCompatibility = resolveOpencodeCompatibility(tool, executionProvider, opencodeRuntimeIdentity);
3109
+ const resolvedOpencodeRuntimeIdentity = resolvedOpencodeCompatibility?.runtimeIdentity ?? null;
3110
+ const opencodeSessionOperations = executionProvider.sessionOperations?.opencode ?? null;
3160
3111
  const subagentTracker = createSubagentRunTracker({
3161
3112
  parentType: 'spec',
3162
3113
  parentId: specSessionId,
@@ -3164,6 +3115,7 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3164
3115
  executionContext,
3165
3116
  mainRun,
3166
3117
  deferMainFinalization: true,
3118
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
3167
3119
  });
3168
3120
  return new Promise((resolve, reject) => {
3169
3121
  let sessionId = null;
@@ -3209,6 +3161,8 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3209
3161
  mode: 'plan',
3210
3162
  resumeSessionId,
3211
3163
  sessionId: randomUUID(),
3164
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
3165
+ opencodeCompatibility: resolvedOpencodeCompatibility,
3212
3166
  };
3213
3167
  const invocation = agentProvider.buildStreamingInvocation(streamingRequest);
3214
3168
  sessionId = invocation.sessionId;
@@ -3223,14 +3177,18 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3223
3177
  subagentTracker.setMainSessionId(resumeSessionId);
3224
3178
  }
3225
3179
  spawnTimestamp = Date.now();
3226
- opencodeMetadataRefresher = createSpecOpencodeMetadataRefresher({
3227
- specSessionId,
3228
- io,
3229
- spawnTimestamp,
3230
- initialSessionId: resumeSessionId,
3231
- });
3232
- specState.metadataRefresher = opencodeMetadataRefresher;
3233
- if (resumeSessionId) {
3180
+ if (opencodeSessionOperations) {
3181
+ opencodeMetadataRefresher = createSpecOpencodeMetadataRefresher({
3182
+ specSessionId,
3183
+ io,
3184
+ spawnTimestamp,
3185
+ cwd,
3186
+ initialSessionId: resumeSessionId,
3187
+ sessionOperations: opencodeSessionOperations,
3188
+ });
3189
+ specState.metadataRefresher = opencodeMetadataRefresher;
3190
+ }
3191
+ if (resumeSessionId && opencodeMetadataRefresher) {
3234
3192
  const metadataRefresher = opencodeMetadataRefresher;
3235
3193
  void metadataRefresher.refresh().then(result => {
3236
3194
  if (!result.hasResolvedTitle) {
@@ -3307,6 +3265,9 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3307
3265
  spawnTimestamp,
3308
3266
  cwd,
3309
3267
  tracker: subagentTracker,
3268
+ taskSessionReader: opencodeSessionOperations
3269
+ ? createOpencodeTaskSessionReader(opencodeSessionOperations, { directory: cwd })
3270
+ : null,
3310
3271
  discoverParentSession: false,
3311
3272
  onParentSessionId: discoveredSessionId => {
3312
3273
  updateSpecSessionIdLocal(specSessionId, 'opencode', discoveredSessionId, io);
@@ -3337,7 +3298,7 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3337
3298
  executionCapableEventObserved = true;
3338
3299
  }
3339
3300
  subagentTracker.handle(subagentEvents);
3340
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
3301
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, line);
3341
3302
  if (logFragment !== null) {
3342
3303
  logLines.push(logFragment);
3343
3304
  }
@@ -3358,7 +3319,7 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3358
3319
  return;
3359
3320
  }
3360
3321
  if (remainder.trim()) {
3361
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3322
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, remainder);
3362
3323
  const parsed = parseClaudeStreamJsonLine(remainder);
3363
3324
  const { logFragment, responseFragment, subagentEvents } = parsed;
3364
3325
  if (isExecutionCapableParsedStreamLine(parsed)) {
@@ -3461,18 +3422,8 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3461
3422
  for (const line of lines) {
3462
3423
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
3463
3424
  if (firstFinalAnswerTime === null && startTime !== null) {
3464
- const trimmedLine = line.trim();
3465
- if (trimmedLine.startsWith('{')) {
3466
- try {
3467
- const event = JSON.parse(trimmedLine);
3468
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
3469
- const phase = event.part.metadata?.openai?.phase;
3470
- if (!phase || phase === 'final_answer') {
3471
- firstFinalAnswerTime = Date.now();
3472
- }
3473
- }
3474
- }
3475
- catch { /* best-effort timing */ }
3425
+ if (hasOpencodeFinalAnswer(line)) {
3426
+ firstFinalAnswerTime = Date.now();
3476
3427
  }
3477
3428
  }
3478
3429
  const parsed = parseOpencodeJsonLine(line, cwd);
@@ -3480,7 +3431,16 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3480
3431
  if (isExecutionCapableParsedStreamLine(parsed)) {
3481
3432
  executionCapableEventObserved = true;
3482
3433
  }
3483
- opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3434
+ if (providerSessionId) {
3435
+ if (opencodeMetadataRefresher) {
3436
+ opencodeMetadataRefresher.pinSessionId(providerSessionId);
3437
+ }
3438
+ else {
3439
+ updateSpecSessionIdLocal(specSessionId, 'opencode', providerSessionId, io);
3440
+ }
3441
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3442
+ subagentTracker.setMainSessionId(providerSessionId);
3443
+ }
3484
3444
  subagentTracker.handle(subagentEvents);
3485
3445
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3486
3446
  firstFinalAnswerTime = null;
@@ -3509,7 +3469,16 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3509
3469
  if (isExecutionCapableParsedStreamLine(parsed)) {
3510
3470
  executionCapableEventObserved = true;
3511
3471
  }
3512
- opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3472
+ if (providerSessionId) {
3473
+ if (opencodeMetadataRefresher) {
3474
+ opencodeMetadataRefresher.pinSessionId(providerSessionId);
3475
+ }
3476
+ else {
3477
+ updateSpecSessionIdLocal(specSessionId, 'opencode', providerSessionId, io);
3478
+ }
3479
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3480
+ subagentTracker.setMainSessionId(providerSessionId);
3481
+ }
3513
3482
  subagentTracker.handle(subagentEvents);
3514
3483
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3515
3484
  firstFinalAnswerTime = null;
@@ -3535,11 +3504,12 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3535
3504
  ?? resumeSessionId;
3536
3505
  await opencodeSubagentMonitor?.refresh();
3537
3506
  if (specState.stopRequested) {
3538
- if (resolvedOpencodeSessionId) {
3507
+ if (resolvedOpencodeSessionId && opencodeSessionOperations) {
3539
3508
  usageSnapshot = await resolveOpencodeUsageSnapshot({
3540
3509
  current: usageSnapshot,
3541
3510
  sessionId: resolvedOpencodeSessionId,
3542
3511
  spawnTimestamp,
3512
+ sessionOperations: opencodeSessionOperations,
3543
3513
  });
3544
3514
  }
3545
3515
  updateSpecSessionStatusLocal(specSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
@@ -3559,11 +3529,12 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3559
3529
  log,
3560
3530
  response: finalResponse,
3561
3531
  });
3562
- if (resolvedOpencodeSessionId && spawnTimestamp !== null) {
3532
+ if (resolvedOpencodeSessionId && spawnTimestamp !== null && opencodeSessionOperations) {
3563
3533
  usageSnapshot = await resolveOpencodeUsageSnapshot({
3564
3534
  current: usageSnapshot,
3565
3535
  sessionId: resolvedOpencodeSessionId,
3566
3536
  spawnTimestamp,
3537
+ sessionOperations: opencodeSessionOperations,
3567
3538
  });
3568
3539
  }
3569
3540
  updateSpecSessionStatusLocal(specSessionId, 'running', finalOutput.log, io, finalOutput.response);
@@ -3637,7 +3608,7 @@ export const runSpecAgent = (options) => {
3637
3608
  * updateSpecReviewRunStatus / updateSpecReviewRunStreaming / updateSpecReviewRunUsage —
3638
3609
  * the spec_sessions row is never touched by this path.
3639
3610
  */
3640
- const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], executionContext, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
3611
+ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, skills = [], executionContext, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), opencodeRuntimeIdentity = null, }) => {
3641
3612
  const registeredState = activeSpecReviewProcesses.get(specReviewRunId);
3642
3613
  if (registeredState?.stopRequested) {
3643
3614
  return Promise.resolve({
@@ -3648,11 +3619,15 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3648
3619
  });
3649
3620
  }
3650
3621
  const resolvedModel = resolveAgentModel(tool, model);
3622
+ const resolvedOpencodeCompatibility = resolveOpencodeCompatibility(tool, executionProvider, opencodeRuntimeIdentity);
3623
+ const resolvedOpencodeRuntimeIdentity = resolvedOpencodeCompatibility?.runtimeIdentity ?? null;
3624
+ const opencodeSessionOperations = executionProvider.sessionOperations?.opencode ?? null;
3651
3625
  const subagentTracker = createSubagentRunTracker({
3652
3626
  parentType: 'spec',
3653
3627
  parentId: specSessionId,
3654
3628
  io,
3655
3629
  executionContext,
3630
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
3656
3631
  });
3657
3632
  return new Promise((resolve, reject) => {
3658
3633
  let spawnTimestamp = null;
@@ -3696,9 +3671,18 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3696
3671
  mode: 'plan',
3697
3672
  resumeSessionId: null,
3698
3673
  sessionId: randomUUID(),
3674
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
3675
+ opencodeCompatibility: resolvedOpencodeCompatibility,
3699
3676
  };
3700
3677
  const invocation = tool === 'opencode'
3701
- ? buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant })
3678
+ ? buildOpencodeSpecReviewInvocation({
3679
+ prompt,
3680
+ files,
3681
+ model: resolvedModel,
3682
+ variant,
3683
+ opencodeRuntimeIdentity: resolvedOpencodeRuntimeIdentity,
3684
+ opencodeCompatibility: resolvedOpencodeCompatibility,
3685
+ })
3702
3686
  : agentProvider.buildStreamingInvocation(streamingRequest);
3703
3687
  try {
3704
3688
  const handle = executionProvider.execute({
@@ -3763,6 +3747,9 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3763
3747
  spawnTimestamp,
3764
3748
  cwd,
3765
3749
  tracker: subagentTracker,
3750
+ taskSessionReader: opencodeSessionOperations
3751
+ ? createOpencodeTaskSessionReader(opencodeSessionOperations, { directory: cwd })
3752
+ : null,
3766
3753
  parentDiscoveryDelayMs: OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS,
3767
3754
  });
3768
3755
  opencodeSubagentMonitor.start();
@@ -3780,7 +3767,7 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3780
3767
  const lines = text.split('\n');
3781
3768
  remainder = lines.pop() ?? '';
3782
3769
  for (const line of lines) {
3783
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
3770
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, line);
3784
3771
  const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(line);
3785
3772
  subagentTracker.handle(subagentEvents);
3786
3773
  if (logFragment !== null) {
@@ -3803,7 +3790,7 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3803
3790
  return;
3804
3791
  }
3805
3792
  if (remainder.trim()) {
3806
- usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3793
+ usageSnapshot = mergeClaudeUsageFromJsonLine(usageSnapshot, remainder);
3807
3794
  const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
3808
3795
  subagentTracker.handle(subagentEvents);
3809
3796
  if (logFragment !== null) {
@@ -3865,18 +3852,8 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3865
3852
  for (const line of lines) {
3866
3853
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
3867
3854
  if (firstFinalAnswerTime === null && startTime !== null) {
3868
- const trimmedLine = line.trim();
3869
- if (trimmedLine.startsWith('{')) {
3870
- try {
3871
- const event = JSON.parse(trimmedLine);
3872
- if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
3873
- const phase = event.part.metadata?.openai?.phase;
3874
- if (!phase || phase === 'final_answer') {
3875
- firstFinalAnswerTime = Date.now();
3876
- }
3877
- }
3878
- }
3879
- catch { /* best-effort timing */ }
3855
+ if (hasOpencodeFinalAnswer(line)) {
3856
+ firstFinalAnswerTime = Date.now();
3880
3857
  }
3881
3858
  }
3882
3859
  const parsed = parseOpencodeJsonLine(line, cwd);
@@ -3917,11 +3894,12 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3917
3894
  const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3918
3895
  ? recoveredResponse
3919
3896
  : responseLines.join('\n\n') || stderrOutput;
3920
- if (opencodeSessionId && spawnTimestamp !== null) {
3897
+ if (opencodeSessionId && spawnTimestamp !== null && opencodeSessionOperations) {
3921
3898
  usageSnapshot = await resolveOpencodeUsageSnapshot({
3922
3899
  current: usageSnapshot,
3923
3900
  sessionId: opencodeSessionId,
3924
3901
  spawnTimestamp,
3902
+ sessionOperations: opencodeSessionOperations,
3925
3903
  });
3926
3904
  }
3927
3905
  if (reviewState.stopRequested) {