openmausbot 0.1.92 → 0.1.94

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 (163) hide show
  1. package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +45 -1
  63. package/dist-server/container-mcp.js +99 -77
  64. package/dist-server/drivers/agents-proxy.js +40 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +18361 -13794
  68. package/dist-server/local-computer-proxy.js +5 -1
  69. package/dist-server/local-computer.js +1 -0
  70. package/dist-server/mcp-gate.js +152 -20
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +8872 -7620
  73. package/dist-server/pair-cli.js +8872 -7620
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +1 -0
  76. package/dist-server/server/auto-approve.js +33 -11
  77. package/dist-server/server/boat.js +3 -3
  78. package/dist-server/server/browser-live.js +14 -5
  79. package/dist-server/server/browser-runtime.js +6 -0
  80. package/dist-server/server/channel-queue.js +4 -1
  81. package/dist-server/server/cli.js +32 -12
  82. package/dist-server/server/cloud-move-http.js +9 -3
  83. package/dist-server/server/cloud-move.js +18 -0
  84. package/dist-server/server/cloud-owner.js +1 -1
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/computer-wait.js +33 -13
  87. package/dist-server/server/config.js +57 -10
  88. package/dist-server/server/container-computer.js +43 -32
  89. package/dist-server/server/decision-log.js +9 -6
  90. package/dist-server/server/delegations.js +47 -22
  91. package/dist-server/server/digest.js +12 -2
  92. package/dist-server/server/drivers/acp/core.js +229 -20
  93. package/dist-server/server/drivers/acp/cursor.js +13 -1
  94. package/dist-server/server/drivers/acp/grok.js +208 -3
  95. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  96. package/dist-server/server/drivers/acp/qwen.js +18 -0
  97. package/dist-server/server/drivers/agents-call.js +28 -0
  98. package/dist-server/server/drivers/agents-catalog.js +10 -1
  99. package/dist-server/server/drivers/boatagent.js +2 -0
  100. package/dist-server/server/drivers/builtIn.js +2 -0
  101. package/dist-server/server/drivers/cerebras.js +108 -0
  102. package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
  103. package/dist-server/server/drivers/claude.js +35 -9
  104. package/dist-server/server/drivers/codex.js +84 -10
  105. package/dist-server/server/drivers/openai-chat.js +37 -5
  106. package/dist-server/server/drivers/pi.js +48 -4
  107. package/dist-server/server/drivers/prompt-split.js +2 -0
  108. package/dist-server/server/env-path.js +6 -2
  109. package/dist-server/server/environment.js +3 -0
  110. package/dist-server/server/guarded-requests.js +2 -1
  111. package/dist-server/server/index.js +1353 -311
  112. package/dist-server/server/live-call-controller.js +868 -0
  113. package/dist-server/server/live-call.js +216 -0
  114. package/dist-server/server/local-vm-stop-reason.js +26 -0
  115. package/dist-server/server/mcp-gate-config.js +43 -12
  116. package/dist-server/server/mcp-gate.js +127 -25
  117. package/dist-server/server/mcp-http.js +46 -8
  118. package/dist-server/server/mcp-remote-proxy.js +108 -0
  119. package/dist-server/server/new-bot-defaults.js +3 -0
  120. package/dist-server/server/peer-provenance.js +4 -2
  121. package/dist-server/server/permission-proxy.js +15 -1
  122. package/dist-server/server/plan-usage.js +797 -0
  123. package/dist-server/server/provider-key-check.js +2 -1
  124. package/dist-server/server/proxy-paths.js +1 -0
  125. package/dist-server/server/redact.js +2 -0
  126. package/dist-server/server/request-auth.js +3 -2
  127. package/dist-server/server/routes/desktop-viewer.js +50 -6
  128. package/dist-server/server/routes/live.js +98 -0
  129. package/dist-server/server/routines.js +6 -3
  130. package/dist-server/server/skill-library.js +207 -0
  131. package/dist-server/server/skills-library-migration.js +235 -0
  132. package/dist-server/server/skills.js +66 -3
  133. package/dist-server/server/steer-queue.js +13 -1
  134. package/dist-server/server/store.js +86 -4
  135. package/dist-server/server/surface.js +9 -3
  136. package/dist-server/server/team-backup.js +2 -1
  137. package/dist-server/server/thread-events.js +1 -1
  138. package/dist-server/server/tunnel.js +20 -1
  139. package/dist-server/server/turn-log.js +10 -0
  140. package/dist-server/server/turn-resources.js +124 -0
  141. package/dist-server/server/vps-computer.js +1 -1
  142. package/dist-server/server/workspace-backup.js +22 -0
  143. package/dist-server/server/workspace.js +1 -1
  144. package/dist-server/shared/approval-mode.js +1 -1
  145. package/dist-server/shared/ask-question.js +13 -0
  146. package/dist-server/shared/call-consent.js +28 -0
  147. package/dist-server/shared/live-approval.js +124 -0
  148. package/dist-server/shared/live-call.js +176 -0
  149. package/dist-server/shared/redact.js +1 -0
  150. package/dist-server/shared/skill-md.js +10 -0
  151. package/dist-server/shared/team-backup.js +3 -0
  152. package/dist-server/shared/tool-name.js +22 -0
  153. package/dist-server/shared/tool-scope-support.js +25 -0
  154. package/dist-server/shared/tool-scope.js +136 -0
  155. package/dist-server/src/lib/thread-refs.js +406 -0
  156. package/dist-server/vps-container-mcp.js +99 -77
  157. package/dist-server/workspace-backup.worker.js +103 -100
  158. package/enterprise/server/index.js +1 -0
  159. package/package.json +1 -1
  160. package/dist/assets/channel-Dtk5Mtic.js +0 -1
  161. package/dist/assets/index-Cdmjrl28.css +0 -1
  162. package/dist/assets/index-DR7YPMkW.js +0 -597
  163. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
