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.
- package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
1542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|