openmausbot 0.1.88 → 0.1.89

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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-qd9VD3GV.js → abnfDiagram-O67JEVCF-BwaMaZEm.js} +1 -1
  3. package/dist/assets/{arc-3RwkUjRe.js → arc-Be-8GKxd.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CFKSJS7a.js → architectureDiagram-NJMV4G6O-Dp_U2Kv1.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CG1rEfWD.js → blockDiagram-BEXU5L5S-DQjUjYUk.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-CBW9N664.js → c4Diagram-YGBWAQC7-Bd3aE4Kc.js} +1 -1
  7. package/dist/assets/channel-DqY_gMLg.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-2mRfTksB.js → chunk-2Q5K7J3B-BijjTBeJ.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-Cv1dpErj.js → chunk-3FUC2YCW-CMGdwvzW.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B7IACJp-.js → chunk-5VM5RSS4-0G09BfsZ.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-An0hZtYa.js → chunk-GTNCS2PH-FOp3vSHe.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BcJB2nEm.js → chunk-GWA4HPMP-Dv-20VIn.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CV_W7TN7.js → chunk-JWPE2WC7-CRgrdFcz.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Bcs_kwnA.js → chunk-NETBCI7D-3hP4f7QB.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-lrx_DxFP.js → chunk-WEXAMYUT-CG98IjlG.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DabHlLGM.js → chunk-XXDRQBXY-BrSTC9xG.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BnHj6P7d.js → classDiagram-v2-NBCMYWYE-rpQQQDFX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CfLp6UDi.js → cose-bilkent-JH36ORCC-G6RSafHA.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Dm4dbVA1.js → cynefin-OW5HDTMX-CXjv-IM2.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-B9mH-PID.js → cynefinDiagram-VND7K2PF-Dw07MR9f.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DDc_NVQJ.js → dagre-6A5THRUB-BwPnlTg2.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-lwr8Ip8-.js → diagram-22UHCM2B-Cm6xcMiz.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BLLe6dmI.js → diagram-3UASUU5V-CDpogPbE.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-6eUIq4lz.js → diagram-ATOU4E4O-BV8z3Pnc.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-O-pRN7VQ.js → diagram-CDSNMT55-CKCQhOOC.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BietvGAI.js → diagram-MLGK6HIB-FGvJg69C.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Bd2Cl62X.js → diagram-MPIPVDR6-CwATvgnS.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-B6iDy69W.js → ebnfDiagram-ZINNZB2B-CH_c3SSC.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DQVHfKe0.js → elk-276RUBZZ-3guZi71R.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BWdVp2tQ.js → erDiagram-OPXOYQCR-BMHphR2n.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-WLE6rKB4.js → flowDiagram-KWPJA3E3-Vm5DkW5x.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BbjS-4gn.js → ganttDiagram-FUAMR5RP-DO-1ct07.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dpy2QSGq.js → gitGraphDiagram-X574FWY7-nxeksEIN.js} +1 -1
  34. package/dist/assets/{index-CHnODCr2.js → index-BVPdbq2r.js} +142 -142
  35. package/dist/assets/{index-CIaHn5NS.js → index-Oxk9x45i.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D_jKWlKe.js → infoDiagram-VRGFBTTK-Dkcvr0p7.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DLJohSuK.js → ishikawaDiagram-OU5B5YK6-BwVibkoO.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-DPOFj0qN.js → journeyDiagram-ZHPQQLJL-ByiBZqgG.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BI7v-xpK.js → kanban-definition-PNTS6WVX-qqjgXBF3.js} +1 -1
  40. package/dist/assets/{linear-C2pWPSxO.js → linear-CtJnKe6A.js} +1 -1
  41. package/dist/assets/{mermaid.core-CR-j5WQU.js → mermaid.core-BEiDklrY.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-lz9GvXRh.js → mindmap-definition-NLK3R4M7-DfIYHia9.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-Dl4_ia61.js → pegDiagram-GJSIUBJH-CYyRqa1a.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-DWuT3n88.js → pieDiagram-5QR66LMP-hOToy_zH.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DXWe9Jx7.js → quadrantDiagram-O4NWA36T-DKAsNN9I.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COY1IBVY.js → railroadDiagram-XR7U4H2S-BSA-d8JU.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-DnyVuhsQ.js → requirementDiagram-PLB6GJNP-CSmlvb-p.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-BSc4NdGK.js → sankeyDiagram-IPEJSGJF-CD5F6fnG.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-kqEU2EGq.js → sequenceDiagram-PO4LG4MO-JUq5sx6d.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BvB4bkWD.js → sizeCapture-INFHLROL-BjIS-Stk.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-BIrqNv7y.js → stateDiagram-v2-GCMORJYK-DklkUxOg.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CFnalQUE.js → swimlanes-2SLR337P-CsjIU6be.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CsSWJnSQ.js → timeline-definition-EJHVYXUP-BkFqK7AB.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-DkAmJ1EE.js → usecaseDiagram-POWQR4AR-70gJB1EU.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-C3vd9drL.js → vennDiagram-UO4OBE2U-83Tm62dN.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-j26CM-pi.js → wardleyDiagram-VNRHLVJA-tTmseoX9.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-BYu1Q_at.js → xychartDiagram-PMCCYNJV-R1LBA55a.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/dist-server/container-mcp.js +8 -0
  61. package/dist-server/index.js +44902 -44655
  62. package/dist-server/openmausbot.js +219 -110
  63. package/dist-server/pair-cli.js +219 -110
  64. package/dist-server/server/automatic-recovery.js +15 -0
  65. package/dist-server/server/cli-api-setup.js +0 -1
  66. package/dist-server/server/config.js +9 -0
  67. package/dist-server/server/container-computer.js +10 -0
  68. package/dist-server/server/contracts.js +10 -0
  69. package/dist-server/server/drivers/acp/core.js +124 -17
  70. package/dist-server/server/drivers/acp/qwen.js +3 -0
  71. package/dist-server/server/drivers/boxagent.js +1 -1
  72. package/dist-server/server/drivers/openai-chat.js +5 -1
  73. package/dist-server/server/harness-capabilities.js +18 -3
  74. package/dist-server/server/index.js +139 -56
  75. package/dist-server/server/system-prompt.js +11 -0
  76. package/dist-server/server/webhooks.js +21 -3
  77. package/dist-server/src/lib/composer-attachments.js +17 -0
  78. package/dist-server/vps-container-mcp.js +8 -0
  79. package/dist-server/workspace-backup.worker.js +8 -0
  80. package/package.json +1 -1
  81. package/dist/assets/channel-CnRnO3tF.js +0 -1
  82. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +0 -8
@@ -40,7 +40,7 @@ import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../proc
40
40
  export function skipSubscriptionAuthForLocalInject(model) {
41
41
  return Boolean(decodeInjectId(model));
42
42
  }
43
- import { newEventId, newId } from "../../contracts.js";
43
+ import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
44
44
  import { augmentedPath } from "../../env-path.js";
45
45
  import { supportsApprovalMode } from "../../../shared/approval-mode.js";
46
46
  import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
@@ -51,6 +51,7 @@ import { extractMcpImages } from "../../mcp-tool-images.js";
51
51
  import { redactSecretsInText } from "../../redact.js";
52
52
  import { recoveryPromptFor } from "../../resume-recovery.js";
53
53
  import { sessionIdlePolicy } from "../session-idle.js";
54
+ import { classifyError } from "../retry.js";
54
55
  /** ACP vendors put the actionable cause in error.data while keeping the
55
56
  * JSON-RPC message generic. Only surface known text fields, never a response
56
57
  * body/config dump, and redact before bounding the displayed diagnostic. */
@@ -98,6 +99,20 @@ const promptIdleTimeoutMs = () => {
98
99
  const ms = Number(raw);
99
100
  return Number.isFinite(ms) && ms > 0 ? ms : 0;
100
101
  };
102
+ /** Keep a turn's set of running tool calls in step with the agent's
103
+ * `tool_call` / `tool_call_update` notifications: a call is running from
104
+ * the first update that is not terminal until one that is. A `tool_call`
105
+ * without a status is pending (ACP's default); a `tool_call_update` without
106
+ * one leaves the call as it was. */
107
+ function trackRunningTool(current, update, defaultStatus) {
108
+ if (typeof update.toolCallId !== "string" || !update.toolCallId)
109
+ return;
110
+ const status = update.status ?? defaultStatus;
111
+ if (status === "completed" || status === "failed")
112
+ current.runningTools.delete(update.toolCallId);
113
+ else if (status === "pending" || status === "in_progress")
114
+ current.runningTools.add(update.toolCallId);
115
+ }
101
116
  const CLIENT_FILE_MAX_BYTES = 8 * 1024 * 1024;
102
117
  function acpVariantOption(result) {
103
118
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.type === "select" && typeof entry.id === "string"
@@ -296,6 +311,23 @@ export function createAcpDriver(support) {
296
311
  // contract prompts the live session instead of paying the handshake
297
312
  // again. An idle session closes after SESSION_IDLE_MS of quiet.
298
313
  const sessions = new Map();
314
+ // Closing removes a session from the pool, not its native writer lease.
315
+ // Retain every closing Qwen child until its process tree is gone, even
316
+ // when Stop or a failed turn lets another turn start during cleanup.
317
+ const retiring = new Map();
318
+ const retireChild = async (threadId, child) => {
319
+ let children = retiring.get(threadId);
320
+ if (!children)
321
+ retiring.set(threadId, children = new Set());
322
+ children.add(child);
323
+ const stopped = await killCliTree(child);
324
+ if (stopped) {
325
+ children.delete(child);
326
+ if (!children.size && retiring.get(threadId) === children)
327
+ retiring.delete(threadId);
328
+ }
329
+ return stopped;
330
+ };
299
331
  const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("ACP");
300
332
  const closeSession = (threadId, why) => {
301
333
  const session = sessions.get(threadId);
@@ -308,16 +340,20 @@ export function createAcpDriver(support) {
308
340
  // a new turn must never adopt a closing session
309
341
  sessions.delete(threadId);
310
342
  session.acp.close();
311
- // stdin EOF asks the agent to exit; EOF is not a guaranteed exit
312
- // signal for ACP agents, so insist after a grace period
343
+ // EOF is not a guaranteed exit signal. Qwen must be tracked before
344
+ // another turn can resume its history; other agents keep their grace.
313
345
  try {
314
346
  session.child.stdin.end();
315
347
  }
316
348
  catch { }
317
- const kill = setTimeout(() => {
318
- void killCliTree(session.child);
319
- }, 5_000);
320
- kill.unref?.();
349
+ if (support.restartOnMcpChange)
350
+ void retireChild(threadId, session.child);
351
+ else {
352
+ const kill = setTimeout(() => {
353
+ void killCliTree(session.child);
354
+ }, 5_000);
355
+ kill.unref?.();
356
+ }
321
357
  };
322
358
  const armIdle = (threadId) => {
323
359
  const session = sessions.get(threadId);
@@ -436,6 +472,7 @@ export function createAcpDriver(support) {
436
472
  if (!current || current.state.settled)
437
473
  return;
438
474
  current.state.settled = true;
475
+ current.acknowledge();
439
476
  if (current.interruptTimer)
440
477
  clearTimeout(current.interruptTimer);
441
478
  for (const finish of current.asks.values())
@@ -521,8 +558,9 @@ export function createAcpDriver(support) {
521
558
  if (idleTimer)
522
559
  clearTimeout(idleTimer);
523
560
  idleTimer = setTimeout(() => {
524
- // Waiting for a person is not an unresponsive agent.
525
- if (session.current?.asks.size) {
561
+ // Waiting for a person, or for a tool the agent is running,
562
+ // is not an unresponsive agent.
563
+ if (session.current?.asks.size || session.current?.runningTools.size) {
526
564
  armIdle();
527
565
  return;
528
566
  }
@@ -639,6 +677,7 @@ export function createAcpDriver(support) {
639
677
  // server→client permission request → canonical request.opened,
640
678
  // answered fail-closed for the running turn
641
679
  const handleServerRequest = (msg, current) => {
680
+ current.state.startupActivity = true;
642
681
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
643
682
  void handleClientFileRequest(msg);
644
683
  return;
@@ -778,6 +817,9 @@ export function createAcpDriver(support) {
778
817
  current.receiveModelVariants(p.update);
779
818
  return;
780
819
  }
820
+ if (current && ["agent_message_chunk", "agent_thought_chunk", "tool_call", "tool_call_update"].includes(p.update?.sessionUpdate)) {
821
+ current.state.startupActivity = true;
822
+ }
781
823
  if (!current || !current.state.promptSent)
782
824
  return;
783
825
  const u = p.update ?? {};
@@ -811,6 +853,7 @@ export function createAcpDriver(support) {
811
853
  }
812
854
  case "tool_call": {
813
855
  current.flushAssistantText();
856
+ trackRunningTool(current, u, "pending");
814
857
  emit({
815
858
  ...base(threadId, current.turnId),
816
859
  type: "item.started",
@@ -823,6 +866,7 @@ export function createAcpDriver(support) {
823
866
  break;
824
867
  }
825
868
  case "tool_call_update": {
869
+ trackRunningTool(current, u);
826
870
  if (u.status === "completed" || u.status === "failed") {
827
871
  current.state.producedItem = true;
828
872
  emit({
@@ -1010,8 +1054,8 @@ export function createAcpDriver(support) {
1010
1054
  // transformEnv-policy supports (opencode). mcpServers are session
1011
1055
  // establishment inputs — they ride session/new and session/load over
1012
1056
  // the wire — and the harness mints fresh integration bearer tokens
1013
- // every turn, so they must not respawn the process; a change instead
1014
- // re-establishes the session below (see sessionKey).
1057
+ // every turn. Most agents apply changes on live load; those that cache
1058
+ // the old MCP clients must resume on a fresh process (see sessionKey).
1015
1059
  // The env the spawned child actually receives is part of the
1016
1060
  // contract too, and arrives hashed as envFingerprint for the same
1017
1061
  // reason.
@@ -1030,7 +1074,8 @@ export function createAcpDriver(support) {
1030
1074
  }
1031
1075
  const pooled = sessions.get(threadId);
1032
1076
  let session;
1033
- if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey) {
1077
+ if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey
1078
+ && (!support.restartOnMcpChange || pooled.sessionKey === sessionKey)) {
1034
1079
  // adoption cancels the idle countdown — a running turn is not quiet
1035
1080
  if (pooled.idleTimer)
1036
1081
  clearTimeout(pooled.idleTimer);
@@ -1053,7 +1098,13 @@ export function createAcpDriver(support) {
1053
1098
  // respawns the child, every wire call must reach the live record, so
1054
1099
  // nothing captures the connection off it.
1055
1100
  const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
1056
- const state = { settled: false, promptSent: false, text: "", producedItem: false };
1101
+ const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false };
1102
+ let acknowledge = () => { };
1103
+ let rejectStartup = (_error) => { };
1104
+ const startupAck = turn.startupRecovery ? new Promise((resolve, reject) => {
1105
+ acknowledge = () => resolve({ turnId });
1106
+ rejectStartup = reject;
1107
+ }) : null;
1057
1108
  const asks = new Map();
1058
1109
  const modelOf = (result) => {
1059
1110
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === (support.selectModel?.configId ?? "model"));
@@ -1101,12 +1152,15 @@ export function createAcpDriver(support) {
1101
1152
  turnConfig,
1102
1153
  controlsHost,
1103
1154
  state,
1155
+ acknowledge,
1104
1156
  asks,
1157
+ runningTools: new Set(),
1105
1158
  interruptTimer: null,
1106
1159
  flushAssistantText,
1107
1160
  receiveModelVariants,
1108
1161
  };
1109
1162
  const interrupt = () => {
1163
+ state.stopped = true;
1110
1164
  if (session.sessionId) {
1111
1165
  session.acp.send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: session.sessionId } });
1112
1166
  if (current.interruptTimer)
@@ -1123,7 +1177,7 @@ export function createAcpDriver(support) {
1123
1177
  closeSession(threadId, "stop");
1124
1178
  }
1125
1179
  };
1126
- active.set(threadId, { stop: () => closeSession(threadId, "stop"), interrupt, turnId, asks });
1180
+ active.set(threadId, { stop: () => { state.stopped = true; closeSession(threadId, "stop"); }, interrupt, turnId, asks });
1127
1181
  emit({ ...base(threadId, turnId), type: "turn.started" });
1128
1182
  session.current = current;
1129
1183
  (async () => {
@@ -1134,6 +1188,19 @@ export function createAcpDriver(support) {
1134
1188
  // Returns whether this runtime accepts image prompts, for the
1135
1189
  // prompt phase below.
1136
1190
  const handshake = async () => {
1191
+ // Also covers timeout/reset/re-establishment, not just rotated
1192
+ // MCP credentials. Stop remains wired while cleanup is pending.
1193
+ for (const child of retiring.get(threadId) ?? []) {
1194
+ const stopped = await retireChild(threadId, child);
1195
+ if (state.settled || session.closing)
1196
+ throw new Error("session closed");
1197
+ if (!stopped) {
1198
+ closeSession(threadId, "replace-failed");
1199
+ throw new Error(`${support.displayName} could not close its previous tool session. Try again.`);
1200
+ }
1201
+ }
1202
+ if (state.settled || session.closing)
1203
+ throw new Error("session closed");
1137
1204
  if (!session.initResult) {
1138
1205
  session.initResult = await request("initialize", {
1139
1206
  protocolVersion: 1,
@@ -1356,10 +1423,13 @@ export function createAcpDriver(support) {
1356
1423
  if (turn.variant !== undefined && requestedVariantOption().currentValue !== turn.variant) {
1357
1424
  throw new Error(`${support.displayName} changed variant before the prompt`);
1358
1425
  }
1426
+ if (state.settled || state.stopped || session.closing)
1427
+ throw new Error("turn stopped");
1359
1428
  state.promptSent = true;
1429
+ acknowledge();
1360
1430
  const promptIdleMs = promptIdleTimeoutMs();
1361
- const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND} went fully silent ${Math.round(promptIdleMs / 1000)} s after the message and the turn was stopped. ` +
1362
- "Raise OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS if this model legitimately takes longer to answer.");
1431
+ const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND} sent nothing for ${Math.round(promptIdleMs / 1000)} s with no tool running, so the turn was stopped as stuck. ` +
1432
+ "Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).");
1363
1433
  if (pendingSplitReceipt) {
1364
1434
  // session/prompt resolving is the acceptance boundary: a
1365
1435
  // rejected prompt leaves the receipt unwritten, so the next
@@ -1405,6 +1475,35 @@ export function createAcpDriver(support) {
1405
1475
  // fallback for existing ACP supports.
1406
1476
  const needsAuth = code === "invalid_credentials" || code === "inactive_subscription"
1407
1477
  || message === support.loginNote;
1478
+ const failure = classifyError({ text: message });
1479
+ const transientStartup = failure.transient || (failure.reason === "unknown" && code === "upstream_outage");
1480
+ 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);
1481
+ if (turn.startupRecovery && transientStartup && !needsAuth && !denied && (!code || code === "upstream_outage")
1482
+ && ![-32700, -32600, -32601, -32602].includes(e?.code)
1483
+ && !state.promptSent && !state.startupActivity && !state.producedItem && !state.text
1484
+ && !state.stopped && !session.closing && !asks.size && !current.runningTools.size) {
1485
+ // Quiesce before killing: the child's close event must not
1486
+ // publish a terminal completion before the harness can recover.
1487
+ state.settled = true;
1488
+ session.current = null;
1489
+ if (current.interruptTimer)
1490
+ clearTimeout(current.interruptTimer);
1491
+ closeSession(threadId, "startup-recovery");
1492
+ const stopped = await killCliTree(session.child).catch(() => false);
1493
+ if (current.interruptTimer)
1494
+ clearTimeout(current.interruptTimer);
1495
+ active.delete(threadId);
1496
+ if (stopped && !state.stopped) {
1497
+ rejectStartup(new TurnNotStartedError(turnId, message));
1498
+ return;
1499
+ }
1500
+ acknowledge();
1501
+ if (!state.stopped)
1502
+ emit({ ...base(threadId, turnId), type: "runtime.error", message });
1503
+ emit({ ...base(threadId, turnId), type: "turn.completed", ok: state.stopped,
1504
+ stopReason: state.stopped ? "cancelled" : "rpc_error", cost: null });
1505
+ return;
1506
+ }
1408
1507
  emit({
1409
1508
  ...base(threadId, turnId),
1410
1509
  type: "runtime.error",
@@ -1424,7 +1523,7 @@ export function createAcpDriver(support) {
1424
1523
  }
1425
1524
  }
1426
1525
  })();
1427
- return { turnId };
1526
+ return startupAck ?? { turnId };
1428
1527
  };
1429
1528
  const snapshot = async () => {
1430
1529
  const env = childEnv();
@@ -1481,6 +1580,10 @@ export function createAcpDriver(support) {
1481
1580
  // idle pooled sessions have no running turn — close them too
1482
1581
  for (const threadId of Array.from(sessions.keys()))
1483
1582
  closeSession(threadId, "stopAll");
1583
+ for (const [threadId, children] of retiring) {
1584
+ for (const child of children)
1585
+ void retireChild(threadId, child);
1586
+ }
1484
1587
  },
1485
1588
  onEvent: (listener) => {
1486
1589
  listeners.add(listener);
@@ -1492,6 +1595,10 @@ export function createAcpDriver(support) {
1492
1595
  stop();
1493
1596
  for (const threadId of Array.from(sessions.keys()))
1494
1597
  closeSession(threadId, "dispose");
1598
+ for (const [threadId, children] of retiring) {
1599
+ for (const child of children)
1600
+ void retireChild(threadId, child);
1601
+ }
1495
1602
  listeners.clear();
1496
1603
  },
1497
1604
  };
@@ -235,6 +235,9 @@ const support = {
235
235
  // A raw -m only changes the model within the saved provider. ACP switches
236
236
  // the complete route and confirms it before any prompt leaves OMB.
237
237
  spawnArgs: (config, turn) => ["--acp", ...qwenApprovalArgs(config.fullAuto, turn.approvalMode)],
238
+ // Qwen's live session/load returns its cached MCP clients, ignoring fresh
239
+ // credentials. Cold-load the same conversation when those inputs change.
240
+ restartOnMcpChange: true,
238
241
  selectModel: { configId: "model" },
239
242
  pickAuthMethod: () => null,
240
243
  authFailure: "continue",
@@ -374,7 +374,7 @@ export const BoxAgentDriver = {
374
374
  snapshot,
375
375
  adapter: {
376
376
  provider: DRIVER_KIND,
377
- capabilities: { sessionModelSwitch: "in-session" },
377
+ capabilities: { sessionModelSwitch: "in-session", remoteAgent: true, usesCloudComputer: true },
378
378
  sendTurn,
379
379
  interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
380
380
  respondToRequest: async (threadId, requestId, decision) => {
@@ -617,7 +617,11 @@ export function createOpenAIChatRuntime(options) {
617
617
  : { state: "unavailable", reason: options.unavailableReason },
618
618
  adapter: {
619
619
  provider: options.driverKind,
620
- capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
620
+ capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
621
+ // Same gate as cloudComputerMcp: with tools off the runtime cannot
622
+ // mount the leased Box descriptor either. The fleet invariant test
623
+ // pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
624
+ usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
621
625
  browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
622
626
  sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
623
627
  // The runtime owns the whole tool loop, so it can always take a
@@ -7,7 +7,7 @@
7
7
  // uses one envelope profile and lets the per-driver capability decide the
8
8
  // delta; the tool names themselves come from the real catalog, so this
9
9
  // never drifts from what a turn is actually shown.
10
- import { APPROVAL_MODES, supportsApprovalMode } from "../shared/approval-mode.js";
10
+ import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
11
11
  import { availableTools } from "./drivers/agents-catalog.js";
12
12
  /** The widest turn the harness can build: every optional integration on and
13
13
  * a coordinating (room) turn, where the room-only coordination tools exist.
@@ -35,8 +35,8 @@ export function harnessCapabilityLines(from, to) {
35
35
  : "Loses peer coordination and every team tool: " + names.length + " tools including " + coordination.join(", ") + " go away.");
36
36
  }
37
37
  if (from.driverKind !== to.driverKind) {
38
- const lost = APPROVAL_MODES.filter((mode) => supportsApprovalMode(from.driverKind, mode) && !supportsApprovalMode(to.driverKind, mode));
39
- const gained = APPROVAL_MODES.filter((mode) => !supportsApprovalMode(from.driverKind, mode) && supportsApprovalMode(to.driverKind, mode));
38
+ const lost = APPROVAL_MODES.filter((mode) => driverKindSupportsApprovalMode(from.driverKind, mode) && !driverKindSupportsApprovalMode(to.driverKind, mode));
39
+ const gained = APPROVAL_MODES.filter((mode) => !driverKindSupportsApprovalMode(from.driverKind, mode) && driverKindSupportsApprovalMode(to.driverKind, mode));
40
40
  if (lost.length)
41
41
  lines.push("Loses approval levels: " + lost.join(", ") + ".");
42
42
  if (gained.length)
@@ -44,3 +44,18 @@ export function harnessCapabilityLines(from, to) {
44
44
  }
45
45
  return lines;
46
46
  }
47
+ /** Binds the registry to the approval-support check so each gate passes its
48
+ * model selection — or a driver view it already holds — and stops
49
+ * unwrapping registry.cliTarget(...)?.driverKind at every call site. The
50
+ * lookup prefers shadow state, so a mid-reconfig instance answers with the
51
+ * driver it would actually run. */
52
+ export function createApprovalModeSupport(registry) {
53
+ return function supportsApprovalMode(target, mode) {
54
+ const driverKind = target === undefined
55
+ ? undefined
56
+ : "driverKind" in target
57
+ ? target.driverKind
58
+ : registry.cliTarget(target.instanceId)?.driverKind;
59
+ return driverKindSupportsApprovalMode(driverKind, mode);
60
+ };
61
+ }