charness 3.2.0 → 3.3.1

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 (313) hide show
  1. package/.env.example +3 -6
  2. package/DEVELOPMENT.md +15 -0
  3. package/README.md +111 -38
  4. package/SETUP.md +121 -16
  5. package/dist/adapters/adapterEventRouting.d.ts +2 -1
  6. package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
  7. package/dist/adapters/adapterEventRouting.js +3 -0
  8. package/dist/adapters/adapterEventRouting.js.map +1 -1
  9. package/dist/adapters/adapterNames.d.ts +13 -0
  10. package/dist/adapters/adapterNames.d.ts.map +1 -1
  11. package/dist/adapters/adapterNames.js +16 -0
  12. package/dist/adapters/adapterNames.js.map +1 -1
  13. package/dist/adapters/backendUnavailableError.d.ts +11 -0
  14. package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
  15. package/dist/adapters/backendUnavailableError.js +19 -0
  16. package/dist/adapters/backendUnavailableError.js.map +1 -0
  17. package/dist/adapters/claudeCliAdapter.d.ts +29 -6
  18. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  19. package/dist/adapters/claudeCliAdapter.js +70 -16
  20. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  21. package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
  22. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  23. package/dist/adapters/claudeJsonStreamAdapter.js +93 -30
  24. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  25. package/dist/adapters/createAdapter.d.ts +3 -14
  26. package/dist/adapters/createAdapter.d.ts.map +1 -1
  27. package/dist/adapters/createAdapter.js +9 -19
  28. package/dist/adapters/createAdapter.js.map +1 -1
  29. package/dist/adapters/openCodeAdapter.d.ts +140 -12
  30. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  31. package/dist/adapters/openCodeAdapter.js +385 -119
  32. package/dist/adapters/openCodeAdapter.js.map +1 -1
  33. package/dist/apiErrorRetry.d.ts +70 -6
  34. package/dist/apiErrorRetry.d.ts.map +1 -1
  35. package/dist/apiErrorRetry.js +312 -34
  36. package/dist/apiErrorRetry.js.map +1 -1
  37. package/dist/apiRetryKick.d.ts +26 -1
  38. package/dist/apiRetryKick.d.ts.map +1 -1
  39. package/dist/apiRetryKick.js +41 -2
  40. package/dist/apiRetryKick.js.map +1 -1
  41. package/dist/bot.d.ts +2 -1
  42. package/dist/bot.d.ts.map +1 -1
  43. package/dist/bot.js +736 -184
  44. package/dist/bot.js.map +1 -1
  45. package/dist/cli/claudeProfiles.d.ts +1 -1
  46. package/dist/cli/claudeProfiles.js +1 -1
  47. package/dist/cli/hot.d.ts +7 -1
  48. package/dist/cli/hot.d.ts.map +1 -1
  49. package/dist/cli/hot.js +29 -3
  50. package/dist/cli/hot.js.map +1 -1
  51. package/dist/configDirFile.d.ts +73 -0
  52. package/dist/configDirFile.d.ts.map +1 -0
  53. package/dist/configDirFile.js +216 -0
  54. package/dist/configDirFile.js.map +1 -0
  55. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
  56. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  57. package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
  58. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  59. package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
  60. package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
  61. package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
  62. package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
  63. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  64. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  65. package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
  66. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  67. package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
  68. package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
  69. package/dist/connectors/telegram/commands/callbackToast.js +83 -0
  70. package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
  71. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
  72. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  73. package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
  74. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  75. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  76. package/dist/connectors/telegram/commands/displayModes.js +4 -2
  77. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  78. package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
  79. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  80. package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
  81. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  82. package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
  83. package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
  84. package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
  85. package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
  86. package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
  87. package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
  88. package/dist/connectors/telegram/commands/modelPage.js +120 -0
  89. package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
  90. package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
  91. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  92. package/dist/connectors/telegram/commands/modelProviders.js +203 -111
  93. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  94. package/dist/connectors/telegram/commands/stt.d.ts +32 -0
  95. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
  96. package/dist/connectors/telegram/commands/stt.js +460 -0
  97. package/dist/connectors/telegram/commands/stt.js.map +1 -0
  98. package/dist/effortLevels.d.ts +57 -7
  99. package/dist/effortLevels.d.ts.map +1 -1
  100. package/dist/effortLevels.js +65 -9
  101. package/dist/effortLevels.js.map +1 -1
  102. package/dist/i18n/de.d.ts.map +1 -1
  103. package/dist/i18n/de.js +160 -17
  104. package/dist/i18n/de.js.map +1 -1
  105. package/dist/i18n/en.d.ts.map +1 -1
  106. package/dist/i18n/en.js +160 -17
  107. package/dist/i18n/en.js.map +1 -1
  108. package/dist/i18n/es.d.ts.map +1 -1
  109. package/dist/i18n/es.js +160 -17
  110. package/dist/i18n/es.js.map +1 -1
  111. package/dist/i18n/fr.d.ts.map +1 -1
  112. package/dist/i18n/fr.js +160 -17
  113. package/dist/i18n/fr.js.map +1 -1
  114. package/dist/i18n/hi.d.ts.map +1 -1
  115. package/dist/i18n/hi.js +160 -17
  116. package/dist/i18n/hi.js.map +1 -1
  117. package/dist/i18n/ja.d.ts.map +1 -1
  118. package/dist/i18n/ja.js +160 -17
  119. package/dist/i18n/ja.js.map +1 -1
  120. package/dist/i18n/ka.d.ts.map +1 -1
  121. package/dist/i18n/ka.js +160 -17
  122. package/dist/i18n/ka.js.map +1 -1
  123. package/dist/i18n/pt.d.ts.map +1 -1
  124. package/dist/i18n/pt.js +160 -17
  125. package/dist/i18n/pt.js.map +1 -1
  126. package/dist/i18n/ru.d.ts.map +1 -1
  127. package/dist/i18n/ru.js +160 -17
  128. package/dist/i18n/ru.js.map +1 -1
  129. package/dist/i18n/uk.d.ts.map +1 -1
  130. package/dist/i18n/uk.js +160 -17
  131. package/dist/i18n/uk.js.map +1 -1
  132. package/dist/i18n/uz.d.ts.map +1 -1
  133. package/dist/i18n/uz.js +160 -17
  134. package/dist/i18n/uz.js.map +1 -1
  135. package/dist/i18n/zh.d.ts.map +1 -1
  136. package/dist/i18n/zh.js +160 -17
  137. package/dist/i18n/zh.js.map +1 -1
  138. package/dist/installManager.d.ts +20 -4
  139. package/dist/installManager.d.ts.map +1 -1
  140. package/dist/installManager.js +82 -14
  141. package/dist/installManager.js.map +1 -1
  142. package/dist/instanceSettings.d.ts +52 -0
  143. package/dist/instanceSettings.d.ts.map +1 -0
  144. package/dist/instanceSettings.js +136 -0
  145. package/dist/instanceSettings.js.map +1 -0
  146. package/dist/pinnedStatus.d.ts +7 -1
  147. package/dist/pinnedStatus.d.ts.map +1 -1
  148. package/dist/pinnedStatus.js +1 -1
  149. package/dist/pinnedStatus.js.map +1 -1
  150. package/dist/requests/requestLedger.d.ts +8 -0
  151. package/dist/requests/requestLedger.d.ts.map +1 -1
  152. package/dist/requests/requestLedger.js +19 -1
  153. package/dist/requests/requestLedger.js.map +1 -1
  154. package/dist/scheduler/mcpSurface.js +4 -4
  155. package/dist/scheduler/mcpSurface.js.map +1 -1
  156. package/dist/shutdown.d.ts +14 -0
  157. package/dist/shutdown.d.ts.map +1 -1
  158. package/dist/shutdown.js +16 -0
  159. package/dist/shutdown.js.map +1 -1
  160. package/dist/state.d.ts +54 -2
  161. package/dist/state.d.ts.map +1 -1
  162. package/dist/state.js +62 -1
  163. package/dist/state.js.map +1 -1
  164. package/dist/stt/sttAutoInstall.d.ts +39 -0
  165. package/dist/stt/sttAutoInstall.d.ts.map +1 -0
  166. package/dist/stt/sttAutoInstall.js +158 -0
  167. package/dist/stt/sttAutoInstall.js.map +1 -0
  168. package/dist/stt/sttChildEnv.d.ts +11 -0
  169. package/dist/stt/sttChildEnv.d.ts.map +1 -0
  170. package/dist/stt/sttChildEnv.js +30 -0
  171. package/dist/stt/sttChildEnv.js.map +1 -0
  172. package/dist/stt/sttConfigEdit.d.ts +71 -0
  173. package/dist/stt/sttConfigEdit.d.ts.map +1 -0
  174. package/dist/stt/sttConfigEdit.js +144 -0
  175. package/dist/stt/sttConfigEdit.js.map +1 -0
  176. package/dist/stt/sttInstall.d.ts +147 -0
  177. package/dist/stt/sttInstall.d.ts.map +1 -0
  178. package/dist/stt/sttInstall.js +491 -0
  179. package/dist/stt/sttInstall.js.map +1 -0
  180. package/dist/stt/sttInstallEntry.d.ts +2 -0
  181. package/dist/stt/sttInstallEntry.d.ts.map +1 -0
  182. package/dist/stt/sttInstallEntry.js +31 -0
  183. package/dist/stt/sttInstallEntry.js.map +1 -0
  184. package/dist/stt/sttInstallReporter.d.ts +45 -0
  185. package/dist/stt/sttInstallReporter.d.ts.map +1 -0
  186. package/dist/stt/sttInstallReporter.js +57 -0
  187. package/dist/stt/sttInstallReporter.js.map +1 -0
  188. package/dist/stt/sttRuntime.d.ts +35 -0
  189. package/dist/stt/sttRuntime.d.ts.map +1 -0
  190. package/dist/stt/sttRuntime.js +73 -0
  191. package/dist/stt/sttRuntime.js.map +1 -0
  192. package/dist/stt/sttScreenPlan.d.ts +193 -0
  193. package/dist/stt/sttScreenPlan.d.ts.map +1 -0
  194. package/dist/stt/sttScreenPlan.js +541 -0
  195. package/dist/stt/sttScreenPlan.js.map +1 -0
  196. package/dist/stt/sttSettings.d.ts +201 -0
  197. package/dist/stt/sttSettings.d.ts.map +1 -0
  198. package/dist/stt/sttSettings.js +387 -0
  199. package/dist/stt/sttSettings.js.map +1 -0
  200. package/dist/stt/sttSidecarOwner.d.ts +115 -0
  201. package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
  202. package/dist/stt/sttSidecarOwner.js +475 -0
  203. package/dist/stt/sttSidecarOwner.js.map +1 -0
  204. package/dist/stt/transcribeChain.d.ts +36 -0
  205. package/dist/stt/transcribeChain.d.ts.map +1 -0
  206. package/dist/stt/transcribeChain.js +42 -0
  207. package/dist/stt/transcribeChain.js.map +1 -0
  208. package/dist/types.d.ts +77 -4
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
  211. package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
  212. package/dist/utils/adapterSwitchSessionIds.js +44 -0
  213. package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
  214. package/dist/utils/backendSwitchPlan.d.ts +63 -0
  215. package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
  216. package/dist/utils/backendSwitchPlan.js +73 -0
  217. package/dist/utils/backendSwitchPlan.js.map +1 -0
  218. package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
  219. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  220. package/dist/utils/claudeAccountPickerPlan.js +20 -1
  221. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  222. package/dist/utils/claudeTranscriptContent.d.ts +24 -0
  223. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
  224. package/dist/utils/claudeTranscriptContent.js +107 -0
  225. package/dist/utils/claudeTranscriptContent.js.map +1 -0
  226. package/dist/utils/claudeUsage.d.ts +47 -8
  227. package/dist/utils/claudeUsage.d.ts.map +1 -1
  228. package/dist/utils/claudeUsage.js +76 -14
  229. package/dist/utils/claudeUsage.js.map +1 -1
  230. package/dist/utils/claudeUsageRefresher.d.ts +8 -1
  231. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  232. package/dist/utils/claudeUsageRefresher.js +9 -4
  233. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  234. package/dist/utils/effortPickerPlan.d.ts +54 -0
  235. package/dist/utils/effortPickerPlan.d.ts.map +1 -0
  236. package/dist/utils/effortPickerPlan.js +56 -0
  237. package/dist/utils/effortPickerPlan.js.map +1 -0
  238. package/dist/utils/ensureSessionPlan.d.ts +20 -4
  239. package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
  240. package/dist/utils/ensureSessionPlan.js +17 -4
  241. package/dist/utils/ensureSessionPlan.js.map +1 -1
  242. package/dist/utils/fallbackBridge.d.ts +6 -2
  243. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  244. package/dist/utils/fallbackBridge.js +10 -3
  245. package/dist/utils/fallbackBridge.js.map +1 -1
  246. package/dist/utils/fallbackChain.d.ts +36 -0
  247. package/dist/utils/fallbackChain.d.ts.map +1 -1
  248. package/dist/utils/fallbackChain.js +65 -1
  249. package/dist/utils/fallbackChain.js.map +1 -1
  250. package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
  251. package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
  252. package/dist/utils/fallbackSwitchNotice.js +75 -0
  253. package/dist/utils/fallbackSwitchNotice.js.map +1 -0
  254. package/dist/utils/fileSendService.d.ts +33 -10
  255. package/dist/utils/fileSendService.d.ts.map +1 -1
  256. package/dist/utils/fileSendService.js +78 -27
  257. package/dist/utils/fileSendService.js.map +1 -1
  258. package/dist/utils/jsonStreamHost.d.ts +39 -0
  259. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  260. package/dist/utils/jsonStreamHost.js +60 -1
  261. package/dist/utils/jsonStreamHost.js.map +1 -1
  262. package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
  263. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
  264. package/dist/utils/limitEpisodeRecovery.js +44 -3
  265. package/dist/utils/limitEpisodeRecovery.js.map +1 -1
  266. package/dist/utils/limitSettings.d.ts +25 -0
  267. package/dist/utils/limitSettings.d.ts.map +1 -0
  268. package/dist/utils/limitSettings.js +31 -0
  269. package/dist/utils/limitSettings.js.map +1 -0
  270. package/dist/utils/limitsBoardPlan.d.ts +52 -0
  271. package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
  272. package/dist/utils/limitsBoardPlan.js +93 -0
  273. package/dist/utils/limitsBoardPlan.js.map +1 -0
  274. package/dist/utils/openCodeUsage.d.ts +93 -0
  275. package/dist/utils/openCodeUsage.d.ts.map +1 -0
  276. package/dist/utils/openCodeUsage.js +248 -0
  277. package/dist/utils/openCodeUsage.js.map +1 -0
  278. package/dist/utils/operatorSwitchPlan.d.ts +61 -0
  279. package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
  280. package/dist/utils/operatorSwitchPlan.js +32 -0
  281. package/dist/utils/operatorSwitchPlan.js.map +1 -0
  282. package/dist/utils/promptDelivery.d.ts +40 -0
  283. package/dist/utils/promptDelivery.d.ts.map +1 -1
  284. package/dist/utils/promptDelivery.js +31 -0
  285. package/dist/utils/promptDelivery.js.map +1 -1
  286. package/dist/utils/recvPreviewRedaction.d.ts +14 -4
  287. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  288. package/dist/utils/recvPreviewRedaction.js +19 -7
  289. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  290. package/dist/utils/startupReadiness.d.ts +3 -3
  291. package/dist/utils/startupReadiness.d.ts.map +1 -1
  292. package/dist/utils/startupReadiness.js +2 -2
  293. package/dist/utils/startupReadiness.js.map +1 -1
  294. package/dist/utils/threadStatusReport.d.ts +3 -1
  295. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  296. package/dist/utils/threadStatusReport.js +4 -3
  297. package/dist/utils/threadStatusReport.js.map +1 -1
  298. package/dist/utils/timezone.d.ts +23 -0
  299. package/dist/utils/timezone.d.ts.map +1 -1
  300. package/dist/utils/timezone.js +38 -0
  301. package/dist/utils/timezone.js.map +1 -1
  302. package/dist/utils/transcribeAudio.d.ts +25 -12
  303. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  304. package/dist/utils/transcribeAudio.js +39 -21
  305. package/dist/utils/transcribeAudio.js.map +1 -1
  306. package/dist/utils/usageRefreshBudget.d.ts +9 -0
  307. package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
  308. package/dist/utils/usageRefreshBudget.js +27 -0
  309. package/dist/utils/usageRefreshBudget.js.map +1 -0
  310. package/examples/.env.example +7 -3
  311. package/examples/docker-compose.yml +9 -7
  312. package/package.json +5 -4
  313. package/stt/gigaam_sidecar.py +800 -0
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.OpenCodeAdapter = exports.schedulerMcpRetryDelaysMs = exports.openCodeErrorMessageMaxLength = exports.providerRetryReplacementStartTimeoutMs = exports.criticalSseEventTypes = exports.openCodeRuntimeContextHydrationConcurrency = void 0;
36
+ exports.OpenCodeAdapter = exports.openCodeServerRestartRetryBudgetMs = exports.openCodeServerRestartRetryDelaysMs = exports.schedulerMcpRetryDelaysMs = exports.openCodeErrorMessageMaxLength = exports.providerRetryReplacementStartTimeoutMs = exports.criticalSseEventTypes = exports.openCodeRuntimeContextHydrationConcurrency = void 0;
37
37
  exports.listSavedOpenCodeModelLabels = listSavedOpenCodeModelLabels;
