openmausbot 0.1.95 → 0.1.97

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 (169) hide show
  1. package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +26 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/container-mcp.js +14 -27
  64. package/dist-server/drivers/agents-proxy.js +108 -49
  65. package/dist-server/harness-mcp-proxy.js +1 -2
  66. package/dist-server/index.js +4664 -4644
  67. package/dist-server/openmausbot.js +1425 -1437
  68. package/dist-server/pair-cli.js +1425 -1437
  69. package/dist-server/permission-proxy.js +0 -4
  70. package/dist-server/server/admin-activity.js +1 -4
  71. package/dist-server/server/auto-approve.js +20 -5
  72. package/dist-server/server/auto-vm-claims.js +13 -1
  73. package/dist-server/server/automatic-recovery.js +1 -2
  74. package/dist-server/server/boat.js +71 -87
  75. package/dist-server/server/bot-overview.js +1 -1
  76. package/dist-server/server/browser-engine.js +38 -1
  77. package/dist-server/server/browser-live.js +112 -25
  78. package/dist-server/server/browser-runtime.js +31 -1
  79. package/dist-server/server/checkpoints.js +340 -167
  80. package/dist-server/server/chief-of-staff.js +7 -6
  81. package/dist-server/server/cli-setup.js +3 -3
  82. package/dist-server/server/cli.js +33 -26
  83. package/dist-server/server/cloud-computer-tools.js +11 -4
  84. package/dist-server/server/cloud-home-start.js +13 -16
  85. package/dist-server/server/cloud-home.js +3 -13
  86. package/dist-server/server/cloud-move-http.js +44 -30
  87. package/dist-server/server/cloud-move.js +20 -18
  88. package/dist-server/server/cloud-owner.js +1 -1
  89. package/dist-server/server/composio.js +10 -3
  90. package/dist-server/server/computer-engine-removal.js +70 -0
  91. package/dist-server/server/computer-selection.js +18 -0
  92. package/dist-server/server/config.js +99 -75
  93. package/dist-server/server/connector-scope-hint.js +83 -0
  94. package/dist-server/server/container-computer.js +16 -5
  95. package/dist-server/server/contracts.js +0 -9
  96. package/dist-server/server/decider/jev.js +1 -4
  97. package/dist-server/server/decider/log.js +1 -3
  98. package/dist-server/server/decision-log.js +1 -4
  99. package/dist-server/server/default-model-selection.js +20 -14
  100. package/dist-server/server/delegations.js +14 -7
  101. package/dist-server/server/delta-context.js +6 -1
  102. package/dist-server/server/drivers/acp/core.js +5 -4
  103. package/dist-server/server/drivers/acp/opencode-go.js +19 -23
  104. package/dist-server/server/drivers/agents-call.js +47 -64
  105. package/dist-server/server/drivers/agents-catalog.js +69 -3
  106. package/dist-server/server/drivers/agents-proxy.js +2 -1
  107. package/dist-server/server/drivers/builtIn.js +0 -2
  108. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  109. package/dist-server/server/drivers/claude.js +49 -30
  110. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  111. package/dist-server/server/drivers/codex.js +7 -6
  112. package/dist-server/server/drivers/grok.js +1 -1
  113. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  114. package/dist-server/server/drivers/openai-chat.js +113 -11
  115. package/dist-server/server/drivers/pi.js +184 -49
  116. package/dist-server/server/drivers/prompt-split.js +4 -4
  117. package/dist-server/server/harness/bus.js +49 -0
  118. package/dist-server/server/harness-mcp-proxy.js +1 -1
  119. package/dist-server/server/index.js +942 -980
  120. package/dist-server/server/live-call.js +11 -6
  121. package/dist-server/server/managed-policy.js +1 -1
  122. package/dist-server/server/mcp-registry.js +35 -0
  123. package/dist-server/server/message-file.js +4 -4
  124. package/dist-server/server/model-catalog/presets.js +8 -0
  125. package/dist-server/server/peer-delivery.js +4 -19
  126. package/dist-server/server/peer-roster.js +40 -6
  127. package/dist-server/server/profile-versions.js +0 -31
  128. package/dist-server/server/provider-key-check.js +2 -1
  129. package/dist-server/server/restart.js +40 -0
  130. package/dist-server/server/room-handoffs.js +12 -0
  131. package/dist-server/server/room-turn-end.js +15 -0
  132. package/dist-server/server/routes/bot-memory.js +0 -24
  133. package/dist-server/server/routes/live.js +3 -3
  134. package/dist-server/server/routine-requests.js +2 -5
  135. package/dist-server/server/routines.js +6 -5
  136. package/dist-server/server/screen-frame-gate.js +1 -3
  137. package/dist-server/server/server-launcher.js +53 -0
  138. package/dist-server/server/store.js +126 -2
  139. package/dist-server/server/surface.js +40 -64
  140. package/dist-server/server/system-prompt.js +23 -13
  141. package/dist-server/server/team-manifest.js +1 -6
  142. package/dist-server/server/tunnel.js +4 -1
  143. package/dist-server/server/turn-continuation.js +162 -0
  144. package/dist-server/server/turn-images.js +1 -21
  145. package/dist-server/server/turn-resources.js +19 -150
  146. package/dist-server/server/usage-ledger.js +3 -1
  147. package/dist-server/server/workspace.js +3 -4
  148. package/dist-server/server-launcher.js +72 -0
  149. package/dist-server/shared/ask-question.js +1 -71
  150. package/dist-server/shared/cloud-computer.js +10 -0
  151. package/dist-server/shared/credential-request.js +1 -1
  152. package/dist-server/shared/failed-turn.js +22 -0
  153. package/dist-server/shared/live-approval.js +2 -2
  154. package/dist-server/shared/package-format.js +3 -3
  155. package/dist-server/shared/pairing-link.js +144 -0
  156. package/dist-server/shared/place-view.js +409 -0
  157. package/dist-server/shared/tool-scope-support.js +1 -2
  158. package/dist-server/vps-container-mcp.js +14 -27
  159. package/dist-server/workspace-backup.worker.js +14 -27
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DS2NbT5x.js +0 -1
  162. package/dist/assets/index-NF7XCHAy.css +0 -1
  163. package/dist/assets/index-X_5ydXIE.js +0 -610
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
  165. package/dist-server/server/claim-idle.js +0 -33
  166. package/dist-server/server/cloud-overflow.js +0 -167
  167. package/dist-server/server/drivers/boatagent.js +0 -443
  168. package/dist-server/server/tightening-requests.js +0 -435
  169. package/dist-server/shared/tightening-request.js +0 -87
