openmausbot 0.1.91 → 0.1.93
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-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- 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-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -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 +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.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.
|
|
@@ -52,6 +53,10 @@ import { redactSecretsInText } from "../../redact.js";
|
|
|
52
53
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
53
54
|
import { sessionIdlePolicy } from "../session-idle.js";
|
|
54
55
|
import { classifyError } from "../retry.js";
|
|
56
|
+
const ACCOUNT_ERROR_CODES = new Set([
|
|
57
|
+
"invalid_credentials", "inactive_subscription", "insufficient_funds", "quota_or_region_restriction",
|
|
58
|
+
]);
|
|
59
|
+
const isAccountError = (code) => code !== undefined && ACCOUNT_ERROR_CODES.has(code);
|
|
55
60
|
/** ACP vendors put the actionable cause in error.data while keeping the
|
|
56
61
|
* JSON-RPC message generic. Only surface known text fields, never a response
|
|
57
62
|
* body/config dump, and redact before bounding the displayed diagnostic. */
|
|
@@ -83,8 +88,13 @@ const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 3
|
|
|
83
88
|
const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
|
|
84
89
|
const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
|
|
85
90
|
/** ACP agents may compact their own history without telling the client;
|
|
86
|
-
* re-send the full prompt after this many bare turns as a backstop
|
|
91
|
+
* re-send the full prompt after this many bare turns as a backstop to
|
|
92
|
+
* compaction detection. */
|
|
87
93
|
const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
94
|
+
/** A reported context below this share of the peak marks a compaction.
|
|
95
|
+
* OpenCode compactions measured 30-52% of the peak; ordinary dips stay
|
|
96
|
+
* above 83%. A false alarm costs one extra full prompt. */
|
|
97
|
+
const ACP_COMPACTION_COLLAPSE_RATIO = 0.6;
|
|
88
98
|
// Read lazily (not at import) so a test can shorten the window. Unlike the
|
|
89
99
|
// setup calls above, session/prompt legitimately streams for minutes, so a
|
|
90
100
|
// wall-clock deadline would false-positive: this guard only trips when the
|
|
@@ -92,13 +102,23 @@ const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
|
92
102
|
// streams thought chunks, then goes silent forever and never resolves). 0
|
|
93
103
|
// disables the guard, restoring the pre-fix "hang until the user cancels"
|
|
94
104
|
// behavior.
|
|
105
|
+
//
|
|
106
|
+
// Healthy agents go silent for minutes too. Qwen Code 0.24 sends nothing
|
|
107
|
+
// over ACP while it compresses history, while it backs off a rate limit
|
|
108
|
+
// (60 s up to 5 min per wait), or while one model request runs (its SDK
|
|
109
|
+
// waits up to 600 s). The old 180 s default killed those turns mid-work,
|
|
110
|
+
// and the retry redid it all. A false trip costs the whole turn; a real
|
|
111
|
+
// wedge only costs waiting, and the user can press Stop - so the default
|
|
112
|
+
// sits above the longest normal silence.
|
|
113
|
+
export const DEFAULT_ACP_PROMPT_IDLE_MS = 15 * 60_000;
|
|
95
114
|
const promptIdleTimeoutMs = () => {
|
|
96
115
|
const raw = process.env.OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS;
|
|
97
116
|
if (raw === undefined)
|
|
98
|
-
return
|
|
117
|
+
return DEFAULT_ACP_PROMPT_IDLE_MS;
|
|
99
118
|
const ms = Number(raw);
|
|
100
119
|
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
101
120
|
};
|
|
121
|
+
const formatQuietLimit = (ms) => ms >= 120_000 && ms % 60_000 === 0 ? `${ms / 60_000} min` : `${Math.round(ms / 1000)} s`;
|
|
102
122
|
/** Keep a turn's set of running tool calls in step with the agent's
|
|
103
123
|
* `tool_call` / `tool_call_update` notifications: a call is running from
|
|
104
124
|
* the first update that is not terminal until one that is. A `tool_call`
|
|
@@ -141,6 +161,24 @@ function acpVariantOption(result) {
|
|
|
141
161
|
};
|
|
142
162
|
}
|
|
143
163
|
const TOOL_LOG_TEXT_LIMIT = 64_000;
|
|
164
|
+
/** Every model id a session config snapshot offers under `configId`,
|
|
165
|
+
* flattening grouped options. Empty when the agent listed none. */
|
|
166
|
+
export function offeredModels(result, configId) {
|
|
167
|
+
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === configId);
|
|
168
|
+
const ids = [];
|
|
169
|
+
const visit = (entries) => {
|
|
170
|
+
if (!Array.isArray(entries))
|
|
171
|
+
return;
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (typeof entry?.value === "string")
|
|
174
|
+
ids.push(entry.value);
|
|
175
|
+
else if (Array.isArray(entry?.options))
|
|
176
|
+
visit(entry.options);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
visit(option?.options);
|
|
180
|
+
return ids;
|
|
181
|
+
}
|
|
144
182
|
async function readAcpImageBlocks(images) {
|
|
145
183
|
return Promise.all(images.map(async (image) => ({
|
|
146
184
|
type: "image",
|
|
@@ -250,6 +288,11 @@ export function versionAtLeast(installed, floor) {
|
|
|
250
288
|
return true;
|
|
251
289
|
}
|
|
252
290
|
export function createAcpDriver(support) {
|
|
291
|
+
// Without a per-turn configureSession nothing would ever apply the mode
|
|
292
|
+
// that sessionScopedApproval keeps out of the spawn contract.
|
|
293
|
+
if (support.sessionScopedApproval && !support.configureSession) {
|
|
294
|
+
throw new Error(`${support.displayName}: sessionScopedApproval needs configureSession to apply each turn's mode`);
|
|
295
|
+
}
|
|
253
296
|
const DRIVER_KIND = support.driverKind;
|
|
254
297
|
const SOURCE = support.nativeSource;
|
|
255
298
|
const decodeConfig = decodeAcpConfig(support.defaultCli);
|
|
@@ -289,13 +332,18 @@ export function createAcpDriver(support) {
|
|
|
289
332
|
return env;
|
|
290
333
|
};
|
|
291
334
|
let models = support.models;
|
|
335
|
+
// Models this account itself offered (see sessionModelEnv): the
|
|
336
|
+
// discovered catalog, never the static one, or a live session's list.
|
|
337
|
+
let accountModels = null;
|
|
292
338
|
const refreshModels = async () => {
|
|
293
339
|
if (!support.resolveModels)
|
|
294
340
|
return;
|
|
295
341
|
try {
|
|
296
342
|
const resolved = await support.resolveModels(childEnv(), config, instanceId);
|
|
297
|
-
if (resolved.options.length)
|
|
343
|
+
if (resolved.options.length) {
|
|
298
344
|
models = resolved;
|
|
345
|
+
accountModels = new Set(resolved.options.map((option) => option.id));
|
|
346
|
+
}
|
|
299
347
|
}
|
|
300
348
|
catch {
|
|
301
349
|
// Keep the last usable catalog when an optional discovery source is down.
|
|
@@ -463,6 +511,101 @@ export function createAcpDriver(support) {
|
|
|
463
511
|
}
|
|
464
512
|
return servers;
|
|
465
513
|
};
|
|
514
|
+
/** While a prompt runs, notice when the agent goes quiet and tell the
|
|
515
|
+
* person what it is doing: retrying, compressing, waiting on its
|
|
516
|
+
* model, busy, or showing no sign of life (see quiet-status.ts). A
|
|
517
|
+
* notice is sent only when that answer changes. New lines in the
|
|
518
|
+
* agent's own log also restart the prompt's idle deadline, so a
|
|
519
|
+
* turn that is visibly retrying is never stopped as stuck. */
|
|
520
|
+
const startQuietWatch = (threadId, session, current) => {
|
|
521
|
+
const noticeAfterMs = envOr("OMB_ACP_QUIET_NOTICE_MS", 60_000);
|
|
522
|
+
const tickMs = envOr("OMB_ACP_QUIET_TICK_MS", 15_000);
|
|
523
|
+
const logPath = support.statusLog && session.sessionId
|
|
524
|
+
? support.statusLog.path(session.env, session.sessionId)
|
|
525
|
+
: null;
|
|
526
|
+
const tail = logPath ? new LogTail(logPath) : null;
|
|
527
|
+
tail?.read(); // skip what this log held before the prompt
|
|
528
|
+
let logged = null;
|
|
529
|
+
let context = null;
|
|
530
|
+
let previous = null;
|
|
531
|
+
let previousAt = 0;
|
|
532
|
+
let shownKey = null;
|
|
533
|
+
let noSignsSince = null;
|
|
534
|
+
let notices = 0;
|
|
535
|
+
let probing = false;
|
|
536
|
+
let state = null;
|
|
537
|
+
const tick = async () => {
|
|
538
|
+
if (probing || current.state.settled || session.current !== current)
|
|
539
|
+
return;
|
|
540
|
+
const now = Date.now();
|
|
541
|
+
for (const line of tail?.read() ?? []) {
|
|
542
|
+
session.acp.touch();
|
|
543
|
+
const signal = support.statusLog.parse(line);
|
|
544
|
+
if (signal?.kind === "context")
|
|
545
|
+
context = { tokens: signal.tokens, threshold: signal.threshold };
|
|
546
|
+
else if (signal)
|
|
547
|
+
logged = { signal, at: now };
|
|
548
|
+
}
|
|
549
|
+
const quietMs = now - session.acp.lastInboundAt;
|
|
550
|
+
// A running tool or an open question is already on screen.
|
|
551
|
+
if (quietMs < noticeAfterMs || current.asks.size || current.runningTools.size) {
|
|
552
|
+
if (quietMs < noticeAfterMs)
|
|
553
|
+
logged = null;
|
|
554
|
+
previous = null;
|
|
555
|
+
shownKey = null;
|
|
556
|
+
noSignsSince = null;
|
|
557
|
+
state = null;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// A retry stays the answer until well past its wait (Qwen's SDK
|
|
561
|
+
// retries on its own for about a minute before the next logged
|
|
562
|
+
// attempt); after that the probe takes over.
|
|
563
|
+
if (logged?.signal.kind === "retry"
|
|
564
|
+
&& now - logged.at > (logged.signal.delayMs ?? 60_000) + 120_000)
|
|
565
|
+
logged = null;
|
|
566
|
+
probing = true;
|
|
567
|
+
const sample = session.child.pid ? await sampleProcessTree(session.child.pid) : { cpuMs: null, connections: null };
|
|
568
|
+
probing = false;
|
|
569
|
+
if (current.state.settled || session.current !== current)
|
|
570
|
+
return;
|
|
571
|
+
let next = classifyQuiet({
|
|
572
|
+
logged: logged?.signal ?? null, context, sample, previous, sinceMs: previous ? now - previousAt : 0,
|
|
573
|
+
});
|
|
574
|
+
const first = previous === null;
|
|
575
|
+
previous = sample;
|
|
576
|
+
previousAt = now;
|
|
577
|
+
// CPU use needs two samples; the first only sets the baseline
|
|
578
|
+
// (unless the log already said what is going on).
|
|
579
|
+
if (first && next.kind === "unknown")
|
|
580
|
+
return;
|
|
581
|
+
// An SDK sleeping between its own retries holds no socket and
|
|
582
|
+
// burns no CPU (seen with Qwen against a 429ing endpoint), so
|
|
583
|
+
// "no sign of life" is called only once that has lasted minutes.
|
|
584
|
+
if (next.kind === "no-signs") {
|
|
585
|
+
noSignsSince ??= now;
|
|
586
|
+
if (now - noSignsSince < envOr("OMB_ACP_STUCK_AFTER_MS", 180_000))
|
|
587
|
+
next = { kind: "between-requests" };
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
noSignsSince = null;
|
|
591
|
+
}
|
|
592
|
+
state = next;
|
|
593
|
+
const key = quietKey(next);
|
|
594
|
+
if (key === shownKey || notices >= 12)
|
|
595
|
+
return;
|
|
596
|
+
shownKey = key;
|
|
597
|
+
notices += 1;
|
|
598
|
+
emit({
|
|
599
|
+
...base(threadId, current.turnId),
|
|
600
|
+
type: "runtime.notice",
|
|
601
|
+
message: describeQuiet(support.displayName, next, quietMs, promptIdleTimeoutMs()),
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
const timer = setInterval(() => { void tick(); }, tickMs);
|
|
605
|
+
timer.unref?.();
|
|
606
|
+
current.stopQuietWatch = () => clearInterval(timer);
|
|
607
|
+
return { lastState: () => state };
|
|
608
|
+
};
|
|
466
609
|
/** The one completion path for a turn: the prompt result, a crashed
|
|
467
610
|
* child, an unanswered cancel, or an rpc error the turn body throws.
|
|
468
611
|
* The child is NOT killed here — a clean settle leaves it pooled for
|
|
@@ -473,6 +616,7 @@ export function createAcpDriver(support) {
|
|
|
473
616
|
return;
|
|
474
617
|
current.state.settled = true;
|
|
475
618
|
current.acknowledge();
|
|
619
|
+
current.stopQuietWatch?.();
|
|
476
620
|
if (current.interruptTimer)
|
|
477
621
|
clearTimeout(current.interruptTimer);
|
|
478
622
|
for (const finish of current.asks.values())
|
|
@@ -565,7 +709,7 @@ export function createAcpDriver(support) {
|
|
|
565
709
|
return;
|
|
566
710
|
}
|
|
567
711
|
rpcPending.delete(id);
|
|
568
|
-
const error = new Error(idleMessage ?? `${method} stopped responding`);
|
|
712
|
+
const error = new Error((typeof idleMessage === "function" ? idleMessage() : idleMessage) ?? `${method} stopped responding`);
|
|
569
713
|
Object.assign(error, { acpPromptStall: true });
|
|
570
714
|
reject(error);
|
|
571
715
|
}, idleMs);
|
|
@@ -584,9 +728,13 @@ export function createAcpDriver(support) {
|
|
|
584
728
|
});
|
|
585
729
|
send({ jsonrpc: "2.0", id, method, params });
|
|
586
730
|
});
|
|
731
|
+
let lastInboundAt = Date.now();
|
|
587
732
|
const acp = {
|
|
588
733
|
send,
|
|
589
734
|
request,
|
|
735
|
+
get lastInboundAt() { return lastInboundAt; },
|
|
736
|
+
touch: () => { for (const p of rpcPending.values())
|
|
737
|
+
p.armIdle(); },
|
|
590
738
|
failAll: (error) => {
|
|
591
739
|
for (const p of rpcPending.values()) {
|
|
592
740
|
if (p.timer)
|
|
@@ -884,12 +1032,26 @@ export function createAcpDriver(support) {
|
|
|
884
1032
|
}
|
|
885
1033
|
break;
|
|
886
1034
|
}
|
|
1035
|
+
case "usage_update": {
|
|
1036
|
+
// opencode 1.18 sends `used` flat; ignore non-positive reports
|
|
1037
|
+
const used = u.used ?? u.usage?.used;
|
|
1038
|
+
if (typeof used !== "number" || !(used > 0))
|
|
1039
|
+
break;
|
|
1040
|
+
if (current.state.usagePeak !== null && used < current.state.usagePeak * ACP_COMPACTION_COLLAPSE_RATIO) {
|
|
1041
|
+
current.state.usageCompacted = true;
|
|
1042
|
+
}
|
|
1043
|
+
if (current.state.usagePeak === null || used > current.state.usagePeak)
|
|
1044
|
+
current.state.usagePeak = used;
|
|
1045
|
+
current.state.usageLast = used;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
887
1048
|
}
|
|
888
1049
|
};
|
|
889
1050
|
const session = {
|
|
890
1051
|
child,
|
|
891
1052
|
acp,
|
|
892
1053
|
launch,
|
|
1054
|
+
env,
|
|
893
1055
|
cwd,
|
|
894
1056
|
contractKey,
|
|
895
1057
|
sessionKey: null,
|
|
@@ -925,6 +1087,7 @@ export function createAcpDriver(support) {
|
|
|
925
1087
|
appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(msg) });
|
|
926
1088
|
// Inbound traffic proves the child is alive and making progress,
|
|
927
1089
|
// so every idle deadline restarts; only total silence trips it.
|
|
1090
|
+
lastInboundAt = Date.now();
|
|
928
1091
|
for (const p of rpcPending.values())
|
|
929
1092
|
p.armIdle();
|
|
930
1093
|
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
@@ -1026,8 +1189,12 @@ export function createAcpDriver(support) {
|
|
|
1026
1189
|
return { turnId };
|
|
1027
1190
|
}
|
|
1028
1191
|
const resolvedModel = support.resolveTurnModel?.(turn.model, env);
|
|
1029
|
-
support.applyTurnEnv?.(env, {
|
|
1030
|
-
|
|
1192
|
+
support.applyTurnEnv?.(env, {
|
|
1193
|
+
model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd,
|
|
1194
|
+
});
|
|
1195
|
+
// Rebound once, before the prompt, when a fallbackModel support swaps
|
|
1196
|
+
// a model this session does not offer for one it does.
|
|
1197
|
+
let cliTurn = resolvedModel !== undefined && resolvedModel !== turn.model
|
|
1031
1198
|
? { ...turn, model: resolvedModel }
|
|
1032
1199
|
: turn;
|
|
1033
1200
|
const mcpServers = acpMcpServers(turn);
|
|
@@ -1067,7 +1234,31 @@ export function createAcpDriver(support) {
|
|
|
1067
1234
|
// riding a child spawned under the old env. Hash it so secrets
|
|
1068
1235
|
// never sit in the key itself.
|
|
1069
1236
|
const envFingerprint = createHash("sha256").update(JSON.stringify(spawnEnv)).digest("hex").slice(0, 16);
|
|
1070
|
-
|
|
1237
|
+
// A support that applies the approval mode to the session on every
|
|
1238
|
+
// turn gets the same process whatever the mode: an approval change
|
|
1239
|
+
// is one RPC, not a cold start.
|
|
1240
|
+
const contractKey = JSON.stringify([
|
|
1241
|
+
launch.command, launch.args ?? [], spawnArgs, cwd,
|
|
1242
|
+
support.sessionScopedApproval ? null : turnConfig.fullAuto === true, envFingerprint,
|
|
1243
|
+
support.spawnFingerprint?.(spawnEnv) ?? null,
|
|
1244
|
+
]);
|
|
1245
|
+
// The new-session default model rides the spawn env but stays out of
|
|
1246
|
+
// the fingerprint above: only session/new reads it, and a pooled
|
|
1247
|
+
// process switches later models over the wire as before.
|
|
1248
|
+
const launchModel = support.sessionModelEnv && cliTurn.model && /^[\w.:/-]{1,200}$/u.test(cliTurn.model)
|
|
1249
|
+
&& accountModels?.has(cliTurn.model) ? cliTurn.model : undefined;
|
|
1250
|
+
const launchEnv = support.sessionModelEnv && launchModel
|
|
1251
|
+
? { ...spawnEnv, [support.sessionModelEnv]: launchModel }
|
|
1252
|
+
: spawnEnv;
|
|
1253
|
+
// whether this turn's process was started by this turn: its model
|
|
1254
|
+
// list is then current, not a pooled process's older one
|
|
1255
|
+
let launchedThisTurn = false;
|
|
1256
|
+
const launchSession = () => {
|
|
1257
|
+
const opened = openSession(threadId, launch, [...(launch.args ?? []), ...spawnArgs], launchEnv, cwd, contractKey);
|
|
1258
|
+
opened.launchModel = launchModel;
|
|
1259
|
+
launchedThisTurn = true;
|
|
1260
|
+
return opened;
|
|
1261
|
+
};
|
|
1071
1262
|
const sessionKey = JSON.stringify(mcpServers);
|
|
1072
1263
|
if (turn.sessionReset) {
|
|
1073
1264
|
closeSession(threadId, "reset");
|
|
@@ -1092,14 +1283,14 @@ export function createAcpDriver(support) {
|
|
|
1092
1283
|
else
|
|
1093
1284
|
closeSession(threadId, "contract");
|
|
1094
1285
|
}
|
|
1095
|
-
session =
|
|
1286
|
+
session = launchSession();
|
|
1096
1287
|
sessions.set(threadId, session);
|
|
1097
1288
|
}
|
|
1098
1289
|
// `session` rebinds mid-turn: when the establishment retry below
|
|
1099
1290
|
// respawns the child, every wire call must reach the live record, so
|
|
1100
1291
|
// nothing captures the connection off it.
|
|
1101
1292
|
const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
|
|
1102
|
-
const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false };
|
|
1293
|
+
const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false, usagePeak: null, usageLast: null, usageCompacted: false };
|
|
1103
1294
|
let acknowledge = () => { };
|
|
1104
1295
|
let rejectStartup = (_error) => { };
|
|
1105
1296
|
const startupAck = turn.startupRecovery ? new Promise((resolve, reject) => {
|
|
@@ -1107,6 +1298,16 @@ export function createAcpDriver(support) {
|
|
|
1107
1298
|
rejectStartup = reject;
|
|
1108
1299
|
}) : null;
|
|
1109
1300
|
const asks = new Map();
|
|
1301
|
+
// What this turn reports and applies. A model fallback (below)
|
|
1302
|
+
// replaces both: a saved variant belongs to the model it replaced.
|
|
1303
|
+
let reportedModel = turn.model;
|
|
1304
|
+
let variant = turn.variant;
|
|
1305
|
+
// set below when this process lists fewer models than the catalog
|
|
1306
|
+
let staleProcess = false;
|
|
1307
|
+
// Set while a sessionScopedApproval support applies this turn's mode.
|
|
1308
|
+
// Still set if that throws: the pooled session may be left on an
|
|
1309
|
+
// earlier turn's looser mode, so its process must not be reused.
|
|
1310
|
+
let approvalUnconfirmed = false;
|
|
1110
1311
|
const modelOf = (result) => {
|
|
1111
1312
|
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === (support.selectModel?.configId ?? "model"));
|
|
1112
1313
|
return typeof option?.currentValue === "string" ? option.currentValue : null;
|
|
@@ -1122,7 +1323,7 @@ export function createAcpDriver(support) {
|
|
|
1122
1323
|
emit({
|
|
1123
1324
|
...base(threadId, turnId),
|
|
1124
1325
|
type: "session.model-variants",
|
|
1125
|
-
model: nativeModel === cliTurn.model ? (
|
|
1326
|
+
model: nativeModel === cliTurn.model ? (reportedModel ?? nativeModel) : nativeModel,
|
|
1126
1327
|
variants: {
|
|
1127
1328
|
options: option?.options ?? [],
|
|
1128
1329
|
...(option?.currentValue !== undefined ? { currentValue: option.currentValue } : {}),
|
|
@@ -1133,8 +1334,8 @@ export function createAcpDriver(support) {
|
|
|
1133
1334
|
if (!support.modelVariants)
|
|
1134
1335
|
throw new Error(`${support.displayName} does not support model variants`);
|
|
1135
1336
|
const option = acpVariantOption(session.sessionConfigResult);
|
|
1136
|
-
if (!option || !option.options.some((entry) => entry.id ===
|
|
1137
|
-
throw new Error(`${support.displayName} does not advertise variant ${
|
|
1337
|
+
if (!option || !option.options.some((entry) => entry.id === variant)) {
|
|
1338
|
+
throw new Error(`${support.displayName} does not advertise variant ${variant} for this model`);
|
|
1138
1339
|
}
|
|
1139
1340
|
return option;
|
|
1140
1341
|
};
|
|
@@ -1157,6 +1358,7 @@ export function createAcpDriver(support) {
|
|
|
1157
1358
|
asks,
|
|
1158
1359
|
runningTools: new Set(),
|
|
1159
1360
|
interruptTimer: null,
|
|
1361
|
+
stopQuietWatch: null,
|
|
1160
1362
|
flushAssistantText,
|
|
1161
1363
|
receiveModelVariants,
|
|
1162
1364
|
};
|
|
@@ -1182,6 +1384,7 @@ export function createAcpDriver(support) {
|
|
|
1182
1384
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1183
1385
|
session.current = current;
|
|
1184
1386
|
(async () => {
|
|
1387
|
+
let pendingSplitReceipt = null;
|
|
1185
1388
|
try {
|
|
1186
1389
|
// The handshake is paid once per process, not once per turn. It
|
|
1187
1390
|
// is a function so the establishment retry below can pay it
|
|
@@ -1304,7 +1507,7 @@ export function createAcpDriver(support) {
|
|
|
1304
1507
|
// session/new on the replacement child.
|
|
1305
1508
|
session.current = null;
|
|
1306
1509
|
closeSession(threadId, "reestablish");
|
|
1307
|
-
session =
|
|
1510
|
+
session = launchSession();
|
|
1308
1511
|
sessions.set(threadId, session);
|
|
1309
1512
|
session.current = current;
|
|
1310
1513
|
runtimeAcceptsImages = await handshake();
|
|
@@ -1352,6 +1555,43 @@ export function createAcpDriver(support) {
|
|
|
1352
1555
|
if (support.selectModel) {
|
|
1353
1556
|
const { configId } = support.selectModel;
|
|
1354
1557
|
selectedModel = modelOf(session.sessionConfigResult);
|
|
1558
|
+
const wanted = cliTurn.model;
|
|
1559
|
+
const offered = offeredModels(session.sessionConfigResult, configId);
|
|
1560
|
+
// A process started without a default model lists exactly
|
|
1561
|
+
// what this account offers.
|
|
1562
|
+
if (support.sessionModelEnv && !session.launchModel && offered.length)
|
|
1563
|
+
accountModels = new Set(offered);
|
|
1564
|
+
// The instance catalog lists the model but a pooled process
|
|
1565
|
+
// does not: the process is older than the catalog (a provider
|
|
1566
|
+
// added since it started). That is no reason to run another
|
|
1567
|
+
// model. The switch below fails with the runtime's own words,
|
|
1568
|
+
// and the process is closed so the retry starts on a fresh
|
|
1569
|
+
// one. A process this turn started is current, so there the
|
|
1570
|
+
// catalog is the stale one and the fallback applies.
|
|
1571
|
+
staleProcess = Boolean(support.fallbackModel && wanted && !launchedThisTurn && offered.length
|
|
1572
|
+
&& !offered.includes(wanted) && models.options.some((option) => option.id === wanted));
|
|
1573
|
+
// A local inject writes its provider for this turn, so a
|
|
1574
|
+
// process started earlier may not list it yet: never replace it.
|
|
1575
|
+
if (support.fallbackModel && offered.length && !skipSubscriptionAuthForLocalInject(turn.model)
|
|
1576
|
+
&& !staleProcess && (!wanted || !offered.includes(wanted))) {
|
|
1577
|
+
const fallback = support.fallbackModel(session.sessionConfigResult, models.default);
|
|
1578
|
+
if (fallback && fallback !== wanted && offered.includes(fallback)) {
|
|
1579
|
+
const notice = wanted ? `${wanted}\n${fallback}` : null;
|
|
1580
|
+
if (notice && session.fallbackNotice !== notice) {
|
|
1581
|
+
session.fallbackNotice = notice;
|
|
1582
|
+
emit({
|
|
1583
|
+
...base(threadId, turnId),
|
|
1584
|
+
type: "runtime.notice",
|
|
1585
|
+
message: `${support.displayName} no longer offers ${wanted}, so this conversation uses ${fallback}. ` +
|
|
1586
|
+
"Choose another model for this bot to stop seeing this.",
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
cliTurn = { ...cliTurn, model: fallback };
|
|
1590
|
+
current.turn = cliTurn;
|
|
1591
|
+
reportedModel = fallback;
|
|
1592
|
+
variant = undefined;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1355
1595
|
if (cliTurn.model && cliTurn.model !== selectedModel) {
|
|
1356
1596
|
sessionResult = await request("session/set_config_option", { sessionId, configId, value: cliTurn.model }, INIT_TIMEOUT, receiveModelVariants);
|
|
1357
1597
|
selectedModel = modelOf(session.sessionConfigResult);
|
|
@@ -1363,6 +1603,7 @@ export function createAcpDriver(support) {
|
|
|
1363
1603
|
}
|
|
1364
1604
|
}
|
|
1365
1605
|
if (support.configureSession) {
|
|
1606
|
+
approvalUnconfirmed = support.sessionScopedApproval === true;
|
|
1366
1607
|
await support.configureSession({
|
|
1367
1608
|
request: (method, params, timeoutMs) => request(method, params, timeoutMs ?? SESSION_CONFIG_TIMEOUT),
|
|
1368
1609
|
sessionId,
|
|
@@ -1372,17 +1613,18 @@ export function createAcpDriver(support) {
|
|
|
1372
1613
|
? sessionResult.models.availableModels
|
|
1373
1614
|
: [],
|
|
1374
1615
|
});
|
|
1616
|
+
approvalUnconfirmed = false;
|
|
1375
1617
|
// initialize's currentModelId is the CLI default,
|
|
1376
1618
|
// not the model this turn asked for. After a successful pin,
|
|
1377
1619
|
// report the slug we set so the UI does not claim otherwise.
|
|
1378
1620
|
if (!selectedModel && cliTurn.model)
|
|
1379
1621
|
selectedModel = cliTurn.model;
|
|
1380
1622
|
}
|
|
1381
|
-
if (
|
|
1623
|
+
if (variant !== undefined) {
|
|
1382
1624
|
const option = requestedVariantOption();
|
|
1383
|
-
await request("session/set_config_option", { sessionId, configId: option.configId, value:
|
|
1384
|
-
if (requestedVariantOption().currentValue !==
|
|
1385
|
-
throw new Error(`${support.displayName} did not apply variant ${
|
|
1625
|
+
await request("session/set_config_option", { sessionId, configId: option.configId, value: variant }, SESSION_CONFIG_TIMEOUT, receiveModelVariants);
|
|
1626
|
+
if (requestedVariantOption().currentValue !== variant) {
|
|
1627
|
+
throw new Error(`${support.displayName} did not apply variant ${variant}`);
|
|
1386
1628
|
}
|
|
1387
1629
|
}
|
|
1388
1630
|
}
|
|
@@ -1403,12 +1645,15 @@ export function createAcpDriver(support) {
|
|
|
1403
1645
|
// adapter call) keeps the legacy full-prompt shape.
|
|
1404
1646
|
const halves = promptHalves(turn);
|
|
1405
1647
|
let promptInput = promptTurn;
|
|
1406
|
-
let pendingSplitReceipt = null;
|
|
1407
1648
|
if (halves.stable !== null) {
|
|
1408
1649
|
const receiptKey = JSON.stringify([threadId, sessionId]);
|
|
1409
|
-
const
|
|
1650
|
+
const previousReceipt = readPromptSplitReceipt(DRIVER_KIND, receiptKey);
|
|
1651
|
+
// Carry the high-water mark, not just the final report: several
|
|
1652
|
+
// ordinary dips across turns can add up to a compaction.
|
|
1653
|
+
state.usagePeak = previousReceipt?.peakUsed ?? previousReceipt?.lastUsed ?? null;
|
|
1654
|
+
const composed = splitSessionPrompt(halves.stable, halves.volatile, previousReceipt, promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
|
|
1410
1655
|
promptInput = { ...promptTurn, system: "", text: composed.text };
|
|
1411
|
-
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
1656
|
+
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt, previous: previousReceipt };
|
|
1412
1657
|
}
|
|
1413
1658
|
const text = support.buildPromptText
|
|
1414
1659
|
? support.buildPromptText(promptInput)
|
|
@@ -1421,7 +1666,7 @@ export function createAcpDriver(support) {
|
|
|
1421
1666
|
if (support.modelVariants && cliTurn.model && modelOf(session.sessionConfigResult) !== cliTurn.model) {
|
|
1422
1667
|
throw new Error(`${support.displayName} changed model before the prompt`);
|
|
1423
1668
|
}
|
|
1424
|
-
if (
|
|
1669
|
+
if (variant !== undefined && requestedVariantOption().currentValue !== variant) {
|
|
1425
1670
|
throw new Error(`${support.displayName} changed variant before the prompt`);
|
|
1426
1671
|
}
|
|
1427
1672
|
if (state.settled || state.stopped || session.closing)
|
|
@@ -1429,23 +1674,62 @@ export function createAcpDriver(support) {
|
|
|
1429
1674
|
state.promptSent = true;
|
|
1430
1675
|
acknowledge();
|
|
1431
1676
|
const promptIdleMs = promptIdleTimeoutMs();
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1677
|
+
const quiet = startQuietWatch(threadId, session, current);
|
|
1678
|
+
const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, () => {
|
|
1679
|
+
const last = quiet.lastState();
|
|
1680
|
+
return `${DRIVER_KIND} sent nothing for ${formatQuietLimit(promptIdleMs)} with no tool running, so the turn was stopped as stuck` +
|
|
1681
|
+
`${last ? ` (last seen: ${lastSeenPhrase(last)})` : ""}. ` +
|
|
1682
|
+
"Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).";
|
|
1683
|
+
});
|
|
1434
1684
|
if (pendingSplitReceipt) {
|
|
1435
1685
|
// session/prompt resolving is the acceptance boundary: a
|
|
1436
1686
|
// rejected prompt leaves the receipt unwritten, so the next
|
|
1437
|
-
// turn redelivers what this one never received.
|
|
1438
|
-
|
|
1687
|
+
// turn redelivers what this one never received. After a
|
|
1688
|
+
// compaction, drop it so the next turn re-sends the full prompt.
|
|
1689
|
+
if (state.usageCompacted) {
|
|
1690
|
+
deletePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key);
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
const previousLastUsed = typeof pendingSplitReceipt.previous?.lastUsed === "number"
|
|
1694
|
+
? pendingSplitReceipt.previous.lastUsed
|
|
1695
|
+
: undefined;
|
|
1696
|
+
const lastUsed = state.usageLast ?? previousLastUsed;
|
|
1697
|
+
writePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key, {
|
|
1698
|
+
...pendingSplitReceipt.receipt,
|
|
1699
|
+
...(lastUsed === undefined ? {} : { lastUsed }),
|
|
1700
|
+
...(state.usagePeak === null ? {} : { peakUsed: state.usagePeak }),
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1439
1703
|
}
|
|
1440
1704
|
// opencode 1.18.18 reports usage at the result root; grok and
|
|
1441
1705
|
// gemini put it under _meta. Read both rather than lose the count.
|
|
1442
1706
|
const usage = result?.usage ?? result?._meta ?? {};
|
|
1443
1707
|
if (typeof usage.inputTokens === "number" || typeof usage.outputTokens === "number") {
|
|
1708
|
+
// opencode 1.18.25 keeps only the uncached share in inputTokens
|
|
1709
|
+
// and reports cache reads beside it (omitted when 0); some
|
|
1710
|
+
// agents count them inside inputTokens. `input` is the whole
|
|
1711
|
+
// prompt with `cachedInput` naming its cached part — the same
|
|
1712
|
+
// convention as the Claude driver and the store — and
|
|
1713
|
+
// totalTokens tells the two wire shapes apart. Cache writes
|
|
1714
|
+
// stay out of `input`, exactly as in the Claude driver, and an
|
|
1715
|
+
// absent cache count emits the bare shape with no cachedInput
|
|
1716
|
+
// key: absent is not zero.
|
|
1717
|
+
const input = usage.inputTokens ?? 0;
|
|
1718
|
+
const output = usage.outputTokens ?? 0;
|
|
1719
|
+
const cachedRead = typeof usage.cachedReadTokens === "number" && usage.cachedReadTokens > 0
|
|
1720
|
+
? usage.cachedReadTokens
|
|
1721
|
+
: null;
|
|
1722
|
+
const exclusive = cachedRead !== null
|
|
1723
|
+
&& (typeof usage.totalTokens !== "number"
|
|
1724
|
+
|| usage.totalTokens >= input + output + cachedRead);
|
|
1444
1725
|
emit({
|
|
1445
1726
|
...base(threadId, turnId),
|
|
1446
1727
|
type: "thread.token-usage.updated",
|
|
1447
|
-
input:
|
|
1448
|
-
output
|
|
1728
|
+
input: cachedRead !== null && exclusive ? input + cachedRead : input,
|
|
1729
|
+
output,
|
|
1730
|
+
...(cachedRead !== null
|
|
1731
|
+
? { cachedInput: exclusive ? cachedRead : Math.min(cachedRead, input) }
|
|
1732
|
+
: {}),
|
|
1449
1733
|
});
|
|
1450
1734
|
}
|
|
1451
1735
|
const reason = result?.stopReason;
|
|
@@ -1468,6 +1752,11 @@ export function createAcpDriver(support) {
|
|
|
1468
1752
|
}
|
|
1469
1753
|
}
|
|
1470
1754
|
catch (e) {
|
|
1755
|
+
// A rejected prompt can still have compacted native history. Its
|
|
1756
|
+
// old receipt must not suppress the next turn's standing rules.
|
|
1757
|
+
if (pendingSplitReceipt && state.promptSent && state.usageCompacted) {
|
|
1758
|
+
deletePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key);
|
|
1759
|
+
}
|
|
1471
1760
|
if (!state.settled) {
|
|
1472
1761
|
const message = e instanceof Error ? e.message : String(e);
|
|
1473
1762
|
const code = support.classifyError?.(e);
|
|
@@ -1475,12 +1764,19 @@ export function createAcpDriver(support) {
|
|
|
1475
1764
|
// classifier is preferred; loginNote remains a compatibility
|
|
1476
1765
|
// fallback for existing ACP supports.
|
|
1477
1766
|
const needsAuth = code === "invalid_credentials" || code === "inactive_subscription"
|
|
1478
|
-
|| message === support.loginNote;
|
|
1767
|
+
|| code === "insufficient_funds" || message === support.loginNote;
|
|
1768
|
+
// A key, credit, quota or subscription problem is the account's,
|
|
1769
|
+
// not the process's: the child that reported it is healthy, so it
|
|
1770
|
+
// stays pooled and the retry after a fix starts warm.
|
|
1771
|
+
const accountError = isAccountError(code);
|
|
1479
1772
|
const failure = classifyError({ text: message });
|
|
1480
1773
|
const transientStartup = failure.transient || (failure.reason === "unknown" && code === "upstream_outage");
|
|
1481
1774
|
const denied = /\b(?:(?:permission|access) denied|(?:approval|permission) (?:required|denied|rejected)|requires? (?:approval|permission)|policy (?:restriction|violation)|(?:blocked|denied|restricted) by (?:the )?policy)\b/i.test(message);
|
|
1482
|
-
|
|
1483
|
-
|
|
1775
|
+
// An unconfirmed approval switch sent no prompt and its process
|
|
1776
|
+
// is discarded either way, so a fresh one can take the turn.
|
|
1777
|
+
const retryableStartup = approvalUnconfirmed || (transientStartup && (!code || code === "upstream_outage")
|
|
1778
|
+
&& ![-32700, -32600, -32601, -32602].includes(e?.code));
|
|
1779
|
+
if (turn.startupRecovery && retryableStartup && !needsAuth && !accountError && !denied
|
|
1484
1780
|
&& !state.promptSent && !state.startupActivity && !state.producedItem && !state.text
|
|
1485
1781
|
&& !state.stopped && !session.closing && !asks.size && !current.runningTools.size) {
|
|
1486
1782
|
// Quiesce before killing: the child's close event must not
|
|
@@ -1508,7 +1804,7 @@ export function createAcpDriver(support) {
|
|
|
1508
1804
|
emit({
|
|
1509
1805
|
...base(threadId, turnId),
|
|
1510
1806
|
type: "runtime.error",
|
|
1511
|
-
message,
|
|
1807
|
+
message: accountError ? support.describeAccountError?.(code, cliTurn.model) ?? message : message,
|
|
1512
1808
|
...(needsAuth ? { setup: true } : {}),
|
|
1513
1809
|
});
|
|
1514
1810
|
// Internal RPC failures can leave a live child poisoned just
|
|
@@ -1516,10 +1812,20 @@ export function createAcpDriver(support) {
|
|
|
1516
1812
|
// start the next turn. Never replay this accepted prompt: it
|
|
1517
1813
|
// may already have executed tools. The next explicit turn can
|
|
1518
1814
|
// resume the recorded session on a fresh process.
|
|
1519
|
-
if (!needsAuth && (e?.acpPromptStall === true || e?.acpSessionFailure === true
|
|
1815
|
+
if (!needsAuth && !accountError && (e?.acpPromptStall === true || e?.acpSessionFailure === true
|
|
1520
1816
|
|| code === "upstream_outage") && session.child.exitCode === null && !session.closing) {
|
|
1521
1817
|
closeSession(threadId, e?.acpPromptStall === true ? "prompt-stall" : "rpc-failure");
|
|
1522
1818
|
}
|
|
1819
|
+
else if (approvalUnconfirmed && session.child.exitCode === null && !session.closing) {
|
|
1820
|
+
// Fail closed: the session may still hold a looser mode from
|
|
1821
|
+
// an earlier turn. Nothing was prompted; the next turn starts
|
|
1822
|
+
// a fresh process, whose session begins in the runtime's own
|
|
1823
|
+
// default mode, and applies its mode there.
|
|
1824
|
+
closeSession(threadId, "approval-unconfirmed");
|
|
1825
|
+
}
|
|
1826
|
+
else if (staleProcess && !state.promptSent && session.child.exitCode === null && !session.closing) {
|
|
1827
|
+
closeSession(threadId, "stale-models");
|
|
1828
|
+
}
|
|
1523
1829
|
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
1524
1830
|
}
|
|
1525
1831
|
}
|