38
38
  exports.normaliseOpenCodeSseEvent = normaliseOpenCodeSseEvent;
39
39
  exports.buildDirectoryScopedPath = buildDirectoryScopedPath;
@@ -45,6 +45,8 @@ exports.checkIsOpenCodeTurnInFlight = checkIsOpenCodeTurnInFlight;
45
45
  exports.summarizeProviderRetryReason = summarizeProviderRetryReason;
46
46
  exports.checkIsOpenCodeAbortError = checkIsOpenCodeAbortError;
47
47
  exports.getOpenCodeErrorMessage = getOpenCodeErrorMessage;
48
+ exports.getOpenCodeErrorLogText = getOpenCodeErrorLogText;
49
+ exports.getOpenCodeErrorRetryAfterAt = getOpenCodeErrorRetryAfterAt;
48
50
  exports.checkIsOpenCodeSessionBusy = checkIsOpenCodeSessionBusy;
49
51
  exports.applyOpenCodeStatusEvent = applyOpenCodeStatusEvent;
50
52
  exports.buildPromptBody = buildPromptBody;
@@ -53,6 +55,7 @@ exports.getLatestOpenCodeParentAssistantContext = getLatestOpenCodeParentAssista
53
55
  exports.buildProviderAuthPath = buildProviderAuthPath;