@@ -31,8 +31,9 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
31
31
  import { createHash } from "node:crypto";
32
32
  import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
33
33
  import { decodeInjectId } from "../local-inject.js";
34
- import { promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
34
+ import { deletePromptSplitReceipt, promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
35
35
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
36
+ import { classifyQuiet, describeQuiet, lastSeenPhrase, LogTail, quietKey, sampleProcessTree, } from "./quiet-status.js";
36
37
  /**
37
38
  * A `host::model` pick talks to a loopback server with its own key.
38
39
  * Subscription ACP login (grok.com cached_token) must not fail that turn.
@@ -40,6 +41,12 @@ import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../proc
40
41
  export function skipSubscriptionAuthForLocalInject(model) {
41
42
  return Boolean(decodeInjectId(model));
42
43
  }
44
+ /** A catalog can contain credential-bearing native configuration, including late replies. */
45
+ export function acpNativeIncomingLogMessage(message, privateResponses) {
46
+ if (!privateResponses.has(message?.id))
47
+ return message;
48
+ return { jsonrpc: message.jsonrpc, id: message.id, ...(message.error !== undefined ? { error: "[MCP catalog error omitted]" } : { result: "[MCP catalog omitted]" }) };
49
+ }
43
50
  import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
44
51
  import { augmentedPath } from "../../env-path.js";
45
52
  import { supportsApprovalMode } from "../../../shared/approval-mode.js";
@@ -52,6 +59,8 @@ import { redactSecretsInText } from "../../redact.js";
52
59
  import { recoveryPromptFor } from "../../resume-recovery.js";
53
60
  import { sessionIdlePolicy } from "../session-idle.js";
54
61
  import { classifyError } from "../retry.js";
62
+ import { canUseMcpServer, narrowsNativeTools, parseToolScope } from "../../../shared/tool-scope.js";
63
+ import { gateServer } from "../../mcp-gate-config.js";
55
64
  const ACCOUNT_ERROR_CODES = new Set([
56
65
  "invalid_credentials", "inactive_subscription", "insufficient_funds", "quota_or_region_restriction",
57
66
  ]);
@@ -87,8 +96,13 @@ const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 3
87
96
  const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
88
97
  const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
89
98
  /** ACP agents may compact their own history without telling the client;
90
- * re-send the full prompt after this many bare turns as a backstop. */
99
+ * re-send the full prompt after this many bare turns as a backstop to
100
+ * compaction detection. */
91
101
  const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
102
+ /** A reported context below this share of the peak marks a compaction.
103
+ * OpenCode compactions measured 30-52% of the peak; ordinary dips stay
104
+ * above 83%. A false alarm costs one extra full prompt. */
105
+ const ACP_COMPACTION_COLLAPSE_RATIO = 0.6;
92
106
  // Read lazily (not at import) so a test can shorten the window. Unlike the
93
107
  // setup calls above, session/prompt legitimately streams for minutes, so a
94
108
  // wall-clock deadline would false-positive: this guard only trips when the
@@ -96,13 +110,23 @@ const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
96
110
  // streams thought chunks, then goes silent forever and never resolves). 0
97
111
  // disables the guard, restoring the pre-fix "hang until the user cancels"
98
112
  // behavior.
113
+ //
114
+ // Healthy agents go silent for minutes too. Qwen Code 0.24 sends nothing
115
+ // over ACP while it compresses history, while it backs off a rate limit
116
+ // (60 s up to 5 min per wait), or while one model request runs (its SDK
117
+ // waits up to 600 s). The old 180 s default killed those turns mid-work,
118
+ // and the retry redid it all. A false trip costs the whole turn; a real
119
+ // wedge only costs waiting, and the user can press Stop - so the default
120
+ // sits above the longest normal silence.
121
+ export const DEFAULT_ACP_PROMPT_IDLE_MS = 15 * 60_000;
99
122
  const promptIdleTimeoutMs = () => {
100
123
  const raw = process.env.OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS;
101
124
  if (raw === undefined)
102
- return 180_000;
125
+ return DEFAULT_ACP_PROMPT_IDLE_MS;
103
126
  const ms = Number(raw);
104
127
  return Number.isFinite(ms) && ms > 0 ? ms : 0;
105
128
  };
129
+ const formatQuietLimit = (ms) => ms >= 120_000 && ms % 60_000 === 0 ? `${ms / 60_000} min` : `${Math.round(ms / 1000)} s`;
106
130
  /** Keep a turn's set of running tool calls in step with the agent's
107
131
  * `tool_call` / `tool_call_update` notifications: a call is running from
108
132
  * the first update that is not terminal until one that is. A `tool_call`
@@ -117,6 +141,18 @@ function trackRunningTool(current, update, defaultStatus) {
117
141
  else if (status === "pending" || status === "in_progress")
118
142
  current.runningTools.add(update.toolCallId);
119
143
  }
144
+ let unkeyedToolSeq = 0;
145
+ /** Stamp an unkeyed `tool_call` with a synthetic id and queue it for the
146
+ * call's first terminal update, which carries no id of its own to match.
147
+ * FIFO is the best available pairing when the agent omits `toolCallId`:
148
+ * the ids exist to pair start with completion, not to order concurrent
149
+ * unkeyed calls, and the synthetic id deliberately stays out of
150
+ * `runningTools` so turn-settle semantics are unchanged. */
151
+ function nextUnkeyedToolId(current) {
152
+ const id = `acp-tool-unkeyed-${++unkeyedToolSeq}`;
153
+ current.unkeyedToolIds.push(id);
154
+ return id;
155
+ }
120
156
  const CLIENT_FILE_MAX_BYTES = 8 * 1024 * 1024;
121
157
  function acpVariantOption(result) {
122
158
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.type === "select" && typeof entry.id === "string"
@@ -493,7 +529,113 @@ export function createAcpDriver(support) {
493
529
  }
494
530
  servers.push({ name, command: server.command, args: server.args, env: acpEnv(server.env) });
495
531
  }
496
- return servers;
532
+ if (turn.toolScope === undefined)
533
+ return servers;
534
+ return servers.filter((server) => canUseMcpServer(turn.toolScope, server.name)).map((server) => {
535
+ const original = "url" in server
536
+ ? { type: server.type, url: server.url, headers: Object.fromEntries(server.headers.map(({ name, value }) => [name, value])) }
537
+ : { command: server.command, args: server.args, env: Object.fromEntries(server.env.map(({ name, value }) => [name, value])) };
538
+ const gated = gateServer({ name: server.name, server: original, threadId: turn.threadId, budget: 0,
539
+ toolScope: turn.toolScope, nodeEnv: { ELECTRON_RUN_AS_NODE: "1" } });
540
+ if (!gated)
541
+ throw new Error("Tool selection requires an MCP gate.");
542
+ return { name: server.name, command: gated.command, args: gated.args, env: acpEnv(gated.env) };
543
+ });
544
+ };
545
+ /** While a prompt runs, notice when the agent goes quiet and tell the
546
+ * person what it is doing: retrying, compressing, waiting on its
547
+ * model, busy, or showing no sign of life (see quiet-status.ts). A
548
+ * notice is sent only when that answer changes. New lines in the
549
+ * agent's own log also restart the prompt's idle deadline, so a
550
+ * turn that is visibly retrying is never stopped as stuck. */
551
+ const startQuietWatch = (threadId, session, current) => {
552
+ const noticeAfterMs = envOr("OMB_ACP_QUIET_NOTICE_MS", 60_000);
553
+ const tickMs = envOr("OMB_ACP_QUIET_TICK_MS", 15_000);
554
+ const logPath = support.statusLog && session.sessionId
555
+ ? support.statusLog.path(session.env, session.sessionId)
556
+ : null;
557
+ const tail = logPath ? new LogTail(logPath) : null;
558
+ tail?.read(); // skip what this log held before the prompt
559
+ let logged = null;
560
+ let context = null;
561
+ let previous = null;
562
+ let previousAt = 0;
563
+ let shownKey = null;
564
+ let noSignsSince = null;
565
+ let notices = 0;
566
+ let probing = false;
567
+ let state = null;
568
+ const tick = async () => {
569
+ if (probing || current.state.settled || session.current !== current)
570
+ return;
571
+ const now = Date.now();
572
+ for (const line of tail?.read() ?? []) {
573
+ session.acp.touch();
574
+ const signal = support.statusLog.parse(line);
575
+ if (signal?.kind === "context")
576
+ context = { tokens: signal.tokens, threshold: signal.threshold };
577
+ else if (signal)
578
+ logged = { signal, at: now };
579
+ }
580
+ const quietMs = now - session.acp.lastInboundAt;
581
+ // A running tool or an open question is already on screen.
582
+ if (quietMs < noticeAfterMs || current.asks.size || current.runningTools.size) {
583
+ if (quietMs < noticeAfterMs)
584
+ logged = null;
585
+ previous = null;
586
+ shownKey = null;
587
+ noSignsSince = null;
588
+ state = null;
589
+ return;
590
+ }
591
+ // A retry stays the answer until well past its wait (Qwen's SDK
592
+ // retries on its own for about a minute before the next logged
593
+ // attempt); after that the probe takes over.
594
+ if (logged?.signal.kind === "retry"
595
+ && now - logged.at > (logged.signal.delayMs ?? 60_000) + 120_000)
596
+ logged = null;
597
+ probing = true;
598
+ const sample = session.child.pid ? await sampleProcessTree(session.child.pid) : { cpuMs: null, connections: null };
599
+ probing = false;
600
+ if (current.state.settled || session.current !== current)
601
+ return;
602
+ let next = classifyQuiet({
603
+ logged: logged?.signal ?? null, context, sample, previous, sinceMs: previous ? now - previousAt : 0,
604
+ });
605
+ const first = previous === null;
606
+ previous = sample;
607
+ previousAt = now;
608
+ // CPU use needs two samples; the first only sets the baseline
609
+ // (unless the log already said what is going on).
610
+ if (first && next.kind === "unknown")
611
+ return;
612
+ // An SDK sleeping between its own retries holds no socket and
613
+ // burns no CPU (seen with Qwen against a 429ing endpoint), so
614
+ // "no sign of life" is called only once that has lasted minutes.
615
+ if (next.kind === "no-signs") {
616
+ noSignsSince ??= now;
617
+ if (now - noSignsSince < envOr("OMB_ACP_STUCK_AFTER_MS", 180_000))
618
+ next = { kind: "between-requests" };
619
+ }
620
+ else {
621
+ noSignsSince = null;
622
+ }
623
+ state = next;
624
+ const key = quietKey(next);
625
+ if (key === shownKey || notices >= 12)
626
+ return;
627
+ shownKey = key;
628
+ notices += 1;
629
+ emit({
630
+ ...base(threadId, current.turnId),
631
+ type: "runtime.notice",
632
+ message: describeQuiet(support.displayName, next, quietMs, promptIdleTimeoutMs()),
633
+ });
634
+ };
635
+ const timer = setInterval(() => { void tick(); }, tickMs);
636
+ timer.unref?.();
637
+ current.stopQuietWatch = () => clearInterval(timer);
638
+ return { lastState: () => state };
497
639
  };
498
640
  /** The one completion path for a turn: the prompt result, a crashed
499
641
  * child, an unanswered cancel, or an rpc error the turn body throws.
@@ -505,6 +647,7 @@ export function createAcpDriver(support) {
505
647
  return;
506
648
  current.state.settled = true;
507
649
  current.acknowledge();
650
+ current.stopQuietWatch?.();
508
651
  if (current.interruptTimer)
509
652
  clearTimeout(current.interruptTimer);
510
653
  for (const finish of current.asks.values())
@@ -555,6 +698,7 @@ export function createAcpDriver(support) {
555
698
  stdio: ["pipe", "pipe", "pipe"],
556
699
  });
557
700
  let nextId = 1;
701
+ const privateResponses = new Set();
558
702
  const rpcPending = new Map();
559
703
  const send = (obj) => {
560
704
  // A permission/file response resumes an agent that was waiting on us.
@@ -571,6 +715,8 @@ export function createAcpDriver(support) {
571
715
  };
572
716
  const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => new Promise((resolve, reject) => {
573
717
  const id = nextId++;
718
+ if (method === "_x.ai/mcp/list")
719
+ privateResponses.add(id);
574
720
  let timer = null;
575
721
  if (timeoutMs) {
576
722
  timer = setTimeout(() => {
@@ -597,7 +743,7 @@ export function createAcpDriver(support) {
597
743
  return;
598
744
  }
599
745
  rpcPending.delete(id);
600
- const error = new Error(idleMessage ?? `${method} stopped responding`);
746
+ const error = new Error((typeof idleMessage === "function" ? idleMessage() : idleMessage) ?? `${method} stopped responding`);
601
747
  Object.assign(error, { acpPromptStall: true });
602
748
  reject(error);
603
749
  }, idleMs);
@@ -616,9 +762,13 @@ export function createAcpDriver(support) {
616
762
  });
617
763
  send({ jsonrpc: "2.0", id, method, params });
618
764
  });
765
+ let lastInboundAt = Date.now();
619
766
  const acp = {
620
767
  send,
621
768
  request,
769
+ get lastInboundAt() { return lastInboundAt; },
770
+ touch: () => { for (const p of rpcPending.values())
771
+ p.armIdle(); },
622
772
  failAll: (error) => {
623
773
  for (const p of rpcPending.values()) {
624
774
  if (p.timer)
@@ -891,7 +1041,7 @@ export function createAcpDriver(support) {
891
1041
  ...base(threadId, current.turnId),
892
1042
  type: "item.started",
893
1043
  itemType: "tool",
894
- itemId: u.toolCallId,
1044
+ itemId: (typeof u.toolCallId === "string" && u.toolCallId) ? u.toolCallId : nextUnkeyedToolId(current),
895
1045
  title: String(u.rawInput?.command ?? u.title ?? "tool").slice(0, 80),
896
1046
  summary: commandSummary(u.rawInput),
897
1047
  input: toolDetailPreview(u.rawInput),
@@ -906,7 +1056,7 @@ export function createAcpDriver(support) {
906
1056
  ...base(threadId, current.turnId),
907
1057
  type: "item.completed",
908
1058
  itemType: "tool",
909
- itemId: u.toolCallId,
1059
+ itemId: (typeof u.toolCallId === "string" && u.toolCallId) ? u.toolCallId : current.unkeyedToolIds.shift(),
910
1060
  ok: u.status !== "failed",
911
1061
  output: toolDetailPreview(u.rawOutput ?? u.content),
912
1062
  });
@@ -916,12 +1066,26 @@ export function createAcpDriver(support) {
916
1066
  }
917
1067
  break;
918
1068
  }
1069
+ case "usage_update": {
1070
+ // opencode 1.18 sends `used` flat; ignore non-positive reports
1071
+ const used = u.used ?? u.usage?.used;
1072
+ if (typeof used !== "number" || !(used > 0))
1073
+ break;
1074
+ if (current.state.usagePeak !== null && used < current.state.usagePeak * ACP_COMPACTION_COLLAPSE_RATIO) {
1075
+ current.state.usageCompacted = true;
1076
+ }
1077
+ if (current.state.usagePeak === null || used > current.state.usagePeak)
1078
+ current.state.usagePeak = used;
1079
+ current.state.usageLast = used;
1080
+ break;
1081
+ }
919
1082
  }
920
1083
  };
921
1084
  const session = {
922
1085
  child,
923
1086
  acp,
924
1087
  launch,
1088
+ env,
925
1089
  cwd,
926
1090
  contractKey,
927
1091
  sessionKey: null,
@@ -954,9 +1118,10 @@ export function createAcpDriver(support) {
954
1118
  catch {
955
1119
  continue;
956
1120
  }
957
- appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(msg) });
1121
+ appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(acpNativeIncomingLogMessage(msg, privateResponses)) });
958
1122
  // Inbound traffic proves the child is alive and making progress,
959
1123
  // so every idle deadline restarts; only total silence trips it.
1124
+ lastInboundAt = Date.now();
960
1125
  for (const p of rpcPending.values())
961
1126
  p.armIdle();
962
1127
  if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
@@ -1029,6 +1194,13 @@ export function createAcpDriver(support) {
1029
1194
  return session;
1030
1195
  };
1031
1196
  const sendTurn = async (turn) => {
1197
+ const parsedScope = parseToolScope(turn.toolScope);
1198
+ if (!parsedScope.ok)
1199
+ throw new Error(parsedScope.error);
1200
+ turn = { ...turn, toolScope: parsedScope.scope };
1201
+ if (narrowsNativeTools(turn.toolScope) && !support.toolScopeSessionParams) {
1202
+ throw new Error(`${support.displayName}: native tool selection is not supported by this engine. Keep native:* in the selection or choose a supported engine.`);
1203
+ }
1032
1204
  const { threadId } = turn;
1033
1205
  if (active.has(threadId))
1034
1206
  throw new Error("a turn is already running on this thread");
@@ -1059,7 +1231,7 @@ export function createAcpDriver(support) {
1059
1231
  }
1060
1232
  const resolvedModel = support.resolveTurnModel?.(turn.model, env);
1061
1233
  support.applyTurnEnv?.(env, {
1062
- model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd,
1234
+ model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd, toolScope: turn.toolScope,
1063
1235
  });
1064
1236
  // Rebound once, before the prompt, when a fallbackModel support swaps
1065
1237
  // a model this session does not offer for one it does.
@@ -1103,6 +1275,8 @@ export function createAcpDriver(support) {
1103
1275
  // riding a child spawned under the old env. Hash it so secrets
1104
1276
  // never sit in the key itself.
1105
1277
  const envFingerprint = createHash("sha256").update(JSON.stringify(spawnEnv)).digest("hex").slice(0, 16);
1278
+ const inheritedScopeFingerprint = narrowsNativeTools(turn.toolScope) && support.toolScopeCacheKey
1279
+ ? createHash("sha256").update(support.toolScopeCacheKey({ config: turnConfig, env, cwd })).digest("hex") : null;
1106
1280
  // A support that applies the approval mode to the session on every
1107
1281
  // turn gets the same process whatever the mode: an approval change
1108
1282
  // is one RPC, not a cold start.
@@ -1110,6 +1284,8 @@ export function createAcpDriver(support) {
1110
1284
  launch.command, launch.args ?? [], spawnArgs, cwd,
1111
1285
  support.sessionScopedApproval ? null : turnConfig.fullAuto === true, envFingerprint,
1112
1286
  support.spawnFingerprint?.(spawnEnv) ?? null,
1287
+ turn.toolScope ?? null,
1288
+ inheritedScopeFingerprint,
1113
1289
  ]);
1114
1290
  // The new-session default model rides the spawn env but stays out of
1115
1291
  // the fingerprint above: only session/new reads it, and a pooled
@@ -1128,7 +1304,7 @@ export function createAcpDriver(support) {
1128
1304
  launchedThisTurn = true;
1129
1305
  return opened;
1130
1306
  };
1131
- const sessionKey = JSON.stringify(mcpServers);
1307
+ const sessionKey = JSON.stringify([mcpServers, turn.toolScope ?? null, inheritedScopeFingerprint]);
1132
1308
  if (turn.sessionReset) {
1133
1309
  closeSession(threadId, "reset");
1134
1310
  sessionAllows.delete(threadId);
@@ -1159,7 +1335,7 @@ export function createAcpDriver(support) {
1159
1335
  // respawns the child, every wire call must reach the live record, so
1160
1336
  // nothing captures the connection off it.
1161
1337
  const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
1162
- const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false };
1338
+ const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false, usagePeak: null, usageLast: null, usageCompacted: false };
1163
1339
  let acknowledge = () => { };
1164
1340
  let rejectStartup = (_error) => { };
1165
1341
  const startupAck = turn.startupRecovery ? new Promise((resolve, reject) => {
@@ -1226,7 +1402,9 @@ export function createAcpDriver(support) {
1226
1402
  acknowledge,
1227
1403
  asks,
1228
1404
  runningTools: new Set(),
1405
+ unkeyedToolIds: [],
1229
1406
  interruptTimer: null,
1407
+ stopQuietWatch: null,
1230
1408
  flushAssistantText,
1231
1409
  receiveModelVariants,
1232
1410
  };
@@ -1252,6 +1430,7 @@ export function createAcpDriver(support) {
1252
1430
  emit({ ...base(threadId, turnId), type: "turn.started" });
1253
1431
  session.current = current;
1254
1432
  (async () => {
1433
+ let pendingSplitReceipt = null;
1255
1434
  try {
1256
1435
  // The handshake is paid once per process, not once per turn. It
1257
1436
  // is a function so the establishment retry below can pay it
@@ -1334,10 +1513,12 @@ export function createAcpDriver(support) {
1334
1513
  // an agent that advertised its transport, so an agent without
1335
1514
  // http/sse never sees an entry it would refuse the session over
1336
1515
  const sessionServers = mcpServers.filter((server) => !("type" in server) || init?.agentCapabilities?.mcpCapabilities?.[server.type] === true);
1516
+ const selectionParams = narrowsNativeTools(turn.toolScope)
1517
+ ? support.toolScopeSessionParams(turn, init, sessionServers.length > 0, { config: turnConfig, env, cwd }) : {};
1337
1518
  let loaded = false;
1338
1519
  if (cursor) {
1339
1520
  try {
1340
- await request(support.resumeMethod === "resume" ? "session/resume" : "session/load", { sessionId: cursor, cwd, mcpServers: sessionServers }, LOAD_SESSION_TIMEOUT, (result) => {
1521
+ await request(support.resumeMethod === "resume" ? "session/resume" : "session/load", { sessionId: cursor, cwd, mcpServers: sessionServers, ...selectionParams }, LOAD_SESSION_TIMEOUT, (result) => {
1341
1522
  if (result) {
1342
1523
  loaded = true;
1343
1524
  session.sessionId = cursor;
@@ -1393,7 +1574,7 @@ export function createAcpDriver(support) {
1393
1574
  promptTurn = { ...turn, text: recovery.text };
1394
1575
  rebuiltFromReplay = recovery.replayed;
1395
1576
  }
1396
- sessionResult = await request("session/new", { cwd, mcpServers: sessionServers }, NEW_SESSION_TIMEOUT, (result) => {
1577
+ sessionResult = await request("session/new", { cwd, mcpServers: sessionServers, ...selectionParams }, NEW_SESSION_TIMEOUT, (result) => {
1397
1578
  session.sessionId = typeof result?.sessionId === "string" ? result.sessionId : null;
1398
1579
  session.sessionKey = sessionKey;
1399
1580
  receiveModelVariants(result);
@@ -1479,6 +1660,7 @@ export function createAcpDriver(support) {
1479
1660
  sessionModels: Array.isArray(sessionResult?.models?.availableModels)
1480
1661
  ? sessionResult.models.availableModels
1481
1662
  : [],
1663
+ currentModelId: session.sessionConfigResult?.models?.currentModelId,
1482
1664
  });
1483
1665
  approvalUnconfirmed = false;
1484
1666
  // initialize's currentModelId is the CLI default,
@@ -1512,12 +1694,15 @@ export function createAcpDriver(support) {
1512
1694
  // adapter call) keeps the legacy full-prompt shape.
1513
1695
  const halves = promptHalves(turn);
1514
1696
  let promptInput = promptTurn;
1515
- let pendingSplitReceipt = null;
1516
1697
  if (halves.stable !== null) {
1517
1698
  const receiptKey = JSON.stringify([threadId, sessionId]);
1518
- const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
1699
+ const previousReceipt = readPromptSplitReceipt(DRIVER_KIND, receiptKey);
1700
+ // Carry the high-water mark, not just the final report: several
1701
+ // ordinary dips across turns can add up to a compaction.
1702
+ state.usagePeak = previousReceipt?.peakUsed ?? previousReceipt?.lastUsed ?? null;
1703
+ const composed = splitSessionPrompt(halves.stable, halves.volatile, previousReceipt, promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
1519
1704
  promptInput = { ...promptTurn, system: "", text: composed.text };
1520
- pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
1705
+ pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt, previous: previousReceipt };
1521
1706
  }
1522
1707
  const text = support.buildPromptText
1523
1708
  ? support.buildPromptText(promptInput)
@@ -1538,13 +1723,32 @@ export function createAcpDriver(support) {
1538
1723
  state.promptSent = true;
1539
1724
  acknowledge();
1540
1725
  const promptIdleMs = promptIdleTimeoutMs();
1541
- 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. ` +
1542
- "Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).");
1726
+ const quiet = startQuietWatch(threadId, session, current);
1727
+ const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, () => {
1728
+ const last = quiet.lastState();
1729
+ return `${DRIVER_KIND} sent nothing for ${formatQuietLimit(promptIdleMs)} with no tool running, so the turn was stopped as stuck` +
1730
+ `${last ? ` (last seen: ${lastSeenPhrase(last)})` : ""}. ` +
1731
+ "Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).";
1732
+ });
1543
1733
  if (pendingSplitReceipt) {
1544
1734
  // session/prompt resolving is the acceptance boundary: a
1545
1735
  // rejected prompt leaves the receipt unwritten, so the next
1546
- // turn redelivers what this one never received.
1547
- writePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key, pendingSplitReceipt.receipt);
1736
+ // turn redelivers what this one never received. After a
1737
+ // compaction, drop it so the next turn re-sends the full prompt.
1738
+ if (state.usageCompacted) {
1739
+ deletePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key);
1740
+ }
1741
+ else {
1742
+ const previousLastUsed = typeof pendingSplitReceipt.previous?.lastUsed === "number"
1743
+ ? pendingSplitReceipt.previous.lastUsed
1744
+ : undefined;
1745
+ const lastUsed = state.usageLast ?? previousLastUsed;
1746
+ writePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key, {
1747
+ ...pendingSplitReceipt.receipt,
1748
+ ...(lastUsed === undefined ? {} : { lastUsed }),
1749
+ ...(state.usagePeak === null ? {} : { peakUsed: state.usagePeak }),
1750
+ });
1751
+ }
1548
1752
  }
1549
1753
  // opencode 1.18.18 reports usage at the result root; grok and
1550
1754
  // gemini put it under _meta. Read both rather than lose the count.
@@ -1597,6 +1801,11 @@ export function createAcpDriver(support) {
1597
1801
  }
1598
1802
  }
1599
1803
  catch (e) {
1804
+ // A rejected prompt can still have compacted native history. Its
1805
+ // old receipt must not suppress the next turn's standing rules.
1806
+ if (pendingSplitReceipt && state.promptSent && state.usageCompacted) {
1807
+ deletePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key);
1808
+ }
1600
1809
  if (!state.settled) {
1601
1810
  const message = e instanceof Error ? e.message : String(e);
1602
1811
  const code = support.classifyError?.(e);
@@ -30,9 +30,21 @@ export function resolveCursorAcpModelId(available, wanted) {
30
30
  const exact = ids.find((m) => m.modelId.toLowerCase() === want);
31
31
  if (exact)
32
32
  return exact.modelId;
33
- const byBase = ids.find((m) => base(m.modelId) === want);
33
+ // A base can be advertised several times with different parameters, e.g.
34
+ // `composer-2.5[fast=false]` and `composer-2.5[fast=true]`. The plain slug
35
+ // means Standard, so prefer the non-fast variant; a `-fast` slug has no base
36
+ // of its own and means the fast=true variant of the base without the suffix.
37
+ const isFast = (id) => /[[,]\s*fast\s*=\s*true\s*[\],]/i.test(id);
38
+ const sameBase = ids.filter((m) => base(m.modelId) === want);
39
+ const byBase = sameBase.find((m) => !isFast(m.modelId)) ?? sameBase[0];
34
40
  if (byBase)
35
41
  return byBase.modelId;
42
+ if (want.endsWith("-fast")) {
43
+ const stem = want.slice(0, -"-fast".length);
44
+ const fast = ids.find((m) => base(m.modelId) === stem && isFast(m.modelId));
45
+ if (fast)
46
+ return fast.modelId;
47
+ }
36
48
  const byName = ids.find((m) => (m.name ?? "").trim().toLowerCase() === want);
37
49
  if (byName)
38
50
  return byName.modelId;