@@ -1145,8 +1145,20 @@ export const ClaudeDriver = {
1145
1145
  // logical turn's stop handle in `active` while it sets up, so Stop is
1146
1146
  // never a silent no-op between two CLI processes of the same turn.
1147
1147
  const relaunch = logicalTurnId !== undefined;
1148
- if (active.has(threadId) && !relaunch)
1149
- throw new Error("a turn is already running on this thread");
1148
+ if (!relaunch) {
1149
+ // Stop is acknowledged at once and the process tree reaped after it
1150
+ // (taskkill is asynchronous on Windows); the stopped turn leaves
1151
+ // `active` only when its close is handled. A Stop that lands while
1152
+ // a turn is still starting lets the harness send the next message
1153
+ // before then. That turn waits for the stopped process to be gone,
1154
+ // never overlapping its helpers, instead of being refused.
1155
+ const stopped = sessions.get(threadId);
1156
+ if (stopped?.turn?.stopRequested && active.get(threadId)?.turnId === stopped.turn.turnId &&
1157
+ await killCliTree(stopped.child))
1158
+ await stopped.closed;
1159
+ if (active.has(threadId))
1160
+ throw new Error("a turn is already running on this thread");
1161
+ }
1150
1162
  // A bot-level mode is authoritative for this turn. In particular, an
1151
1163
  // old provider instance may still be configured with
1152
1164
  // `bypassPermissions`; Ask/Auto must restore Claude's interactive
@@ -1459,7 +1471,7 @@ export const ClaudeDriver = {
1459
1471
  if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
1460
1472
  if (live.idleTimer)
1461
1473
  clearTimeout(live.idleTimer);
1462
- live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
1474
+ live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, awaitingInit: true, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
1463
1475
  active.set(threadId, { stop: () => {
1464
1476
  if (live.turn)
1465
1477
  live.turn.stopRequested = true;
@@ -1475,20 +1487,13 @@ export const ClaudeDriver = {
1475
1487
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1476
1488
  live.volatile = volatile;
1477
1489
  const running = live.turn;
1478
- const written = await writeUser(live, threadId, message);
1479
- if (!written && !running?.stopRequested) {
1480
- active.delete(threadId);
1481
- live.turn = null;
1490
+ // A warm process can end between turns just as the next one is
1491
+ // written: this driver learns of an exit only when the event loop
1492
+ // gets to it (later still on Windows), so the check above can find it
1493
+ // live and the write then fail. Nothing was submitted; its close
1494
+ // resumes this same turn on a fresh process (see `awaitingInit`).
1495
+ if (!(await writeUser(live, threadId, message)) && !running?.stopRequested)
1482
1496
  closeSession(threadId, "stdin write failed");
1483
- retryState.delete(threadId);
1484
- if (mcpConfigPath) {
1485
- try {
1486
- rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
1487
- }
1488
- catch { }
1489
- }
1490
- throw new Error("claude session stdin is not writable");
1491
- }
1492
1497
  // the MCP config was for the first spawn; nothing to clean here
1493
1498
  if (mcpConfigPath) {
1494
1499
  try {
@@ -1630,7 +1635,9 @@ export const ClaudeDriver = {
1630
1635
  cleanupUnownedLaunch();
1631
1636
  throw error;
1632
1637
  }
1638
+ let markClosed;
1633
1639
  const session = {
1640
+ closed: new Promise((resolve) => { markClosed = resolve; }),
1634
1641
  child,
1635
1642
  broker,
1636
1643
  mcpConfigPath,
@@ -1749,6 +1756,8 @@ export const ClaudeDriver = {
1749
1756
  break;
1750
1757
  }
1751
1758
  session.sawInit = true;
1759
+ if (session.turn)
1760
+ session.turn.awaitingInit = false;
1752
1761
  session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
1753
1762
  if (typeof o.session_id === "string")
1754
1763
  session.sessionId = o.session_id;
@@ -2051,11 +2060,18 @@ export const ClaudeDriver = {
2051
2060
  const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
2052
2061
  const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
2053
2062
  const verdict = classifyError({ exitCode: code, stderr: message });
2063
+ // A process kept warm from an earlier turn ended before it
2064
+ // announced this one: it never took the prompt. That is a session
2065
+ // ending between turns, not a failed turn, so the same turn
2066
+ // resumes the session on a fresh process at once — no retry row,
2067
+ // no retry budget spent. (A fresh process is never retained, so
2068
+ // this happens at most once per turn.)
2069
+ const endedBeforeTurn = session.turn.awaitingInit === true;
2054
2070
  if (!retry.cancelled &&
2055
- code !== 0 &&
2056
- verdict.transient &&
2057
- !session.turn.sawStreamDelta &&
2058
- retry.attempt < RETRY_MAX_ATTEMPTS - 1) {
2071
+ (endedBeforeTurn || (code !== 0 &&
2072
+ verdict.transient &&
2073
+ !session.turn.sawStreamDelta &&
2074
+ retry.attempt < RETRY_MAX_ATTEMPTS - 1))) {
2059
2075
  // the CLI is gone but the TURN continues: keep the thread busy,
2060
2076
  // emit no terminal event, and relaunch after the backoff. The
2061
2077
  // `active` entry STAYS — it is what makes an interrupt during
@@ -2077,15 +2093,18 @@ export const ClaudeDriver = {
2077
2093
  }
2078
2094
  sessions.delete(threadId);
2079
2095
  session.turn = null;
2080
- retry.attempt++;
2081
- const delayMs = computeBackoff(retry.attempt - 1);
2082
- emit({
2083
- ...base(threadId, turnId),
2084
- type: "turn.retrying",
2085
- attempt: retry.attempt,
2086
- delayMs,
2087
- reason: verdict.reason,
2088
- });
2096
+ let delayMs = 0;
2097
+ if (!endedBeforeTurn) {
2098
+ retry.attempt++;
2099
+ delayMs = computeBackoff(retry.attempt - 1);
2100
+ emit({
2101
+ ...base(threadId, turnId),
2102
+ type: "turn.retrying",
2103
+ attempt: retry.attempt,
2104
+ delayMs,
2105
+ reason: verdict.reason,
2106
+ });
2107
+ }
2089
2108
  void (async () => {
2090
2109
  const wait = interruptibleDelay(delayMs * retryScale, retryAbort.signal);
2091
2110
  await wait.promise;
@@ -2225,7 +2244,7 @@ export const ClaudeDriver = {
2225
2244
  };
2226
2245
  child.on("close", (code) => {
2227
2246
  session.finishClose = () => finalizeClose(code);
2228
- void session.finishClose();
2247
+ void session.finishClose().finally(markClosed);
2229
2248
  });
2230
2249
  const stop = () => {
2231
2250
  if (session.turn)
@@ -9,7 +9,9 @@ const MAX_OUTPUT = 16_384;
9
9
  // Two configured instances can use the same on-disk login. Only the auth
10
10
  // operation is exclusive: normal turns are not blocked by this lock.
11
11
  const authenticatingHomes = new Set();
12
- function canonicalPath(path) {
12
+ /** The real path of a credential home, so two spellings of one home share
13
+ * one sign-in lock. A missing tail keeps its name under its real parent. */
14
+ export function canonicalPath(path) {
13
15
  try {
14
16
  return realpathSync(path);
15
17
  }
@@ -1567,10 +1567,11 @@ export const CodexDriver = {
1567
1567
  // Only the stable half of the prompt belongs in the developer slot:
1568
1568
  // it is the part that must survive compaction unchanged, and any
1569
1569
  // change to it invalidates the provider's cached prefix. The volatile
1570
- // half (memory, mentions, outstanding teammate work) is delivered
1571
- // inside the turn that changed it, after the cached prefix, the same
1572
- // contract SendTurnInput.systemStable documents. Without the split
1573
- // the driver keeps the previous single-block behaviour.
1570
+ // half (the sections in VOLATILE_SECTIONS, system-prompt.ts) is
1571
+ // delivered inside the turn that changed it, after the cached
1572
+ // prefix, the same contract SendTurnInput.systemStable documents.
1573
+ // Without the split the driver keeps the previous single-block
1574
+ // behaviour.
1574
1575
  const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
1575
1576
  ? turn.systemStable
1576
1577
  : null;
@@ -1710,8 +1711,8 @@ export const CodexDriver = {
1710
1711
  // back. The app-server offers no way to clear a level either:
1711
1712
  // "" is rejected outright and thread/start takes no effort at
1712
1713
  // all. So a thread keeps the last level it was sent until it is
1713
- // sent another, and choosing Default lands on the bot's next new
1714
- // thread rather than the current one.
1714
+ // sent another; back on Default, the harness gives a thread that
1715
+ // holds a level a new thread instead of resuming it (server/index.ts).
1715
1716
  ...(turn.effort ? { effort: turn.effort } : {}),
1716
1717
  });
1717
1718
  };
@@ -45,7 +45,7 @@ export const GrokDriver = {
45
45
  generateModel: () => "grok-3-mini",
46
46
  nativeLog: {
47
47
  source: "xai.chat.completions",
48
- outgoing: (_turn, messages, model) => ({ model, messages }),
48
+ outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
49
49
  incoming: ({ text, usage }) => ({ text, usage }),
50
50
  },
51
51
  });
@@ -1,4 +1,6 @@
1
- export const MAX_CHAT_TOOL_CALLS = 32;
1
+ /** Calls one model reply may carry (its index bound). The turn's own total is
2
+ * MAX_TURN_TOOL_CALLS in openai-chat.ts. */
3
+ const MAX_REPLY_TOOL_CALLS = 32;
2
4
  const MAX_ARGUMENT_CHARS = 256_000;
3
5
  export class ChatProtocolError extends Error {
4
6
  }
@@ -36,7 +38,7 @@ export class ChatToolCalls {
36
38
  for (const [position, raw] of value.entries()) {
37
39
  const delta = object(raw);
38
40
  const index = streaming ? delta?.index : position;
39
- if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_CHAT_TOOL_CALLS) {
41
+ if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_REPLY_TOOL_CALLS) {
40
42
  throw new ChatProtocolError("provider returned an invalid tool-call index");
41
43
  }
42
44
  const call = this.calls.get(Number(index)) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
@@ -7,9 +7,10 @@ import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
7
7
  import { assertImageTransport, chatImageBudget, chatToolImages, chatUserContent } from "./chat-images.js";
8
8
  import { promptHalves, volatileContextNote, withContextNote } from "./prompt-split.js";
9
9
  import { createChatToolApproval } from "./chat-tool-approval.js";
10
- import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, MAX_CHAT_TOOL_CALLS, object } from "./openai-chat-protocol.js";
10
+ import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, object } from "./openai-chat-protocol.js";
11
11
  import { appendNative } from "./native.js";
12
12
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
13
+ import { classifyContinuable, writeTurnHandoff } from "../turn-continuation.js";
13
14
  /** The message of a JSON error body a provider returned with HTTP 200.
14
15
  * MiniMax reports auth, balance, and parameter failures as `base_resp`. */
15
16
  function providerError(json) {
@@ -31,6 +32,15 @@ const usageFrom = (usage) => usage
31
32
  const asError = (value) => value instanceof Error ? value : new Error(String(value));
32
33
  class UnsupportedChatToolsError extends Error {
33
34
  }
35
+ /** One turn's safety stops: model steps, and tool calls across all of them.
36
+ * Tool-heavy models (a memory server, a browser) take dozens of steps, and
37
+ * a stopped turn cannot simply be continued — the next turn is rebuilt from
38
+ * the text transcript, which holds none of these tool results. The calls
39
+ * that ran have taken effect, so the stop asks only for what is left: a
40
+ * retry of the whole task would repeat them. */
41
+ const MAX_CHAT_ROUNDS = 64;
42
+ const MAX_TURN_TOOL_CALLS = 200;
43
+ const stoppedAfter = (count) => `Stopped after ${count} without a final answer. The steps so far already ran, so ask only for what's left.`;
34
44
  const NUDGE_ANNOUNCED_ACTION = "You said what you would do next but called no tool. Do it now with your tools, or reply with your final answer if nothing is left to do.";
35
45
  /** A short reply that only announces a next step, with nothing to answer. */
36
46
  export function announcesAction(text) {
@@ -78,6 +88,53 @@ function rejectsReasoningReplay(status, body) {
78
88
  return typeof message === "string" && /\breasoning_content\b/.test(message) &&
79
89
  /\b(?:unsupported|not supported|unknown|unrecognized|unexpected|not permitted|not allowed)\b/i.test(message);
80
90
  }
91
+ /** Groq checks each generated tool call against the request and fails the
92
+ * completion with `tool_use_failed` when the call is invalid: most often a
93
+ * tool it was not offered (gpt-oss inventing a "json" tool to answer in
94
+ * JSON), sometimes arguments that miss the schema. `refusal` is the
95
+ * provider's whole error object, `failed_generation` included. */
96
+ class RejectedToolCallError extends ChatProtocolError {
97
+ refusal;
98
+ constructor(message, refusal) {
99
+ super(message);
100
+ this.refusal = refusal;
101
+ }
102
+ }
103
+ const toolCallRefusal = (error) => {
104
+ const body = object(error);
105
+ return body?.code === "tool_use_failed" ? body : undefined;
106
+ };
107
+ function refusedToolCall(status, body) {
108
+ if (status !== 400)
109
+ return undefined;
110
+ try {
111
+ return toolCallRefusal(object(JSON.parse(body))?.error);
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ function completionError(json, label) {
118
+ const message = providerError(json);
119
+ if (!message)
120
+ return null;
121
+ const text = `provider returned a ${label}: ${message.slice(0, 200)}`;
122
+ const refusal = toolCallRefusal(json.error);
123
+ return refusal ? new RejectedToolCallError(text, refusal) : new ChatProtocolError(text);
124
+ }
125
+ /** What a failed turn says once the provider refused every attempt: what
126
+ * happened, the next step, then the provider's words. The refused call ran
127
+ * nothing, but tool calls from earlier steps of the turn did, and a Retry
128
+ * of the whole request would repeat them. */
129
+ function refusedToolCallFailure(error, earlierSteps) {
130
+ const words = typeof error.refusal.message === "string" ? error.refusal.message : error.message;
131
+ const tool = /attempted to call tool '([^']+)'/.exec(words)?.[1];
132
+ const what = tool ? `tried to use a tool it was not given ("${tool}")` : "made a tool call the provider rejected";
133
+ const next = earlierSteps
134
+ ? "The steps before it already ran, so ask only for what's left."
135
+ : "Nothing ran. Retry; if it keeps happening, rephrase the request or choose another model.";
136
+ return `The model ${what}. ${next} Provider: ${words.slice(0, 200)}`;
137
+ }
81
138
  /** Shared runtime for the three providers that speak OpenAI chat completions. */
82
139
  export function createOpenAIChatRuntime(options) {
83
140
  const { input } = options;
@@ -128,6 +185,9 @@ export function createOpenAIChatRuntime(options) {
128
185
  const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
129
186
  if (rejectsToolsParameter(response.status, body))
130
187
  throw new UnsupportedChatToolsError(message);
188
+ const refusal = refusedToolCall(response.status, body);
189
+ if (refusal)
190
+ throw new RejectedToolCallError(message, refusal);
131
191
  if (messages.some((entry) => entry.reasoning_content !== undefined) && rejectsReasoningReplay(response.status, body)) {
132
192
  throw new UnsupportedReasoningReplayError(message);
133
193
  }
@@ -146,9 +206,9 @@ export function createOpenAIChatRuntime(options) {
146
206
  costUsd: count(json.usage?.cost),
147
207
  });
148
208
  }
149
- const bodyError = providerError(json);
209
+ const bodyError = completionError(json, "completion error");
150
210
  if (bodyError)
151
- throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
211
+ throw bodyError;
152
212
  const message = json.choices?.[0]?.message;
153
213
  if (!message || !object(message) || !["content", "reasoning_content", "reasoning", "reasoning_details", "tool_calls", "function_call"].some((key) => key in message)) {
154
214
  throw new ChatProtocolError("provider returned no completion message");
@@ -210,9 +270,9 @@ export function createOpenAIChatRuntime(options) {
210
270
  malformedFrame = true;
211
271
  return false;
212
272
  }
213
- const chunkError = providerError(chunk);
273
+ const chunkError = completionError(chunk, "streaming completion error");
214
274
  if (chunkError)
215
- throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
275
+ throw chunkError;
216
276
  const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
217
277
  const delta = choice?.delta;
218
278
  if (object(delta))
@@ -262,9 +322,9 @@ export function createOpenAIChatRuntime(options) {
262
322
  catch {
263
323
  body = undefined;
264
324
  }
265
- const bodyError = body ? providerError(body) : null;
325
+ const bodyError = body ? completionError(body, "completion error") : null;
266
326
  if (bodyError)
267
- throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
327
+ throw bodyError;
268
328
  }
269
329
  throw new ChatProtocolError("Stream ended before completion");
270
330
  }
@@ -413,7 +473,7 @@ export function createOpenAIChatRuntime(options) {
413
473
  if (questionAllowed && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
414
474
  tools.definitions.push(ASK_USER_TOOL_DEFINITION);
415
475
  }
416
- for (let round = 0; round < 16; round++) {
476
+ for (let round = 0; round < MAX_CHAT_ROUNDS; round++) {
417
477
  abort.signal.throwIfAborted();
418
478
  // Drain parked mid-turn input here, before the next completion
419
479
  // request: the previous round's tool results are complete, so a
@@ -427,6 +487,7 @@ export function createOpenAIChatRuntime(options) {
427
487
  let completion;
428
488
  for (;;) {
429
489
  let streamed = false;
490
+ let answerStreamed = false;
430
491
  const pending = { assistant_text: "", reasoning_text: "" };
431
492
  const delta = (text, streamKind, flush = false) => {
432
493
  let combined = pending[streamKind] + text;
@@ -455,6 +516,8 @@ export function createOpenAIChatRuntime(options) {
455
516
  try {
456
517
  completion = await complete(messages, model, true, abort.signal, (text, streamKind) => {
457
518
  streamed = true;
519
+ if (streamKind === "assistant_text")
520
+ answerStreamed = true;
458
521
  delta(text, streamKind);
459
522
  }, tools.definitions);
460
523
  delta("", "assistant_text", true);
@@ -474,6 +537,20 @@ export function createOpenAIChatRuntime(options) {
474
537
  delete message.reasoning_content;
475
538
  continue;
476
539
  }
540
+ // A refused tool call ran nothing, so the same request goes
541
+ // again; the model usually answers properly next time. Not once
542
+ // answer text streamed: the phones' live bubble would join it to
543
+ // the next attempt's. Streamed thinking stays thinking. Each
544
+ // refusal, failed_generation included, goes to the native log.
545
+ if (error instanceof RejectedToolCallError) {
546
+ native("in", { refused: "tool_use_failed", attempt: attempt + 1, error: error.refusal });
547
+ if (!answerStreamed && !abort.signal.aborted && attempt < RETRY_MAX_ATTEMPTS - 1) {
548
+ emit({ ...base(turn.threadId, turnId), type: "turn.retrying", attempt: ++attempt, delayMs: 0, reason: "tool_use_failed" });
549
+ continue;
550
+ }
551
+ if (!abort.signal.aborted)
552
+ throw new ChatProtocolError(refusedToolCallFailure(error, seenCalls.size > 0));
553
+ }
477
554
  // Only our optional question changed a previously plain request.
478
555
  // A structured parameter rejection has executed nothing; never
479
556
  // downgrade mounted tools, streamed output or a handled call.
@@ -517,6 +594,9 @@ export function createOpenAIChatRuntime(options) {
517
594
  continue;
518
595
  }
519
596
  if (toolFailed) {
597
+ // A failed or denied tool op ends the turn: the final answer is
598
+ // not an execution receipt, and a person's "no" must never be
599
+ // answered with a prompt to run the same thing again.
520
600
  stopReason = "tool_error";
521
601
  throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
522
602
  }
@@ -531,8 +611,9 @@ export function createOpenAIChatRuntime(options) {
531
611
  throw new ChatProtocolError("provider reused a tool-call ID; refusing to repeat an operation");
532
612
  seenCalls.add(call.id);
533
613
  }
534
- if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
535
- throw new ChatProtocolError("tool-call limit reached");
614
+ // The whole batch is refused before any of it runs.
615
+ if (seenCalls.size > MAX_TURN_TOOL_CALLS)
616
+ throw new ChatProtocolError(stoppedAfter(`${MAX_TURN_TOOL_CALLS} tool calls`));
536
617
  messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
537
618
  ...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { [options.reasoningReplayField ?? "reasoning_content"]: completion.protocolReasoning } : {}),
538
619
  ...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
@@ -645,7 +726,7 @@ export function createOpenAIChatRuntime(options) {
645
726
  messages.push({ role: "user", content: screenshotParts });
646
727
  }
647
728
  if (!ok)
648
- throw new ChatProtocolError("model-call limit reached before a final response");
729
+ throw new ChatProtocolError(stoppedAfter(`${MAX_CHAT_ROUNDS} steps`));
649
730
  }
650
731
  catch (value) {
651
732
  stopReason = abort.signal.aborted ? "interrupted" : stopReason ?? "error";
@@ -670,6 +751,27 @@ export function createOpenAIChatRuntime(options) {
670
751
  if (failure && (!abort.signal.aborted || cleanupFailed)) {
671
752
  emit({ ...base(turn.threadId, turnId), type: "runtime.error", message: failure, terminal: !abort.signal.aborted });
672
753
  }
754
+ // Automatic continuity: a resumable terminal (budget cap or tool
755
+ // errors) persists a handoff and raises cap.exhausted so the harness
756
+ // can start a `Continue:` thread. Interruptions and provider config
757
+ // errors are excluded by classifyContinuable.
758
+ if (!ok && !abort.signal.aborted) {
759
+ const continuable = classifyContinuable(stopReason, failure);
760
+ if (continuable) {
761
+ const handoffPath = writeTurnHandoff({
762
+ threadId: turn.threadId,
763
+ turnId,
764
+ model,
765
+ usage,
766
+ hasUsage,
767
+ failure,
768
+ toolCalls: seenCalls.size,
769
+ messages,
770
+ });
771
+ if (handoffPath)
772
+ emit({ ...base(turn.threadId, turnId), type: "cap.exhausted", handoffPath, reason: continuable });
773
+ }
774
+ }
673
775
  active.delete(turn.threadId);
674
776
  emit({ ...base(turn.threadId, turnId), type: "turn.completed", ok, stopReason, cost: null,
675
777
  ...(hasUsage && (options.includeUsageInCompleted || seenCalls.size) ? { usage } : {}),