54
56
  exports.checkNeedsSchedulerMcpReregister = checkNeedsSchedulerMcpReregister;
55
57
  exports.getSchedulerMcpRetryDelayMs = getSchedulerMcpRetryDelayMs;
58
+ exports.getOpenCodeServerRestartRetryDelayMs = getOpenCodeServerRestartRetryDelayMs;
56
59
  exports.buildProviderApiAuthPayload = buildProviderApiAuthPayload;
57
60
  exports.resetOpenCodeProviderCaches = resetOpenCodeProviderCaches;
58
61
  exports.parseProvidersResponse = parseProvidersResponse;
@@ -66,6 +69,8 @@ const apiErrorRetry_1 = require("../apiErrorRetry");
66
69
  const installManager_1 = require("../installManager");
67
70
  const state_1 = require("../state");
68
71
  const diagLog_1 = require("../diagLog");
72
+ const utils_1 = require("../utils");
73
+ const backendUnavailableError_1 = require("./backendUnavailableError");
69
74
  const openCodeSessionRouting_1 = require("../openCodeSessionRouting");
70
75
  const i18n_1 = require("../i18n");
71
76
  const resumeContext_1 = require("../resumeContext");
@@ -574,6 +579,8 @@ function checkIsOpenCodeAbortError(errorMsg) {
574
579
  return errorMsg.trim().toLowerCase() === 'aborted';
575
580
  }
576
581
  const genericOpenCodeErrorMessage = 'OpenCode request failed';
582
+ /** The two channels OpenCode reports a turn's failure on, and the line each one posts. */
583
+ const openCodeFailurePrefixes = { 'session.error': 'OpenCode error: ', 'message.error': 'Error: ' };
577
584
  /** Placeholder written over every redacted credential. */
578
585
  const openCodeErrorRedactedMarker = '[redacted]';
579
586
  /**
@@ -614,18 +621,45 @@ const openCodeErrorCredentialValueRe = /\bsk-[A-Za-z0-9_-]+|\bghp_[A-Za-z0-9]+|\
614
621
  * required: a bare `https://<token>@host` is just as much a credential.
615
622
  */
616
623
  const openCodeErrorUrlUserInfoRe = /(\w+:\/\/)[^\s/@]+@/g;
