openmausbot 0.1.91 → 0.1.92

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 (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -52,6 +52,10 @@ import { redactSecretsInText } from "../../redact.js";
52
52
  import { recoveryPromptFor } from "../../resume-recovery.js";
53
53
  import { sessionIdlePolicy } from "../session-idle.js";
54
54
  import { classifyError } from "../retry.js";
55
+ const ACCOUNT_ERROR_CODES = new Set([
56
+ "invalid_credentials", "inactive_subscription", "insufficient_funds", "quota_or_region_restriction",
57
+ ]);
58
+ const isAccountError = (code) => code !== undefined && ACCOUNT_ERROR_CODES.has(code);
55
59
  /** ACP vendors put the actionable cause in error.data while keeping the
56
60
  * JSON-RPC message generic. Only surface known text fields, never a response
57
61
  * body/config dump, and redact before bounding the displayed diagnostic. */
@@ -141,6 +145,24 @@ function acpVariantOption(result) {
141
145
  };
142
146
  }
143
147
  const TOOL_LOG_TEXT_LIMIT = 64_000;
148
+ /** Every model id a session config snapshot offers under `configId`,
149
+ * flattening grouped options. Empty when the agent listed none. */
150
+ export function offeredModels(result, configId) {
151
+ const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === configId);
152
+ const ids = [];
153
+ const visit = (entries) => {
154
+ if (!Array.isArray(entries))
155
+ return;
156
+ for (const entry of entries) {
157
+ if (typeof entry?.value === "string")
158
+ ids.push(entry.value);
159
+ else if (Array.isArray(entry?.options))
160
+ visit(entry.options);
161
+ }
162
+ };
163
+ visit(option?.options);
164
+ return ids;
165
+ }
144
166
  async function readAcpImageBlocks(images) {
145
167
  return Promise.all(images.map(async (image) => ({
146
168
  type: "image",
@@ -250,6 +272,11 @@ export function versionAtLeast(installed, floor) {
250
272
  return true;
251
273
  }
252
274
  export function createAcpDriver(support) {
275
+ // Without a per-turn configureSession nothing would ever apply the mode
276
+ // that sessionScopedApproval keeps out of the spawn contract.
277
+ if (support.sessionScopedApproval && !support.configureSession) {
278
+ throw new Error(`${support.displayName}: sessionScopedApproval needs configureSession to apply each turn's mode`);
279
+ }
253
280
  const DRIVER_KIND = support.driverKind;
254
281
  const SOURCE = support.nativeSource;
255
282
  const decodeConfig = decodeAcpConfig(support.defaultCli);
@@ -289,13 +316,18 @@ export function createAcpDriver(support) {
289
316
  return env;
290
317
  };
291
318
  let models = support.models;
319
+ // Models this account itself offered (see sessionModelEnv): the
320
+ // discovered catalog, never the static one, or a live session's list.
321
+ let accountModels = null;
292
322
  const refreshModels = async () => {
293
323
  if (!support.resolveModels)
294
324
  return;
295
325
  try {
296
326
  const resolved = await support.resolveModels(childEnv(), config, instanceId);
297
- if (resolved.options.length)
327
+ if (resolved.options.length) {
298
328
  models = resolved;
329
+ accountModels = new Set(resolved.options.map((option) => option.id));
330
+ }
299
331
  }
300
332
  catch {
301
333
  // Keep the last usable catalog when an optional discovery source is down.
@@ -1026,8 +1058,12 @@ export function createAcpDriver(support) {
1026
1058
  return { turnId };
1027
1059
  }
1028
1060
  const resolvedModel = support.resolveTurnModel?.(turn.model, env);
1029
- support.applyTurnEnv?.(env, { model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true });
1030
- const cliTurn = resolvedModel !== undefined && resolvedModel !== turn.model
1061
+ support.applyTurnEnv?.(env, {
1062
+ model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd,
1063
+ });
1064
+ // Rebound once, before the prompt, when a fallbackModel support swaps
1065
+ // a model this session does not offer for one it does.
1066
+ let cliTurn = resolvedModel !== undefined && resolvedModel !== turn.model
1031
1067
  ? { ...turn, model: resolvedModel }
1032
1068
  : turn;
1033
1069
  const mcpServers = acpMcpServers(turn);
@@ -1067,7 +1103,31 @@ export function createAcpDriver(support) {
1067
1103
  // riding a child spawned under the old env. Hash it so secrets
1068
1104
  // never sit in the key itself.
1069
1105
  const envFingerprint = createHash("sha256").update(JSON.stringify(spawnEnv)).digest("hex").slice(0, 16);
1070
- const contractKey = JSON.stringify([launch.command, launch.args ?? [], spawnArgs, cwd, turnConfig.fullAuto === true, envFingerprint]);
1106
+ // A support that applies the approval mode to the session on every
1107
+ // turn gets the same process whatever the mode: an approval change
1108
+ // is one RPC, not a cold start.
1109
+ const contractKey = JSON.stringify([
1110
+ launch.command, launch.args ?? [], spawnArgs, cwd,
1111
+ support.sessionScopedApproval ? null : turnConfig.fullAuto === true, envFingerprint,
1112
+ support.spawnFingerprint?.(spawnEnv) ?? null,
1113
+ ]);
1114
+ // The new-session default model rides the spawn env but stays out of
1115
+ // the fingerprint above: only session/new reads it, and a pooled
1116
+ // process switches later models over the wire as before.
1117
+ const launchModel = support.sessionModelEnv && cliTurn.model && /^[\w.:/-]{1,200}$/u.test(cliTurn.model)
1118
+ && accountModels?.has(cliTurn.model) ? cliTurn.model : undefined;
1119
+ const launchEnv = support.sessionModelEnv && launchModel
1120
+ ? { ...spawnEnv, [support.sessionModelEnv]: launchModel }
1121
+ : spawnEnv;
1122
+ // whether this turn's process was started by this turn: its model
1123
+ // list is then current, not a pooled process's older one
1124
+ let launchedThisTurn = false;
1125
+ const launchSession = () => {
1126
+ const opened = openSession(threadId, launch, [...(launch.args ?? []), ...spawnArgs], launchEnv, cwd, contractKey);
1127
+ opened.launchModel = launchModel;
1128
+ launchedThisTurn = true;
1129
+ return opened;
1130
+ };
1071
1131
  const sessionKey = JSON.stringify(mcpServers);
1072
1132
  if (turn.sessionReset) {
1073
1133
  closeSession(threadId, "reset");
@@ -1092,7 +1152,7 @@ export function createAcpDriver(support) {
1092
1152
  else
1093
1153
  closeSession(threadId, "contract");
1094
1154
  }
1095
- session = openSession(threadId, launch, [...(launch.args ?? []), ...spawnArgs], spawnEnv, cwd, contractKey);
1155
+ session = launchSession();
1096
1156
  sessions.set(threadId, session);
1097
1157
  }
1098
1158
  // `session` rebinds mid-turn: when the establishment retry below
@@ -1107,6 +1167,16 @@ export function createAcpDriver(support) {
1107
1167
  rejectStartup = reject;
1108
1168
  }) : null;
1109
1169
  const asks = new Map();
1170
+ // What this turn reports and applies. A model fallback (below)
1171
+ // replaces both: a saved variant belongs to the model it replaced.
1172
+ let reportedModel = turn.model;
1173
+ let variant = turn.variant;
1174
+ // set below when this process lists fewer models than the catalog
1175
+ let staleProcess = false;
1176
+ // Set while a sessionScopedApproval support applies this turn's mode.
1177
+ // Still set if that throws: the pooled session may be left on an
1178
+ // earlier turn's looser mode, so its process must not be reused.
1179
+ let approvalUnconfirmed = false;
1110
1180
  const modelOf = (result) => {
1111
1181
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === (support.selectModel?.configId ?? "model"));
1112
1182
  return typeof option?.currentValue === "string" ? option.currentValue : null;
@@ -1122,7 +1192,7 @@ export function createAcpDriver(support) {
1122
1192
  emit({
1123
1193
  ...base(threadId, turnId),
1124
1194
  type: "session.model-variants",
1125
- model: nativeModel === cliTurn.model ? (turn.model ?? nativeModel) : nativeModel,
1195
+ model: nativeModel === cliTurn.model ? (reportedModel ?? nativeModel) : nativeModel,
1126
1196
  variants: {
1127
1197
  options: option?.options ?? [],
1128
1198
  ...(option?.currentValue !== undefined ? { currentValue: option.currentValue } : {}),
@@ -1133,8 +1203,8 @@ export function createAcpDriver(support) {
1133
1203
  if (!support.modelVariants)
1134
1204
  throw new Error(`${support.displayName} does not support model variants`);
1135
1205
  const option = acpVariantOption(session.sessionConfigResult);
1136
- if (!option || !option.options.some((entry) => entry.id === turn.variant)) {
1137
- throw new Error(`${support.displayName} does not advertise variant ${turn.variant} for this model`);
1206
+ if (!option || !option.options.some((entry) => entry.id === variant)) {
1207
+ throw new Error(`${support.displayName} does not advertise variant ${variant} for this model`);
1138
1208
  }
1139
1209
  return option;
1140
1210
  };
@@ -1304,7 +1374,7 @@ export function createAcpDriver(support) {
1304
1374
  // session/new on the replacement child.
1305
1375
  session.current = null;
1306
1376
  closeSession(threadId, "reestablish");
1307
- session = openSession(threadId, launch, [...(launch.args ?? []), ...spawnArgs], spawnEnv, cwd, contractKey);
1377
+ session = launchSession();
1308
1378
  sessions.set(threadId, session);
1309
1379
  session.current = current;
1310
1380
  runtimeAcceptsImages = await handshake();
@@ -1352,6 +1422,43 @@ export function createAcpDriver(support) {
1352
1422
  if (support.selectModel) {
1353
1423
  const { configId } = support.selectModel;
1354
1424
  selectedModel = modelOf(session.sessionConfigResult);
1425
+ const wanted = cliTurn.model;
1426
+ const offered = offeredModels(session.sessionConfigResult, configId);
1427
+ // A process started without a default model lists exactly
1428
+ // what this account offers.
1429
+ if (support.sessionModelEnv && !session.launchModel && offered.length)
1430
+ accountModels = new Set(offered);
1431
+ // The instance catalog lists the model but a pooled process
1432
+ // does not: the process is older than the catalog (a provider
1433
+ // added since it started). That is no reason to run another
1434
+ // model. The switch below fails with the runtime's own words,
1435
+ // and the process is closed so the retry starts on a fresh
1436
+ // one. A process this turn started is current, so there the
1437
+ // catalog is the stale one and the fallback applies.
1438
+ staleProcess = Boolean(support.fallbackModel && wanted && !launchedThisTurn && offered.length
1439
+ && !offered.includes(wanted) && models.options.some((option) => option.id === wanted));
1440
+ // A local inject writes its provider for this turn, so a
1441
+ // process started earlier may not list it yet: never replace it.
1442
+ if (support.fallbackModel && offered.length && !skipSubscriptionAuthForLocalInject(turn.model)
1443
+ && !staleProcess && (!wanted || !offered.includes(wanted))) {
1444
+ const fallback = support.fallbackModel(session.sessionConfigResult, models.default);
1445
+ if (fallback && fallback !== wanted && offered.includes(fallback)) {
1446
+ const notice = wanted ? `${wanted}\n${fallback}` : null;
1447
+ if (notice && session.fallbackNotice !== notice) {
1448
+ session.fallbackNotice = notice;
1449
+ emit({
1450
+ ...base(threadId, turnId),
1451
+ type: "runtime.notice",
1452
+ message: `${support.displayName} no longer offers ${wanted}, so this conversation uses ${fallback}. ` +
1453
+ "Choose another model for this bot to stop seeing this.",
1454
+ });
1455
+ }
1456
+ cliTurn = { ...cliTurn, model: fallback };
1457
+ current.turn = cliTurn;
1458
+ reportedModel = fallback;
1459
+ variant = undefined;
1460
+ }
1461
+ }
1355
1462
  if (cliTurn.model && cliTurn.model !== selectedModel) {
1356
1463
  sessionResult = await request("session/set_config_option", { sessionId, configId, value: cliTurn.model }, INIT_TIMEOUT, receiveModelVariants);
1357
1464
  selectedModel = modelOf(session.sessionConfigResult);
@@ -1363,6 +1470,7 @@ export function createAcpDriver(support) {
1363
1470
  }
1364
1471
  }
1365
1472
  if (support.configureSession) {
1473
+ approvalUnconfirmed = support.sessionScopedApproval === true;
1366
1474
  await support.configureSession({
1367
1475
  request: (method, params, timeoutMs) => request(method, params, timeoutMs ?? SESSION_CONFIG_TIMEOUT),
1368
1476
  sessionId,
@@ -1372,17 +1480,18 @@ export function createAcpDriver(support) {
1372
1480
  ? sessionResult.models.availableModels
1373
1481
  : [],
1374
1482
  });
1483
+ approvalUnconfirmed = false;
1375
1484
  // initialize's currentModelId is the CLI default,
1376
1485
  // not the model this turn asked for. After a successful pin,
1377
1486
  // report the slug we set so the UI does not claim otherwise.
1378
1487
  if (!selectedModel && cliTurn.model)
1379
1488
  selectedModel = cliTurn.model;
1380
1489
  }
1381
- if (turn.variant !== undefined) {
1490
+ if (variant !== undefined) {
1382
1491
  const option = requestedVariantOption();
1383
- await request("session/set_config_option", { sessionId, configId: option.configId, value: turn.variant }, SESSION_CONFIG_TIMEOUT, receiveModelVariants);
1384
- if (requestedVariantOption().currentValue !== turn.variant) {
1385
- throw new Error(`${support.displayName} did not apply variant ${turn.variant}`);
1492
+ await request("session/set_config_option", { sessionId, configId: option.configId, value: variant }, SESSION_CONFIG_TIMEOUT, receiveModelVariants);
1493
+ if (requestedVariantOption().currentValue !== variant) {
1494
+ throw new Error(`${support.displayName} did not apply variant ${variant}`);
1386
1495
  }
1387
1496
  }
1388
1497
  }
@@ -1421,7 +1530,7 @@ export function createAcpDriver(support) {
1421
1530
  if (support.modelVariants && cliTurn.model && modelOf(session.sessionConfigResult) !== cliTurn.model) {
1422
1531
  throw new Error(`${support.displayName} changed model before the prompt`);
1423
1532
  }
1424
- if (turn.variant !== undefined && requestedVariantOption().currentValue !== turn.variant) {
1533
+ if (variant !== undefined && requestedVariantOption().currentValue !== variant) {
1425
1534
  throw new Error(`${support.displayName} changed variant before the prompt`);
1426
1535
  }
1427
1536
  if (state.settled || state.stopped || session.closing)
@@ -1441,11 +1550,31 @@ export function createAcpDriver(support) {
1441
1550
  // gemini put it under _meta. Read both rather than lose the count.
1442
1551
  const usage = result?.usage ?? result?._meta ?? {};
1443
1552
  if (typeof usage.inputTokens === "number" || typeof usage.outputTokens === "number") {
1553
+ // opencode 1.18.25 keeps only the uncached share in inputTokens
1554
+ // and reports cache reads beside it (omitted when 0); some
1555
+ // agents count them inside inputTokens. `input` is the whole
1556
+ // prompt with `cachedInput` naming its cached part — the same
1557
+ // convention as the Claude driver and the store — and
1558
+ // totalTokens tells the two wire shapes apart. Cache writes
1559
+ // stay out of `input`, exactly as in the Claude driver, and an
1560
+ // absent cache count emits the bare shape with no cachedInput
1561
+ // key: absent is not zero.
1562
+ const input = usage.inputTokens ?? 0;
1563
+ const output = usage.outputTokens ?? 0;
1564
+ const cachedRead = typeof usage.cachedReadTokens === "number" && usage.cachedReadTokens > 0
1565
+ ? usage.cachedReadTokens
1566
+ : null;
1567
+ const exclusive = cachedRead !== null
1568
+ && (typeof usage.totalTokens !== "number"
1569
+ || usage.totalTokens >= input + output + cachedRead);
1444
1570
  emit({
1445
1571
  ...base(threadId, turnId),
1446
1572
  type: "thread.token-usage.updated",
1447
- input: usage.inputTokens ?? 0,
1448
- output: usage.outputTokens ?? 0,
1573
+ input: cachedRead !== null && exclusive ? input + cachedRead : input,
1574
+ output,
1575
+ ...(cachedRead !== null
1576
+ ? { cachedInput: exclusive ? cachedRead : Math.min(cachedRead, input) }
1577
+ : {}),
1449
1578
  });
1450
1579
  }
1451
1580
  const reason = result?.stopReason;
@@ -1475,12 +1604,19 @@ export function createAcpDriver(support) {
1475
1604
  // classifier is preferred; loginNote remains a compatibility
1476
1605
  // fallback for existing ACP supports.
1477
1606
  const needsAuth = code === "invalid_credentials" || code === "inactive_subscription"
1478
- || message === support.loginNote;
1607
+ || code === "insufficient_funds" || message === support.loginNote;
1608
+ // A key, credit, quota or subscription problem is the account's,
1609
+ // not the process's: the child that reported it is healthy, so it
1610
+ // stays pooled and the retry after a fix starts warm.
1611
+ const accountError = isAccountError(code);
1479
1612
  const failure = classifyError({ text: message });
1480
1613
  const transientStartup = failure.transient || (failure.reason === "unknown" && code === "upstream_outage");
1481
1614
  const denied = /\b(?:(?:permission|access) denied|(?:approval|permission) (?:required|denied|rejected)|requires? (?:approval|permission)|policy (?:restriction|violation)|(?:blocked|denied|restricted) by (?:the )?policy)\b/i.test(message);
1482
- if (turn.startupRecovery && transientStartup && !needsAuth && !denied && (!code || code === "upstream_outage")
1483
- && ![-32700, -32600, -32601, -32602].includes(e?.code)
1615
+ // An unconfirmed approval switch sent no prompt and its process
1616
+ // is discarded either way, so a fresh one can take the turn.
1617
+ const retryableStartup = approvalUnconfirmed || (transientStartup && (!code || code === "upstream_outage")
1618
+ && ![-32700, -32600, -32601, -32602].includes(e?.code));
1619
+ if (turn.startupRecovery && retryableStartup && !needsAuth && !accountError && !denied
1484
1620
  && !state.promptSent && !state.startupActivity && !state.producedItem && !state.text
1485
1621
  && !state.stopped && !session.closing && !asks.size && !current.runningTools.size) {
1486
1622
  // Quiesce before killing: the child's close event must not
@@ -1508,7 +1644,7 @@ export function createAcpDriver(support) {
1508
1644
  emit({
1509
1645
  ...base(threadId, turnId),
1510
1646
  type: "runtime.error",
1511
- message,
1647
+ message: accountError ? support.describeAccountError?.(code, cliTurn.model) ?? message : message,
1512
1648
  ...(needsAuth ? { setup: true } : {}),
1513
1649
  });
1514
1650
  // Internal RPC failures can leave a live child poisoned just
@@ -1516,10 +1652,20 @@ export function createAcpDriver(support) {
1516
1652
  // start the next turn. Never replay this accepted prompt: it
1517
1653
  // may already have executed tools. The next explicit turn can
1518
1654
  // resume the recorded session on a fresh process.
1519
- if (!needsAuth && (e?.acpPromptStall === true || e?.acpSessionFailure === true
1655
+ if (!needsAuth && !accountError && (e?.acpPromptStall === true || e?.acpSessionFailure === true
1520
1656
  || code === "upstream_outage") && session.child.exitCode === null && !session.closing) {
1521
1657
  closeSession(threadId, e?.acpPromptStall === true ? "prompt-stall" : "rpc-failure");
1522
1658
  }
1659
+ else if (approvalUnconfirmed && session.child.exitCode === null && !session.closing) {
1660
+ // Fail closed: the session may still hold a looser mode from
1661
+ // an earlier turn. Nothing was prompted; the next turn starts
1662
+ // a fresh process, whose session begins in the runtime's own
1663
+ // default mode, and applies its mode there.
1664
+ closeSession(threadId, "approval-unconfirmed");
1665
+ }
1666
+ else if (staleProcess && !state.promptSent && session.child.exitCode === null && !session.closing) {
1667
+ closeSession(threadId, "stale-models");
1668
+ }
1523
1669
  settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
1524
1670
  }
1525
1671
  }