617
- /**
618
- * @description Redact the credentials a surfaced error string is known to carry
619
- * and cap it. Redaction runs BEFORE the cap: capping first could cut a secret
620
- * assignment in half and leave the head of the credential visible.
621
- */
622
- function redactOpenCodeErrorMessage(message) {
624
+ /** @description Redact the credentials an error string is known to carry (see the rules above). */
625
+ function redactOpenCodeErrorCredentials(message) {
623
626
  return message
624
627
  .replace(openCodeErrorSecretAssignmentRe, `$1$2${openCodeErrorRedactedMarker}`)
625
628
  .replace(openCodeErrorAuthSchemeRe, `$1 ${openCodeErrorRedactedMarker}`)
626
629
  .replace(openCodeErrorCredentialValueRe, openCodeErrorRedactedMarker)
627
- .replace(openCodeErrorUrlUserInfoRe, `$1${openCodeErrorRedactedMarker}@`)
628
- .slice(0, exports.openCodeErrorMessageMaxLength);
630
+ .replace(openCodeErrorUrlUserInfoRe, `$1${openCodeErrorRedactedMarker}@`);
631
+ }
632
+ /**
633
+ * The start of the diagnostics block an auth plugin appends to every provider error it relays
634
+ * (opencode-antigravity-auth: `\n\n[Debug Info]\nRequested Model: …\nProject: …\nEndpoint: …\nTool Debug Payload: …`): the
635
+ * user's Google project id and part of the tool schema — nothing the person in the topic can act on, and it pushed the
636
+ * provider's own reason out of the capped line. Cut before the text is surfaced; the bot log keeps it
637
+ * ({@link getOpenCodeErrorLogText}).
638
+ */
639
+ const openCodePluginDiagnosticsMarker = '\n\n[Debug Info]';
640
+ /**
641
+ * @description The surfaced form of an error string: the plugin diagnostics cut ({@link openCodePluginDiagnosticsMarker}),
642
+ * the credentials redacted, the length capped. Redaction runs BEFORE the cap: capping first could cut a secret
643
+ * assignment in half and leave the head of the credential visible.
644
+ */
645
+ function redactOpenCodeErrorMessage(message) {
646
+ const diagnosticsStart = message.indexOf(openCodePluginDiagnosticsMarker);
647
+ const reason = diagnosticsStart >= 0 ? message.slice(0, diagnosticsStart) : message;
648
+ return redactOpenCodeErrorCredentials(reason).slice(0, exports.openCodeErrorMessageMaxLength);
649
+ }
650
+ /** @description The message string of OpenCode's known error shapes (`{ data: { message } }`, `{ message }`, a string), else `null`. */
651
+ function readOpenCodeErrorString(error) {
652
+ if (typeof error === 'string')
653
+ return error;
654
+ if (!error || typeof error !== 'object')
655
+ return null;
656
+ const errorRecord = error;
657
+ if (errorRecord.data && typeof errorRecord.data === 'object') {
658
+ const errorData = errorRecord.data;
659
+ if (typeof errorData.message === 'string')
660
+ return errorData.message;
661
+ }
662
+ return typeof errorRecord.message === 'string' ? errorRecord.message : null;
629
663
  }
630
664
  /**
631
665
  * @description Extract the readable, SAFE-TO-SURFACE error form from OpenCode's
@@ -638,19 +672,16 @@ function redactOpenCodeErrorMessage(message) {
638
672
  * The retry classifier reads {@link getOpenCodeErrorClassificationText} instead.
639
673
  */
640
674
  function getOpenCodeErrorMessage(error) {
641
- if (typeof error === 'string')
642
- return redactOpenCodeErrorMessage(error);
643
- if (!error || typeof error !== 'object')
644
- return genericOpenCodeErrorMessage;
645
- const errorRecord = error;
646
- if (errorRecord.data && typeof errorRecord.data === 'object') {
647
- const errorData = errorRecord.data;
648
- if (typeof errorData.message === 'string')
649
- return redactOpenCodeErrorMessage(errorData.message);
650
- }
651
- if (typeof errorRecord.message === 'string')
652
- return redactOpenCodeErrorMessage(errorRecord.message);
653
- return genericOpenCodeErrorMessage;
675
+ const errorString = readOpenCodeErrorString(error);
676
+ return errorString === null ? genericOpenCodeErrorMessage : redactOpenCodeErrorMessage(errorString);
677
+ }
678
+ /**
679
+ * @description The bot-log form of an error: the whole message string, plugin diagnostics included, credentials
680
+ * redacted — what {@link getOpenCodeErrorMessage} cuts for the topic stays findable in the log.
681
+ */
682
+ function getOpenCodeErrorLogText(error) {
683
+ const errorString = readOpenCodeErrorString(error);
684
+ return errorString === null ? genericOpenCodeErrorMessage : redactOpenCodeErrorCredentials(errorString);
654
685
  }
655
686
  /**
656
687
  * @description Serialise an error payload FOR CLASSIFICATION ONLY. The result
@@ -664,7 +695,10 @@ function getOpenCodeErrorMessage(error) {
664
695
  * shape to a constant, and the classifier reads text to decide whether an error
665
696
  * is an auto-retryable rate-limit / usage-limit. Feeding it that constant
666
697
  * silently cancelled the retry of a real rate limit, so the two paths are split:
667
- * the user gets the constant, the classifier gets the full payload.
698
+ * the user gets the constant, the classifier gets the payload's text. For
699
+ * OpenCode's own `{ name, data }` provider error that text is the message, the
700
+ * response body and the status code only — never its headers or metadata (see
701
+ * the inline note); an unrecognised shape is serialized whole.
668
702
  */
669
703
  function getOpenCodeErrorClassificationText(error) {
670
704
  if (typeof error === 'string')
@@ -673,6 +707,17 @@ function getOpenCodeErrorClassificationText(error) {
673
707
  return error.message;
674
708
  if (!error || typeof error !== 'object')
675
709
  return '';
710
+ const apiErrorData = getOpenCodeApiErrorData(error);
711
+ if (apiErrorData) {
712
+ // OpenCode's own shape: what the PROVIDER said — its message, its response body, the status code. Never the
713
+ // response headers or the metadata: they name limits and resets of their own (`x-…-limit-…`, a per-minute
714
+ // counter's `…-reset: <ISO>`), which read as a usage window that is not there.
715
+ return [
716
+ apiErrorData.message,
717
+ apiErrorData.responseBody,
718
+ typeof apiErrorData.statusCode === 'number' ? `HTTP ${apiErrorData.statusCode}` : undefined,
719
+ ].filter((part) => typeof part === 'string' && part !== '').join('\n');
720
+ }
676
721
  try {
677
722
  // A payload whose `toJSON` opts out yields undefined rather than text.
678
723
  const serialized = JSON.stringify(error);
@@ -683,6 +728,39 @@ function getOpenCodeErrorClassificationText(error) {
683
728
  return '';
684
729
  }
685
730
  }
731
+ const msPerSecond = 1000;
732
+ /** @description The `data` of an OpenCode `{ name, data: { message, … } }` error, or `null` for any other shape. */
733
+ function getOpenCodeApiErrorData(error) {
734
+ const data = 'data' in error && error.data && typeof error.data === 'object' ? error.data : undefined;
735
+ return data && typeof data.message === 'string' ? data : null;
736
+ }
737
+ /**
738
+ * @description The reset a provider states as a FIELD: the `retry-after-ms` / `retry-after` response header of an
739
+ * OpenCode provider error (seconds, or an HTTP date), as epoch ms; `undefined` when there is none. The one header the
740
+ * bot reads — the others name limits and counters of their own.
741
+ */
742
+ function getOpenCodeErrorRetryAfterAt(error, now) {
743
+ if (!error || typeof error !== 'object')
744
+ return undefined;
745
+ const headers = getOpenCodeApiErrorData(error)?.responseHeaders;
746
+ if (!headers || typeof headers !== 'object')
747
+ return undefined;
748
+ const readHeader = (name) => {
749
+ const entry = Object.entries(headers).find(([headerName]) => headerName.toLowerCase() === name);
750
+ return typeof entry?.[1] === 'string' ? entry[1] : undefined;
751
+ };
752
+ const retryAfterMs = Number.parseFloat(readHeader('retry-after-ms') ?? '');
753
+ if (Number.isFinite(retryAfterMs))
754
+ return now + retryAfterMs;
755
+ const retryAfter = readHeader('retry-after');
756
+ if (retryAfter === undefined)
757
+ return undefined;
758
+ const retryAfterSeconds = Number.parseFloat(retryAfter);
759
+ if (Number.isFinite(retryAfterSeconds))
760
+ return now + retryAfterSeconds * msPerSecond;
761
+ const retryAfterDateMs = Date.parse(retryAfter);
762
+ return Number.isNaN(retryAfterDateMs) ? undefined : retryAfterDateMs;
763
+ }
686
764
  /**
687
765
  * @description Sync "is this session occupied with a turn?" decision for the
688
766
  * scheduler's wait-for-idle loop ({@link AgentAdapter.checkIsBusy}): busy when
@@ -915,6 +993,39 @@ exports.schedulerMcpRetryDelaysMs = [15_000, 60_000, 5 * 60_000, 15 * 60_000];
915
993
  function getSchedulerMcpRetryDelayMs(attempt) {
916
994
  return exports.schedulerMcpRetryDelaysMs[attempt] ?? null;
917
995
  }
996
+ /**
997
+ * Pauses before each retry of a failed attempt to bring the OpenCode server up
998
+ * (a restart, or a session start / resume that finds it down). An attempt can
999
+ * fail transiently — the binary missing (`spawn opencode ENOENT`), a server not
1000
+ * ready in time on a loaded machine; one attempt used to tear every active
1001
+ * session down and show the raw error.
1002
+ */
1003
+ exports.openCodeServerRestartRetryDelaysMs = [2_000, 5_000, 10_000, 15_000, 20_000];
1004
+ /**
1005
+ * The bound on the retries, counted from the first attempt: no retry is paused
1006
+ * for once it would start past this. An attempt that waits out the server
1007
+ * readiness probe takes ~15 s, so the bound — not the pause list — is what keeps
1008
+ * a hopeless start from holding the topics for minutes.
1009
+ */
1010
+ exports.openCodeServerRestartRetryBudgetMs = 60_000;
1011
+ /**
1012
+ * @description The pause before retry number `retryIndex` (0-based), or
1013
+ * `null` once the pauses are used up or the retry would start past `budgetMs`
1014
+ * after the first attempt began (`elapsedMs` ago).
1015
+ */
1016
+ function getOpenCodeServerRestartRetryDelayMs(retryIndex, elapsedMs, delaysMs = exports.openCodeServerRestartRetryDelaysMs, budgetMs = exports.openCodeServerRestartRetryBudgetMs) {
1017
+ const delayMs = delaysMs[retryIndex];
1018
+ if (delayMs === undefined || elapsedMs + delayMs > budgetMs)
1019
+ return null;
1020
+ return delayMs;
1021
+ }
1022
+ const defaultOpenCodeServerRestartOps = {
1023
+ checkIsInstalled: () => (0, installManager_1.checkIsInstalled)('opencode'),
1024
+ installTool: () => (0, installManager_1.installTool)('opencode'),
1025
+ checkIsServerRunning: installManager_1.checkIsOpenCodeServerRunning,
1026
+ ensureServer: installManager_1.ensureOpenCodeServer,
1027
+ restartServer: installManager_1.restartOpenCodeServer,
1028
+ };
918
1029
  function buildProviderApiAuthPayload(apiKey) {
919
1030
  return { type: 'api', key: apiKey };
920
1031
  }
@@ -930,6 +1041,17 @@ function resetOpenCodeProviderCaches() {
930
1041
  cachedProviders = null;
931
1042
  providersCacheTime = 0;
932
1043
  }
1044
+ /**
1045
+ * @description The variants the LAST providers snapshot lists for `model`, however old: a model's variants hardly
1046
+ * change, and the synchronous effort display must not wait for a request. `[]` is a model the snapshot lists with
1047
+ * no variants (KNOWN to have none); `null` is a model the display cannot judge — no model, no snapshot yet, or a
1048
+ * model the snapshot does not list.
1049
+ */
1050
+ function getKnownModelVariants(model) {
1051
+ if (!model)
1052
+ return null;
1053
+ return cachedProviders?.providers.get(model.providerID)?.get(model.modelID)?.variants ?? null;
1054
+ }
933
1055
  function getFreshProvidersConfig(now = Date.now()) {
934
1056
  if (!cachedProviders || now - providersCacheTime >= providersCacheTtlMs)
935
1057
  return null;
@@ -1129,8 +1251,20 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1129
1251
  runtimeContextHydrationCount = 0;
1130
1252
  /** Callers waiting for one of the bounded runtime-context hydration slots. */
1131
1253
  runtimeContextHydrationWaiters = [];
1132
- /** Whether a server restart is already in progress (prevents concurrent restart attempts) */
1133
- isServerRestarting = false;
1254
+ /**
1255
+ * The server restart in progress, `null` when none: a second caller shares it
1256
+ * instead of starting another, and a prompt sent meanwhile waits for it
1257
+ * ({@link sendPromptAsync}) rather than posting to a server that is down.
1258
+ */
1259
+ serverRestart = null;
1260
+ /**
1261
+ * The installManager calls of an attempt to bring the server up and its retry
1262
+ * schedule. Instance fields (initialised from the module defaults) only so the
1263
+ * tests can fail attempts and shrink the pauses — production never changes them.
1264
+ */
1265
+ serverRestartOps = defaultOpenCodeServerRestartOps;
1266
+ serverRestartRetryDelaysMs = exports.openCodeServerRestartRetryDelaysMs;
1267
+ serverRestartRetryBudgetMs = exports.openCodeServerRestartRetryBudgetMs;
1134
1268
  /**
1135
1269
  * Per-thread display-prefs reader, injected by the bot at boot via
1136
1270
  * `createAdapter.registerDisplayPrefsReader` (S4). DELIBERATE deviation from
@@ -1215,45 +1349,44 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1215
1349
  return true;
1216
1350
  }
1217
1351
  /**
1218
- * @description Handle unexpected server process death.
1219
- * Notifies all active users and attempts to restart the server.
1352
+ * @description Handle unexpected server process death: restart it. The topics
1353
+ * hear nothing unless it matters to them — a turn that was in flight, a session
1354
+ * the new server no longer has, or a restart that failed ({@link restartServer}).
1220
1355
  * SSE reconnect loop will detect the restart and recover automatically.
1221
1356
  */
1222
1357
  handleServerCrash(code, signal) {
1223
1358
  const reason = code === 137 ? 'out of memory (OOM killed)' :
1224
1359
  signal ? `signal ${signal}` : `exit code ${code}`;
1225
- // Notify all active session threads about the crash
1226
- for (const session of this.sessions.values()) {
1227
- if (session.isActive) {
1228
- this.emit('output', session.key, `OpenCode server crashed (${reason}). Restarting...`);
1229
- }
1230
- }
1231
- // Auto-restart the server (SSE reconnect loop will pick it up)
1232
- this.restartServer();
1360
+ console.error(`[OpenCode] Server stopped (${reason}), restarting it`);
1361
+ void this.restartServer();
1233
1362
  }
1234
1363
  /**
1235
- * @description Restart the OpenCode server process.
1236
- * Prevents concurrent restart attempts. If restart is already in progress,
1237
- * waits for it to complete and returns its result.
1364
+ * @description Restart the OpenCode server process. A restart already in
1365
+ * progress is shared: its result is the caller's.
1238
1366
  */
1239
- async restartServer(force = false) {
1240
- if (this.isServerRestarting) {
1241
- console.log(`[OpenCode] Server restart already in progress, waiting...`);
1242
- // Wait for the in-progress restart to complete
1243
- while (this.isServerRestarting) {
1244
- await new Promise(resolve => setTimeout(resolve, 500));
1245
- }
1246
- // Check if server is now running
1247
- return (0, installManager_1.checkIsOpenCodeServerRunning)();
1248
- }
1249
- this.isServerRestarting = true;
1367
+ restartServer(force = false) {
1368
+ if (this.serverRestart) {
1369
+ console.log(`[OpenCode] Server restart already in progress, waiting for it`);
1370
+ return this.serverRestart;
1371
+ }
1372
+ const restart = this.runServerRestart(force).finally(() => {
1373
+ if (this.serverRestart === restart)
1374
+ this.serverRestart = null;
1375
+ });
1376
+ this.serverRestart = restart;
1377
+ return restart;
1378
+ }
1379
+ /**
1380
+ * @description The body of {@link restartServer}. Bringing the server back is
1381
+ * retried ({@link ensureServerWithRetries}) and silent: a topic whose session
1382
+ * is restored hears about it only when its turn was in flight — that reply is
1383
+ * lost and has to be asked again.
1384
+ */
1385
+ async runServerRestart(force) {
1386
+ // Read before the restore loop: resuming rebuilds every session object.
1387
+ const turnInFlightKeys = new Set(Array.from(this.sessions.entries()).filter(([, session]) => session.isActive && session.isBusy).map(([k]) => k));
1250
1388
  try {
1251
- console.log(`[OpenCode] Attempting server restart...`);
1252
- if (force)
1253
- await (0, installManager_1.restartOpenCodeServer)();
1254
- else
1255
- await (0, installManager_1.ensureOpenCodeServer)();
1256
- console.log(`[OpenCode] Server restarted successfully`);
1389
+ await this.ensureServerWithRetries(force ? 'replace' : 'adopt');
1257
1390
  // Close the global stream up front: its reader points at the crashed
1258
1391
  // server's connection. The resume loop below re-opens the (idempotent)
1259
1392
  // single stream via connectSse (plan 2026-06-17 S4). Without this the
@@ -1281,7 +1414,9 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1281
1414
  // lifecycle lock here exactly like the public resumeSession does, so
1282
1415
  // the lock is taken once (no double-acquire from the crash handler).
1283
1416
  await this.withLifecycleLock(k, () => this.resumeSessionInner(sessionKey, workDir, sessionId));
1284
- this.emit('output', sessionKey, `OpenCode server restarted; session restored. In-flight reply was lost — resend if needed.`);
1417
+ if (turnInFlightKeys.has(k)) {
1418
+ this.emit('output', sessionKey, this.tl(sessionKey, () => (0, i18n_1.t)('openCodeServer.restartedSessionRestoredNotice')));
1419
+ }
1285
1420
  }
1286
1421
  catch (e) {
1287
1422
  // The id is gone on the restarted server (or resume otherwise
@@ -1290,7 +1425,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1290
1425
  // wipes the persisted session id too — otherwise the next bot
1291
1426
  // restart would try to resume an id the server no longer has.
1292
1427
  console.error(`[OpenCode] Could not restore session ${sessionId} after restart:`, e);
1293
- this.emit('output', sessionKey, `OpenCode server restarted; previous session lost. Starting a fresh one with /opencode (or /quit to release).`);
1428
+ this.emit('output', sessionKey, this.tl(sessionKey, () => (0, i18n_1.t)('openCodeServer.restartedSessionLostNotice')));
1294
1429
  this.stopSessionInner(sessionKey);
1295
1430
  this.emit('closed', sessionKey);
1296
1431
  // Defensive: stopSessionInner deletes from `this.sessions`, but
@@ -1301,13 +1436,16 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1301
1436
  return true;
1302
1437
  }
1303
1438
  catch (e) {
1304
- console.error(`[OpenCode] Server restart failed:`, e);
1439
+ // The technical detail stays in the log; the topic gets one short
1440
+ // localized line — a raw exception (`spawn opencode ENOENT`) tells the
1441
+ // person in the topic nothing they can act on.
1442
+ console.error(`[OpenCode] Server restart failed, giving up:`, e);
1305
1443
  // Notify threads about the failure and tear them down
1306
1444
  // Take a snapshot first so we can safely mutate `this.sessions` inside the loop.
1307
1445
  const snapshot = Array.from(this.sessions.entries());
1308
1446
  for (const [k, session] of snapshot) {
1309
1447
  if (session.isActive) {
1310
- this.emit('output', session.key, `Failed to restart OpenCode server: ${e instanceof Error ? e.message : String(e)}`);
1448
+ this.emit('output', session.key, this.tl(session.key, () => (0, i18n_1.t)('agent.serverUnavailableNotice', { label: this.label })));
1311
1449
  // Mark session as inactive — no point keeping it alive
1312
1450
  session.isActive = false;
1313
1451
  // This teardown bypasses stopSessionInner, so the replacement bound
@@ -1319,10 +1457,61 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1319
1457
  }
1320
1458
  return false;
1321
1459
  }
1322
- finally {
1323
- this.isServerRestarting = false;
1460
+ }
1461
+ /**
1462
+ * @description The ONE way the adapter brings the server up — a restart, a
1463
+ * session start or resume, the provider sign-in: each attempt first makes sure
1464
+ * the binary is installed, then starts (or adopts) the server per `mode`; a
1465
+ * failed attempt is retried on the {@link openCodeServerRestartRetryDelaysMs}
1466
+ * pauses within {@link openCodeServerRestartRetryBudgetMs}. `onStep` tells a
1467
+ * waiting person what is happening (a start or resume; a restart passes none
1468
+ * and stays silent). Throws {@link AgentBackendUnavailableError} once the
1469
+ * retries are used up — the attempts' errors stay in the log.
1470
+ */
1471
+ async ensureServerWithRetries(mode, onStep) {
1472
+ const firstAttemptStartedAt = Date.now();
1473
+ for (let retryIndex = 0;; retryIndex++) {
1474
+ try {
1475
+ if (mode === 'ifDown' && await this.serverRestartOps.checkIsServerRunning())
1476
+ return;
1477
+ console.log(`[OpenCode] Bringing the server up (${mode}, attempt ${retryIndex + 1})...`);
1478
+ if (!this.serverRestartOps.checkIsInstalled()) {
1479
+ console.log(`[OpenCode] opencode binary not found, installing it first...`);
1480
+ if (retryIndex === 0)
1481
+ onStep?.('installing');
1482
+ await this.serverRestartOps.installTool();
1483
+ }
1484
+ if (retryIndex === 0)
1485
+ onStep?.('starting');
1486
+ if (mode === 'replace')
1487
+ await this.serverRestartOps.restartServer();
1488
+ else
1489
+ await this.serverRestartOps.ensureServer();
1490
+ console.log(`[OpenCode] Server is up`);
1491
+ return;
1492
+ }
1493
+ catch (e) {
1494
+ const delayMs = getOpenCodeServerRestartRetryDelayMs(retryIndex, Date.now() - firstAttemptStartedAt, this.serverRestartRetryDelaysMs, this.serverRestartRetryBudgetMs);
1495
+ if (delayMs === null) {
1496
+ console.error(`[OpenCode] The server could not be brought up after ${retryIndex + 1} attempts:`, e);
1497
+ throw new backendUnavailableError_1.AgentBackendUnavailableError(this.label, e);
1498
+ }
1499
+ console.warn(`[OpenCode] Attempt ${retryIndex + 1} to bring the server up failed, retrying in ${Math.round(delayMs / 1000)}s:`, e instanceof Error ? e.message : e);
1500
+ await (0, utils_1.sleep)(delayMs);
1501
+ }
1324
1502
  }
1325
1503
  }
1504
+ /**
1505
+ * @description {@link ensureServerWithRetries} for a session start or resume of
1506
+ * `key`: the server is brought up only when it is down, and the topic is told
1507
+ * (in its language) that OpenCode is being installed or started.
1508
+ */
1509
+ ensureServerForSession(key) {
1510
+ return this.ensureServerWithRetries('ifDown', (step) => {
1511
+ const noticeKey = step === 'installing' ? 'openCodeServer.installingNotice' : 'openCodeServer.startingNotice';
1512
+ this.emit('output', key, this.tl(key, () => (0, i18n_1.t)(noticeKey)));
1513
+ });
1514
+ }
1326
1515
  /**
1327
1516
  * @description Reload credentials written by the out-of-band `opencode auth
1328
1517
  * login` CLI. The CLI updates auth.json directly, but a running OpenCode
@@ -1432,16 +1621,10 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1432
1621
  // mapping `SessionKey → opencodeSessionId` in state.json (plan §13.19) so
1433
1622
  // resumes after a restart go through `resumeSession()`, not startSession.
1434
1623
  const k = (0, sessionKey_1.keyToString)(key);
1624
+ await this.waitForServerRestart();
1435
1625
  return this.withLifecycleLock(k, async () => {
1436
1626
  this.stopSessionInner(key);
1437
- if (!(0, installManager_1.checkIsInstalled)('opencode')) {
1438
- this.emit('output', key, 'Installing OpenCode...');
1439
- await (0, installManager_1.installTool)('opencode');
1440
- }
1441
- if (!await (0, installManager_1.checkIsOpenCodeServerRunning)()) {
1442
- this.emit('output', key, 'Starting OpenCode server...');
1443
- await (0, installManager_1.ensureOpenCodeServer)();
1444
- }
1627
+ await this.ensureServerForSession(key);
1445
1628
  console.log(`[OpenCode] Starting session for ${k} in ${workDir}`);
1446
1629
  try {
1447
1630
  // R1 (verified live 2026-06-04): a session created WITHOUT a title is
@@ -1490,6 +1673,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1490
1673
  isBusy: false,
1491
1674
  awaitingTurnResponse: false,
1492
1675
  sawTurnActivity: false,
1676
+ handledTurnErrorTexts: new Set(),
1493
1677
  unconsumedInputCount: 0,
1494
1678
  seenUserMessageIds: new Set(),
1495
1679
  providerRetrySignature: null,
@@ -1646,6 +1830,14 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1646
1830
  * model selector. No separate request, no env configuration.
1647
1831
  */
1648
1832
  sendPromptAsync(key, input) {
1833
+ if (this.serverRestart) {
1834
+ // Posted now, the prompt would hit the server that is down and be lost with
1835
+ // a raw `fetch failed`; it goes out once the restart has restored the session.
1836
+ // A restart that failed has stopped the session: the topic was told to send again.
1837
+ console.log(`[OpenCode] holding a prompt for ${(0, sessionKey_1.keyToString)(key)} until the server restart ends`);
1838
+ void this.serverRestart.then(() => this.sendPromptAsync(key, input));
1839
+ return;
1840
+ }
1649
1841
  const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
1650
1842
  if (!session?.isActive) {
1651
1843
  console.log(`[OpenCode] sendInput: no active session for ${(0, sessionKey_1.keyToString)(key)}`);
@@ -1671,6 +1863,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1671
1863
  // New turn → previous turn's tool-result dedup ids can no longer matter
1672
1864
  // (see the field's JSDoc for why the turn flush is too early to clear).
1673
1865
  session.emittedToolResultPartIds.clear();
1866
+ session.handledTurnErrorTexts.clear();
1674
1867
  // Mark busy optimistically so an immediately-following message correctly
1675
1868
  // sees a turn in flight; the `session.status` stream corrects/confirms it.
1676
1869
  session.isBusy = true;
@@ -1960,13 +2153,8 @@ class OpenCodeAdapter extends events_1.EventEmitter {
1960
2153
  return (0, i18n_1.t)('disconnect.failed', { provider: normalizedProviderId, reason });
1961
2154
  }
1962
2155
  }
1963
- async ensureProviderAuthServerReady() {
1964
- if (!(0, installManager_1.checkIsInstalled)('opencode')) {
1965
- await (0, installManager_1.installTool)('opencode');
1966
- }
1967
- if (!await (0, installManager_1.checkIsOpenCodeServerRunning)()) {
1968
- await (0, installManager_1.ensureOpenCodeServer)();
1969
- }
2156
+ ensureProviderAuthServerReady() {
2157
+ return this.ensureServerWithRetries('ifDown');
1970
2158
  }
1971
2159
  /**
1972
2160
  * @description Fetch a provider's auth methods for the `/connect` picker.
@@ -2026,7 +2214,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2026
2214
  }
2027
2215
  }
2028
2216
  else {
2029
- session.effortLevel = (0, effortLevels_1.clampEffortToAvailable)(effortLevels_1.defaultEffortLevel, await this.getModelVariants(resolved));
2217
+ session.effortLevel = (0, effortLevels_1.clampEffortToAvailable)((0, effortLevels_1.getInstanceDefaultEffort)(), await this.getModelVariants(resolved));
2030
2218
  }
2031
2219
  return null;
2032
2220
  }
@@ -2154,7 +2342,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2154
2342
  * started/resumed session that has NO explicit per-thread `/effort` pref.
2155
2343
  *
2156
2344
  * Explicit prefs always win and are left untouched (early return). With no
2157
- * pref, the default (`defaultEffortLevel`) is clamped to the resolved
2345
+ * pref, the instance default (`getInstanceDefaultEffort`) is clamped to the resolved
2158
2346
  * model's variants — OpenCode effort is a model variant and not every model
2159
2347
  * ships `xhigh`, so an unclamped value would POST an invalid `body.variant`.
2160
2348
  * A model with no variants resolves to `null` (no effort), which is correct.
@@ -2169,7 +2357,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2169
2357
  if (loadSavedEffort(key) !== null)
2170
2358
  return; // explicit pref wins
2171
2359
  const variants = await this.getModelVariants(this.getSessionModelRef(session));
2172
- session.effortLevel = (0, effortLevels_1.clampEffortToAvailable)(effortLevels_1.defaultEffortLevel, variants);
2360
+ session.effortLevel = (0, effortLevels_1.clampEffortToAvailable)((0, effortLevels_1.getInstanceDefaultEffort)(), variants);
2173
2361
  }
2174
2362
  /**
2175
2363
  * @description Resolve the model the thread's effort applies to, WITHOUT
@@ -2234,19 +2422,57 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2234
2422
  }
2235
2423
  return this.getModelVariants(await this.getProspectiveModelRef(key));
2236
2424
  }
2425
+ async getProspectiveModelLabel(key) {
2426
+ const ref = await this.getProspectiveModelRef(key);
2427
+ return ref ? `${ref.providerID}/${ref.modelID}` : null;
2428
+ }
2237
2429
  getEffort(key) {
2238
2430
  const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2239
2431
  if (session)
2240
2432
  return session.effortLevel;
2241
2433
  return loadSavedEffort(key);
2242
2434
  }
2435
+ /** Only the persisted pick: a live session's `effortLevel` may be the clamped default (see {@link getEffort}). */
2436
+ getOwnEffort(key) {
2437
+ return loadSavedEffort(key);
2438
+ }
2439
+ /**
2440
+ * The live session sends its `effortLevel` with every prompt (a model variant, or none for `auto` / a model
2441
+ * without variants). The next session seeds the pick, else the default rounded to the variants of the model it
2442
+ * will run (the live one, else the topic's saved `/model`) — read from the last providers snapshot, so this stays
2443
+ * synchronous (the banner needs it); unknown variants (the server's default model, no snapshot yet) leave the
2444
+ * default as it is. A model KNOWN to have no variants runs without any level, so there is no state to show
2445
+ * (`null`, as `/effort` answers «not available» for it).
2446
+ */
2447
+ getEffortState(key) {
2448
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2449
+ const liveSession = session?.isActive ? session : null;
2450
+ const pick = loadSavedEffort(key);
2451
+ const defaultLevel = (0, effortLevels_1.getInstanceDefaultEffort)();
2452
+ const variants = getKnownModelVariants(liveSession ? this.getSessionModelRef(liveSession) : loadSavedModel(key));
2453
+ if (variants !== null && variants.length === 0)
2454
+ return null;
2455
+ return {
2456
+ running: liveSession ? liveSession.effortLevel : null,
2457
+ nextStart: pick ?? (0, effortLevels_1.clampEffortToAvailable)(defaultLevel, variants ?? []) ?? defaultLevel,
2458
+ };
2459
+ }
2460
+ /**
2461
+ * @description Forget the thread's own pick; a live session takes the clamped instance default for its next
2462
+ * prompt ({@link applyDefaultEffortIfUnset}), a later one seeds it at creation.
2463
+ */
2464
+ async clearEffort(key) {
2465
+ clearEffortPref(key);
2466
+ await this.applyDefaultEffortIfUnset(key);
2467
+ }
2243
2468
  /**
2244
2469
  * @description Set the per-thread effort (variant). Validates against the
2245
2470
  * thread's available variants — live model when a session is running, the
2246
2471
  * PROSPECTIVE model (saved `/model` pref → server default) when not — then
2247
2472
  * persists on success (applied per-prompt by `sendPromptAsync`, plan D3; a
2248
- * later session seeds `effortLevel` from this pref at creation). Returns a
2249
- * user-facing notice string on any non-success, `null` on success.
2473
+ * later session seeds `effortLevel` from this pref at creation). `refused` with a
2474
+ * user-facing reason on any non-success, `applied` on success — also with no
2475
+ * session: there is nothing left to do, a later session seeds the pick.
2250
2476
  *
2251
2477
  * Mirrors `setModel`'s persist-first symmetry (no `'No active session'`
2252
2478
  * hard-fail): a level picked BEFORE `/opencode` is saved and replayed by the
@@ -2261,10 +2487,10 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2261
2487
  // session is up, else the prospective model — so the notice is correct
2262
2488
  // pre-session (the live `currentModelLabel` is null then).
2263
2489
  const modelLabel = await this.getEffortModelLabel(key);
2264
- return (0, i18n_1.t)('effort.not_supported', { model: modelLabel });
2490
+ return { status: 'refused', reason: (0, i18n_1.t)('effort.not_supported', { model: modelLabel }) };
2265
2491
  }
2266
2492
  if (!available.includes(level)) {
2267
- return (0, i18n_1.t)('effort.invalid_level', { level, valid: available.join(', ') });
2493
+ return { status: 'refused', reason: (0, i18n_1.t)('effort.invalid_level', { level, valid: available.join(', ') }) };
2268
2494
  }
2269
2495
  // Persist unconditionally (like `setModel`'s `saveModelPref`): a session
2270
2496
  // started later seeds `effortLevel: loadSavedEffort(key)` at creation.
@@ -2275,7 +2501,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2275
2501
  if (session?.isActive)
2276
2502
  session.effortLevel = level;
2277
2503
  console.log(`[OpenCode] Effort set to: ${level}`);
2278
- return null;
2504
+ return { status: 'applied' };
2279
2505
  }
2280
2506
  /**
2281
2507
  * @description Human label for the model the thread's effort validates
@@ -2494,8 +2720,20 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2494
2720
  // resumes to the wrong folder as soon as the bot started managing
2495
2721
  // multiple bindings (plan §10.3, fix to old openCodeAdapter.ts:599).
2496
2722
  const k = (0, sessionKey_1.keyToString)(key);
2723
+ await this.waitForServerRestart();
2497
2724
  return this.withLifecycleLock(k, () => this.resumeSessionInner(key, workDir, sessionId, options));
2498
2725
  }
2726
+ /**
2727
+ * @description Let a server restart in progress finish before a session start
2728
+ * or resume brings the server up on its own (it would race the restart's
2729
+ * attempts). Its outcome does not matter here: a failed restart leaves the
2730
+ * server down, and the caller's own attempts follow. Never called from inside
2731
+ * the restart, whose restore loop resumes through the inner body.
2732
+ */
2733
+ async waitForServerRestart() {
2734
+ if (this.serverRestart)
2735
+ await this.serverRestart;
2736
+ }
2499
2737
  /**
2500
2738
  * @description Fork the thread's CURRENT session into a new one that carries
2501
2739
  * the full conversation, and attach to it. Used by the bot's wedged-turn
@@ -2554,14 +2792,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2554
2792
  async resumeSessionInner(key, workDir, sessionId, options) {
2555
2793
  this.stopSessionInner(key);
2556
2794
  const k = (0, sessionKey_1.keyToString)(key);
2557
- if (!(0, installManager_1.checkIsInstalled)('opencode')) {
2558
- this.emit('output', key, 'Installing OpenCode...');
2559
- await (0, installManager_1.installTool)('opencode');
2560
- }
2561
- if (!await (0, installManager_1.checkIsOpenCodeServerRunning)()) {
2562
- this.emit('output', key, 'Starting OpenCode server...');
2563
- await (0, installManager_1.ensureOpenCodeServer)();
2564
- }
2795
+ await this.ensureServerForSession(key);
2565
2796
  console.log(`[OpenCode] Resuming session ${sessionId} for ${k} in ${workDir}`);
2566
2797
  try {
2567
2798
  // Verify session exists
@@ -2597,6 +2828,7 @@ class OpenCodeAdapter extends events_1.EventEmitter {
2597
2828
  isBusy: false,
2598
2829
  awaitingTurnResponse: false,
2599
2830
  sawTurnActivity: false,
2831
+ handledTurnErrorTexts: new Set(),
2600
2832
  unconsumedInputCount: 0,
2601
2833
  seenUserMessageIds: new Set(),
2602
2834
  providerRetrySignature: null,
@@ -4134,12 +4366,60 @@ class OpenCodeAdapter extends events_1.EventEmitter {
4134
4366
  if (checkIsOpenCodeAbortError(errorMsg)) {
4135
4367
  console.log(`[OpenCode] Swallowed bot-issued abort (message.error "Aborted") for ${(0, sessionKey_1.keyToString)(key)}`);
4136
4368
  }
4369
+ else if (info.sessionID && info.sessionID !== session.sessionId) {
4370
+ // A sub-agent's failed step reaches the parent as a failed tool call; it is not the topic's failure.
4371
+ console.log(`[OpenCode] ${(0, sessionKey_1.keyToString)(key)}: message error of session ${info.sessionID}, not the topic's own:`, getOpenCodeErrorLogText(info.error));
4372
+ }
4137
4373
  else {
4138
- console.error(`[OpenCode] Message error:`, errorMsg);
4139
- this.emit('output', key, `Error: ${errorMsg}`);
4374
+ this.handleOwnTurnFailure(key, info.error, 'message.error');
4140
4375
  }
4141
4376
  }
4142
4377
  }
4378
+ /**
4379
+ * @description One failure of the topic's OWN turn, from either channel OpenCode reports it on: `session.error` and the
4380
+ * failed assistant message (`message.updated` with `info.error`). OpenCode publishes most failures on both, in either
4381
+ * order, and may update the message more than once — so a failure is handled ONCE per turn
4382
+ * ({@link OpenCodeSession.handledTurnErrorTexts}); one that comes only on the message (a compaction, a structured-output
4383
+ * step) is handled all the same. Handled = posted (`OpenCode error: …` / `Error: …` by the channel that came first)
4384
+ * and, when the classifier knows it, handed to the
4385
+ * bot's auto-retry (`apiError`). A conversation-tail refusal is not posted: the retry's own notice is its one line.
4386
+ *
4387
+ * Classified from the RAW payload, not from the surfaced text: that collapses an unrecognised shape to a constant
4388
+ * that carries no marker to classify. The raw text stops here — the classifier returns only a class.
4389
+ */
4390
+ handleOwnTurnFailure(key, error, channel) {
4391
+ const errorMsg = getOpenCodeErrorMessage(error);
4392
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
4393
+ if (session?.handledTurnErrorTexts.has(errorMsg)) {
4394
+ console.log(`[OpenCode] ${(0, sessionKey_1.keyToString)(key)}: the turn's failure came again on ${channel} — already handled, not posted again`);
4395
+ return;
4396
+ }
4397
+ session?.handledTurnErrorTexts.add(errorMsg);
4398
+ console.error(`[OpenCode] ${(0, sessionKey_1.keyToString)(key)}: turn failure (${channel}):`, getOpenCodeErrorLogText(error));
4399
+ // A PARENT-session error aborts the current turn/generation, so the
4400
+ // delegation the dedicated sub-agent status describes is no longer running:
4401
+ // close it defensively (mirrors the parent-idle / parent-finish closes) so
4402
+ // an error that never reaches a clean idle can't leave the "working" frame
4403
+ // and its self-re-arming tick wedged, flooding the topic (live 2026-08-03).
4404
+ // If an auto-retry re-delegates, a fresh `subagentStatus{active:true}` re-opens it.
4405
+ if (session?.isActive)
4406
+ this.closeSubagentStatusOnParentTurnEnd(key);
4407
+ // Provider-side API error at the proxy boundary → emit `apiError`. transient / usageLimit / conversationTail arm
4408
+ // the auto-retry (the session stays active after the error, so the kick reuses it); `auth` (logged out / bad
4409
+ // credentials) is SURFACED as a pinned notice by the bot, never retried. Unrecognised errors classify to `null`.
4410
+ const now = Date.now();
4411
+ const apiError = (0, apiErrorRetry_1.classifyAgentApiError)(getOpenCodeErrorClassificationText(error), now);
4412
+ if (apiError?.kind !== 'conversationTail')
4413
+ this.emit('output', key, `${openCodeFailurePrefixes[channel]}${errorMsg}`);
4414
+ if (!apiError)
4415
+ return;
4416
+ // A usage limit whose text names no reset takes the provider's Retry-After, read as a field — unless it is a
4417
+ // short retry hint (a daily quota comes with Google's 40-second one), which is not the window's reset.
4418
+ const retryAfterAt = apiError.kind === 'usageLimit' && apiError.resetAt === undefined ? getOpenCodeErrorRetryAfterAt(error, now) : undefined;
4419
+ const limitRetryAfterAt = retryAfterAt !== undefined && !(0, apiErrorRetry_1.checkIsShortStatedReset)(retryAfterAt, now) ? retryAfterAt : undefined;
4420
+ const resetAt = apiError.kind === 'usageLimit' ? apiError.resetAt ?? limitRetryAfterAt : undefined;
4421
+ this.emit('apiError', key, apiError.kind === 'usageLimit' ? { ...apiError, resetAt } : apiError);
4422
+ }
4143
4423
  /**
4144
4424
  * @description Handle session becoming idle (AI done processing).
4145
4425
  * Flush any remaining accumulated output.
@@ -4387,32 +4667,18 @@ class OpenCodeAdapter extends events_1.EventEmitter {
4387
4667
  }
4388
4668
  return;
4389
4669
  }
4390
- console.error(`[OpenCode] Session error:`, errorMsg);
4391
- this.emit('output', key, `OpenCode error: ${errorMsg}`);
4392
- // A PARENT-session error aborts the current turn/generation, so the
4393
- // delegation the dedicated sub-agent status describes is no longer running:
4394
- // close it defensively (mirrors the parent-idle / parent-finish closes) so
4395
- // an error that never reaches a clean idle can't leave the "working" frame
4396
- // and its self-re-arming tick wedged, flooding the topic (live 2026-08-03).
4397
- // A CHILD (sub-agent) error must NOT close it — the parent delegation is
4398
- // still in flight (guarded the same way as `handleSessionIdle`). If an
4399
- // auto-retry re-delegates, a fresh `subagentStatus{active:true}` re-opens it.
4400
- if (session?.isActive && isOwnSession) {
4401
- this.closeSubagentStatusOnParentTurnEnd(key);
4402
- }
4403
- // Provider-side API error at the proxy boundary → emit `apiError`. transient
4404
- // / usageLimit arm the auto-retry (the session stays active after
4405
- // `session.error`, so the kick reuses it); `auth` (logged out / bad
4406
- // credentials) is SURFACED as a pinned notice by the bot, never retried.
4407
- // Unrecognised errors classify to `null` and emit nothing.
4408
- //
4409
- // Classified from the RAW payload, not from `errorMsg`: the surfaced text
4410
- // collapses an unrecognised shape to a constant that carries no marker to
4411
- // classify. The raw text stops here — the classifier returns only a class.
4412
- const apiError = (0, apiErrorRetry_1.classifyAgentApiError)(getOpenCodeErrorClassificationText(properties.error), Date.now());
4413
- if (apiError) {
4414
- this.emit('apiError', key, apiError);
4670
+ // Only the topic's OWN session's error is the topic's: a sub-agent's child session (often on another model or
4671
+ // provider) and a foreign session routed here by the folder report limits that are not this conversation's —
4672
+ // armed, they held a parent that was still working and marked its model limited. A child's real limit reaches the
4673
+ // parent as a failed tool call; if it is the parent's too, the parent's own retry or error says so. Such an error is
4674
+ // still shown, but never closes the parent's sub-agent status (its delegation is still in flight).
4675
+ if (!isOwnSession) {
4676
+ console.error(`[OpenCode] Session error:`, getOpenCodeErrorLogText(properties.error));
4677
+ this.emit('output', key, `OpenCode error: ${errorMsg}`);
4678
+ console.log(`[OpenCode] ${(0, sessionKey_1.keyToString)(key)}: session.error of session ${eventSessionId}, not the topic's own — no retry armed`);
4679
+ return;
4415
4680
  }
4681
+ this.handleOwnTurnFailure(key, properties.error, 'session.error');
4416
4682
  }
4417
4683
  handlePermissionAsked(_key, properties, directory) {
4418
4684
  console.log(`[OpenCode] Permission requested:`, JSON.stringify(properties));