openmausbot 0.1.92 → 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-Do6lBa4C.js → DesktopViewer-CKNR3SEW.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DGBaEXrM.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +6 -1
- package/dist-server/drivers/agents-proxy.js +2 -0
- package/dist-server/index.js +1722 -906
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/mcp-gate.js +5 -1
- package/dist-server/openmausbot.js +763 -93
- package/dist-server/pair-cli.js +763 -93
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +14 -1
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +172 -12
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- 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/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/claude.js +17 -3
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/index.js +210 -57
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/store.js +54 -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/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/vps-container-mcp.js +6 -1
- package/dist-server/workspace-backup.worker.js +14 -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
|
@@ -923,9 +923,9 @@ export async function verifyToken(token) {
|
|
|
923
923
|
return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
|
|
924
924
|
}
|
|
925
925
|
}
|
|
926
|
-
/** A rejected Cloud
|
|
927
|
-
* for them to fix in Settings. */
|
|
928
|
-
const INCLUDED_BOAT_UNAVAILABLE = "
|
|
926
|
+
/** A rejected Cloud relay token: nothing the person pasted, so nothing
|
|
927
|
+
* for them to fix in Settings. Plan-neutral: every Cloud plan includes them. */
|
|
928
|
+
const INCLUDED_BOAT_UNAVAILABLE = "The cloud computers included with your Cloud plan aren't available right now. Try again later.";
|
|
929
929
|
/** Turn a provider refusal into something a person can act on. The
|
|
930
930
|
* provider's own message is better than anything we can invent — it knows
|
|
931
931
|
* the plan, the limit and the link — so prefer it and only fall back to
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { lstatSync, readFileSync } from "node:fs";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import { writeFileAtomic } from "./atomic.js";
|
|
32
|
-
export const CLOUD_PERSONAL_REFUSAL = "Cloud
|
|
32
|
+
export const CLOUD_PERSONAL_REFUSAL = "OMB Cloud is personal: only your own devices can connect.";
|
|
33
33
|
const KEY = /^p_[\w-]{22}$/;
|
|
34
34
|
const keys = z.array(z.string().regex(KEY)).max(100_000);
|
|
35
35
|
const recordFile = z.object({
|
|
@@ -71,6 +71,12 @@ export class ComputerControl {
|
|
|
71
71
|
});
|
|
72
72
|
return { snapshot: this.changed(botId), owned: true, acquired: true };
|
|
73
73
|
}
|
|
74
|
+
/** Whether this workspace lease is the one holding the wheel right now.
|
|
75
|
+
* Read-only: unlike `acquireLease`, a free computer is not taken. */
|
|
76
|
+
ownsLease(botId, controlLeaseId) {
|
|
77
|
+
const entry = this.entries.get(botId);
|
|
78
|
+
return entry?.heldSinceMs != null && entry.controlLeaseId === controlLeaseId;
|
|
79
|
+
}
|
|
74
80
|
/** The person hands the wheel back. Also settles any open help request —
|
|
75
81
|
* the waiting bot resumes from this one state change. */
|
|
76
82
|
release(botId) {
|
|
@@ -388,6 +388,7 @@ const appConfigSchema = z.object({
|
|
|
388
388
|
phone: z.enum(["ios", "android"]).optional(),
|
|
389
389
|
}).optional(),
|
|
390
390
|
mistral: z.object({ key: optionalText }).optional(),
|
|
391
|
+
cerebras: z.object({ key: optionalText }).optional(),
|
|
391
392
|
xai: z.object({ key: optionalText, url: optionalText }).optional(),
|
|
392
393
|
/** Anthropic API key for Claude Code billed per token, handed only to
|
|
393
394
|
* Claude instances; `url` only for a proxy or a test double. Never a
|
|
@@ -884,6 +885,9 @@ export function loadConfig() {
|
|
|
884
885
|
cfg.mistral = { ...cfg.mistral };
|
|
885
886
|
if (process.env.MISTRAL_API_KEY !== undefined)
|
|
886
887
|
cfg.mistral.key = process.env.MISTRAL_API_KEY;
|
|
888
|
+
cfg.cerebras = { ...cfg.cerebras };
|
|
889
|
+
if (process.env.CEREBRAS_API_KEY !== undefined)
|
|
890
|
+
cfg.cerebras.key = process.env.CEREBRAS_API_KEY;
|
|
887
891
|
cfg.xai = { ...cfg.xai };
|
|
888
892
|
if (process.env.XAI_API_KEY !== undefined)
|
|
889
893
|
cfg.xai.key = process.env.XAI_API_KEY;
|
|
@@ -961,6 +965,7 @@ export function syncCredentialEnv(patch) {
|
|
|
961
965
|
const secrets = [
|
|
962
966
|
[patch.xai?.key, "XAI_API_KEY"],
|
|
963
967
|
[patch.mistral?.key, "MISTRAL_API_KEY"],
|
|
968
|
+
[patch.cerebras?.key, "CEREBRAS_API_KEY"],
|
|
964
969
|
[patch.anthropic?.key, "OMB_ANTHROPIC_API_KEY"],
|
|
965
970
|
[patch.openaiCompat?.key, "OPENAI_COMPAT_API_KEY"],
|
|
966
971
|
[patch.openai?.key, "OMB_OPENAI_API_KEY"],
|
|
@@ -1007,6 +1012,7 @@ export function syncCredentialEnv(patch) {
|
|
|
1007
1012
|
export const WORKSPACE_CREDENTIAL_ENV = [
|
|
1008
1013
|
"XAI_API_KEY",
|
|
1009
1014
|
"MISTRAL_API_KEY",
|
|
1015
|
+
"CEREBRAS_API_KEY",
|
|
1010
1016
|
"OMB_ANTHROPIC_API_KEY",
|
|
1011
1017
|
"OMB_ANTHROPIC_API_URL",
|
|
1012
1018
|
"OMB_HOSTED_MODEL_TOKEN",
|
|
@@ -1079,6 +1085,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
1079
1085
|
"OPENCODE_API_KEY",
|
|
1080
1086
|
"XAI_API_KEY",
|
|
1081
1087
|
"MISTRAL_API_KEY",
|
|
1088
|
+
"CEREBRAS_API_KEY",
|
|
1082
1089
|
"CURSOR_API_KEY",
|
|
1083
1090
|
"CURSOR_AUTH_TOKEN",
|
|
1084
1091
|
];
|
|
@@ -1111,7 +1118,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1111
1118
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1112
1119
|
if (storedProfiles.success)
|
|
1113
1120
|
disk.browserProfiles = storedProfiles.data;
|
|
1114
|
-
for (const key of ["xai", "anthropic", "mistral", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1121
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1115
1122
|
const section = checkedPatch[key];
|
|
1116
1123
|
if (!section)
|
|
1117
1124
|
continue;
|
|
@@ -1340,6 +1347,8 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
|
1340
1347
|
|| ownUrl(routingDefaults?.url || cfg.openaiCompat?.url || process.env.OPENAI_COMPAT_URL || "https://openrouter.ai/api/v1");
|
|
1341
1348
|
case "mistral":
|
|
1342
1349
|
return own(entry.environment?.MISTRAL_API_KEY) || ownUrl("https://api.mistral.ai/v1");
|
|
1350
|
+
case "cerebras":
|
|
1351
|
+
return own(entry.environment?.CEREBRAS_API_KEY) || ownUrl("https://api.cerebras.ai/v1");
|
|
1343
1352
|
case "grok":
|
|
1344
1353
|
return own(entry.environment?.XAI_API_KEY) || (own(config.apiKeyEnv) && config.apiKeyEnv !== "XAI_API_KEY")
|
|
1345
1354
|
|| ownUrl("https://api.x.ai/v1", cfg.xai?.url);
|
|
@@ -1357,6 +1366,8 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1357
1366
|
const environment = new Map();
|
|
1358
1367
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
1359
1368
|
environment.set("MISTRAL_API_KEY", cfg.mistral.key);
|
|
1369
|
+
if (driver === "cerebras" && cfg.cerebras?.key)
|
|
1370
|
+
environment.set("CEREBRAS_API_KEY", cfg.cerebras.key);
|
|
1360
1371
|
if (driver === "grok" && cfg.xai?.key)
|
|
1361
1372
|
environment.set("XAI_API_KEY", cfg.xai.key);
|
|
1362
1373
|
// The workspace Anthropic key reaches Claude Code as the variable it
|
|
@@ -1453,6 +1464,7 @@ export function instanceConfigs(cfg) {
|
|
|
1453
1464
|
computer: { driver: "boxAgent" },
|
|
1454
1465
|
openaiCompat: { driver: "openai-compat" },
|
|
1455
1466
|
mistral: { driver: "mistral" },
|
|
1467
|
+
cerebras: { driver: "cerebras" },
|
|
1456
1468
|
...API_KEY_FLEET,
|
|
1457
1469
|
qwen: { driver: "qwenAgent" },
|
|
1458
1470
|
hermes: { driver: "hermesAgent" },
|
|
@@ -1471,6 +1483,7 @@ export function instanceConfigs(cfg) {
|
|
|
1471
1483
|
cursor: { driver: "cursorAgent" },
|
|
1472
1484
|
openaiCompat: { driver: "openai-compat" },
|
|
1473
1485
|
mistral: { driver: "mistral" },
|
|
1486
|
+
cerebras: { driver: "cerebras" },
|
|
1474
1487
|
...API_KEY_FLEET,
|
|
1475
1488
|
...CUSTOM_ONLY,
|
|
1476
1489
|
};
|
|
@@ -82,7 +82,7 @@ export function poolLocalVmTarget(seat) {
|
|
|
82
82
|
label: `pool-${seat}`,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
/** Only provisioning creates this durable directory; idle
|
|
85
|
+
/** Only provisioning creates this durable directory; idle shutdown keeps it. */
|
|
86
86
|
export function localVmWorkspaceExists(target) {
|
|
87
87
|
try {
|
|
88
88
|
return lstatSync(target.workspaceDir, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
@@ -269,6 +269,7 @@ function emptyStatus(platform, target) {
|
|
|
269
269
|
desktopReady: false,
|
|
270
270
|
desktop_error: null,
|
|
271
271
|
create_supported: true,
|
|
272
|
+
resumable: false,
|
|
272
273
|
ready: false,
|
|
273
274
|
problem: "Install a supported container runtime first",
|
|
274
275
|
image_ref: IMAGE,
|
|
@@ -283,21 +284,8 @@ function emptyStatus(platform, target) {
|
|
|
283
284
|
viewer_url: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
|
-
/**
|
|
287
|
-
*
|
|
288
|
-
* True for exactly one state: the container is gone, and a plain `run` is all
|
|
289
|
-
* that is needed to bring it back. That is what `LocalVmIdleTimer` leaves
|
|
290
|
-
* behind — it removes an unused Local VM rather than pausing it — so a turn
|
|
291
|
-
* arriving after an idle period should not have to send the person to App
|
|
292
|
-
* Settings for a container the app itself deleted.
|
|
293
|
-
*
|
|
294
|
-
* Every other problem in `statusProblem` stays the person's call and returns
|
|
295
|
-
* false here: no runtime, daemon down, image never prepared, `create_supported`
|
|
296
|
-
* false, and any existing container — stale image, unmanaged, unsafe network,
|
|
297
|
-
* security or persistence. A stopped container is excluded deliberately, since
|
|
298
|
-
* `statusProblem` says this desktop image cannot safely resume and asks for a
|
|
299
|
-
* recreate rather than a start.
|
|
300
|
-
*/
|
|
287
|
+
/** Recreate a missing desktop when its image and runtime are already prepared.
|
|
288
|
+
* This also recovers desktops deleted by older versions' idle cleanup. */
|
|
301
289
|
export function localVmRecreatableOnDemand(status) {
|
|
302
290
|
return Boolean(status.runtime)
|
|
303
291
|
&& status.daemonUp
|
|
@@ -305,12 +293,28 @@ export function localVmRecreatableOnDemand(status) {
|
|
|
305
293
|
&& status.container === "missing"
|
|
306
294
|
&& status.create_supported;
|
|
307
295
|
}
|
|
296
|
+
/** Start only an existing, compatible desktop with the managed safety boundary.
|
|
297
|
+
* Fails closed on fields left "unknown" by a partial inspect, which
|
|
298
|
+
* `statusProblem` alone would let through. */
|
|
299
|
+
export function localVmResumable(status) {
|
|
300
|
+
return Boolean(status.runtime) && status.daemonUp && status.image && status.container === "stopped"
|
|
301
|
+
&& existingContainerProblem(status) === null
|
|
302
|
+
&& status.network === "loopback" && status.security === "hardened" && status.persistence === "durable";
|
|
303
|
+
}
|
|
304
|
+
/** What a turn may do on its own to bring this Local VM up, if anything. */
|
|
305
|
+
export function localVmWakeAction(status) {
|
|
306
|
+
if (localVmRecreatableOnDemand(status))
|
|
307
|
+
return "run";
|
|
308
|
+
if (localVmResumable(status))
|
|
309
|
+
return "start";
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
308
312
|
/** Whether Auto may attach this Local VM without a person choosing it: the
|
|
309
|
-
* desktop is ready,
|
|
310
|
-
*
|
|
313
|
+
* desktop is ready, a compatible stopped desktop can be started, or its image
|
|
314
|
+
* is prepared and a missing container can be recreated. Anything else — no runtime, daemon down, image
|
|
311
315
|
* never prepared, an unmanaged or unsafe container — stays the person's call. */
|
|
312
316
|
export function autoLocalVmAttachable(status) {
|
|
313
|
-
return status.ready === true ||
|
|
317
|
+
return status.ready === true || localVmWakeAction(status) !== null;
|
|
314
318
|
}
|
|
315
319
|
function statusProblem(status) {
|
|
316
320
|
if (!status.runtime)
|
|
@@ -324,6 +328,19 @@ function statusProblem(status) {
|
|
|
324
328
|
}
|
|
325
329
|
if (status.container === "missing")
|
|
326
330
|
return "Create the Local VM";
|
|
331
|
+
const existing = existingContainerProblem(status);
|
|
332
|
+
if (existing)
|
|
333
|
+
return existing;
|
|
334
|
+
if (status.container === "stopped")
|
|
335
|
+
return "The Local VM is stopped; start it to continue";
|
|
336
|
+
if (status.desktop_error)
|
|
337
|
+
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
338
|
+
if (!status.desktopReady)
|
|
339
|
+
return "The Local VM started, but Cua Driver is not ready yet";
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
/** Problems with an existing container that only a recreate fixes. */
|
|
343
|
+
function existingContainerProblem(status) {
|
|
327
344
|
if (!status.imageMatches)
|
|
328
345
|
return "The existing Local VM uses an older desktop or Cua Driver; recreate it";
|
|
329
346
|
if (!status.managed)
|
|
@@ -334,12 +351,6 @@ function statusProblem(status) {
|
|
|
334
351
|
return "The existing Local VM is missing safety limits; recreate it";
|
|
335
352
|
if (status.persistence === "unsafe")
|
|
336
353
|
return "The existing Local VM is missing its durable folder; recreate it";
|
|
337
|
-
if (status.container === "stopped")
|
|
338
|
-
return "This desktop image cannot safely resume; recreate the Local VM";
|
|
339
|
-
if (status.desktop_error)
|
|
340
|
-
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
341
|
-
if (!status.desktopReady)
|
|
342
|
-
return "The Local VM started, but Cua Driver is not ready yet";
|
|
343
354
|
return null;
|
|
344
355
|
}
|
|
345
356
|
/** Shared with the BYO-VPS backend (vps-computer.ts): both containers are
|
|
@@ -457,6 +468,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
457
468
|
const inspected = JSON.parse(stdout);
|
|
458
469
|
const detail = inspected[0];
|
|
459
470
|
status.container = detail?.State?.Running ? "running" : "stopped";
|
|
471
|
+
status.stopped_at = status.container === "stopped" ? detail?.State?.FinishedAt ?? null : null;
|
|
460
472
|
status.network = dockerPortsAreLocal(detail?.HostConfig?.PortBindings) ? "loopback" : "unsafe";
|
|
461
473
|
status.viewer_port = dockerViewerPort(detail?.NetworkSettings?.Ports, target.viewerPort);
|
|
462
474
|
status.imageMatches =
|
|
@@ -530,6 +542,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
530
542
|
}
|
|
531
543
|
status.problem = statusProblem(status);
|
|
532
544
|
status.ready = status.problem === null;
|
|
545
|
+
status.resumable = localVmResumable(status);
|
|
533
546
|
return status;
|
|
534
547
|
}
|
|
535
548
|
function loopback(address) {
|
|
@@ -597,8 +610,8 @@ function appleWorkspaceMountIsSafe(mounts, platform, expectedWorkspace) {
|
|
|
597
610
|
* runtime-specific capability exception is Podman's Firefox sandbox chroot.
|
|
598
611
|
* Callers also differ on restart policy — the VPS
|
|
599
612
|
* container must survive a reboot nobody is watching ("unless-stopped"),
|
|
600
|
-
* while
|
|
601
|
-
*
|
|
613
|
+
* while Local VM starts remain controlled by OMB's idle policy and turn
|
|
614
|
+
* lifecycle rather than a daemon restart policy. */
|
|
602
615
|
export function dockerSecurityIsHardened(config, options = {}) {
|
|
603
616
|
if (!config)
|
|
604
617
|
return false;
|
|
@@ -727,10 +740,8 @@ export async function containerComputerAction(action, runner = sh, platform = pr
|
|
|
727
740
|
if (action === "run" && !before.create_supported) {
|
|
728
741
|
throw Object.assign(new Error(before.problem ?? "This runtime cannot create a per-bot Local VM"), { status: 409 });
|
|
729
742
|
}
|
|
730
|
-
if (action === "start") {
|
|
731
|
-
throw Object.assign(new Error(
|
|
732
|
-
status: 409,
|
|
733
|
-
});
|
|
743
|
+
if (action === "start" && !localVmResumable(before)) {
|
|
744
|
+
throw Object.assign(new Error(before.problem ?? "The Local VM is not stopped"), { status: 409 });
|
|
734
745
|
}
|
|
735
746
|
if (action === "stop" && before.container !== "running") {
|
|
736
747
|
throw Object.assign(new Error("The Local VM is not running"), { status: 409 });
|
|
@@ -943,7 +954,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
|
|
|
943
954
|
run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
|
|
944
955
|
? null
|
|
945
956
|
: command(containerRunArgs(runtime, "CHANGE_ME", target)),
|
|
946
|
-
start:
|
|
957
|
+
start: command(["start", target.containerName]),
|
|
947
958
|
stop: command(["stop", target.containerName]),
|
|
948
959
|
remove: command(["rm", runtime === "container" ? "--force" : "-f", target.containerName]),
|
|
949
960
|
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolIdentity } from "../shared/tool-name.js";
|
|
1
2
|
/** Evidence is observed, not inferred from a provider's brand. In particular,
|
|
2
3
|
* OpenAI-compatible and Grok turns now expose structured tool activity too. */
|
|
3
4
|
export function coverageForDriver(_driverKind, fullResults = false, observedTools = false) {
|
|
@@ -20,13 +21,19 @@ export function buildTurnDigest(input) {
|
|
|
20
21
|
for (const m of input.activities) {
|
|
21
22
|
if (m.kind !== "activity" || !m.tool?.itemId || m.turnId !== input.turnId)
|
|
22
23
|
continue;
|
|
23
|
-
|
|
24
|
+
// by identity, not by the chip's title: a command-titled chip would
|
|
25
|
+
// otherwise be its own bucket and carry the unredacted command line into
|
|
26
|
+
// a row that is FTS-indexed and replayed into rebuilt context
|
|
27
|
+
const identity = toolIdentity(m.tool.name);
|
|
28
|
+
const entry = byName.get(identity) ?? { name: fitBytes(identity, 160), count: 0, failed: 0 };
|
|
24
29
|
entry.count += 1;
|
|
25
30
|
if (m.tool.ok === false)
|
|
26
31
|
entry.failed += 1;
|
|
32
|
+
// the driver's summary is the bounded, redacted one-line command; the
|
|
33
|
+
// title is neither, so a shell bucket says what it ran only through this
|
|
27
34
|
if (entry.sample === undefined && m.tool.summary)
|
|
28
35
|
entry.sample = fitBytes(m.tool.summary.replace(/\s+/g, " "), 300);
|
|
29
|
-
byName.set(
|
|
36
|
+
byName.set(identity, entry);
|
|
30
37
|
}
|
|
31
38
|
// busiest first; ties keep first-seen order (Map preserves insertion)
|
|
32
39
|
const ranked = [...byName.values()].sort((a, b) => b.count - a.count);
|
|
@@ -78,6 +85,9 @@ function firstSentence(reply) {
|
|
|
78
85
|
export function renderDigest(d) {
|
|
79
86
|
const parts = ["[digest]"];
|
|
80
87
|
if (d.tools.length) {
|
|
88
|
+
// every entry ends at "×N" or "(N failed)": the phones split the tools
|
|
89
|
+
// part on exactly that (ios DigestSummary, android TurnDigest), so a
|
|
90
|
+
// shell bucket's sample stays in the stored digest, not in this line
|
|
81
91
|
const tools = d.tools.map((t) => `${t.name} ×${t.count}${t.failed ? ` (${t.failed} failed)` : ""}`).join(", ");
|
|
82
92
|
parts.push(`tools: ${tools}${d.toolsDropped ? ` +${d.toolsDropped} more` : ""}${d.hookCoverage === "preview" ? " (from tool previews)" : ""}`);
|
|
83
93
|
}
|
|
@@ -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.
|
|
@@ -87,8 +88,13 @@ const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 3
|
|
|
87
88
|
const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
|
|
88
89
|
const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
|
|
89
90
|
/** 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
|
|
91
|
+
* re-send the full prompt after this many bare turns as a backstop to
|
|
92
|
+
* compaction detection. */
|
|
91
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;
|
|
92
98
|
// Read lazily (not at import) so a test can shorten the window. Unlike the
|
|
93
99
|
// setup calls above, session/prompt legitimately streams for minutes, so a
|
|
94
100
|
// wall-clock deadline would false-positive: this guard only trips when the
|
|
@@ -96,13 +102,23 @@ const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
|
96
102
|
// streams thought chunks, then goes silent forever and never resolves). 0
|
|
97
103
|
// disables the guard, restoring the pre-fix "hang until the user cancels"
|
|
98
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;
|
|
99
114
|
const promptIdleTimeoutMs = () => {
|
|
100
115
|
const raw = process.env.OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS;
|
|
101
116
|
if (raw === undefined)
|
|
102
|
-
return
|
|
117
|
+
return DEFAULT_ACP_PROMPT_IDLE_MS;
|
|
103
118
|
const ms = Number(raw);
|
|
104
119
|
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
105
120
|
};
|
|
121
|
+
const formatQuietLimit = (ms) => ms >= 120_000 && ms % 60_000 === 0 ? `${ms / 60_000} min` : `${Math.round(ms / 1000)} s`;
|
|
106
122
|
/** Keep a turn's set of running tool calls in step with the agent's
|
|
107
123
|
* `tool_call` / `tool_call_update` notifications: a call is running from
|
|
108
124
|
* the first update that is not terminal until one that is. A `tool_call`
|
|
@@ -495,6 +511,101 @@ export function createAcpDriver(support) {
|
|
|
495
511
|
}
|
|
496
512
|
return servers;
|
|
497
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
|
+
};
|
|
498
609
|
/** The one completion path for a turn: the prompt result, a crashed
|
|
499
610
|
* child, an unanswered cancel, or an rpc error the turn body throws.
|
|
500
611
|
* The child is NOT killed here — a clean settle leaves it pooled for
|
|
@@ -505,6 +616,7 @@ export function createAcpDriver(support) {
|
|
|
505
616
|
return;
|
|
506
617
|
current.state.settled = true;
|
|
507
618
|
current.acknowledge();
|
|
619
|
+
current.stopQuietWatch?.();
|
|
508
620
|
if (current.interruptTimer)
|
|
509
621
|
clearTimeout(current.interruptTimer);
|
|
510
622
|
for (const finish of current.asks.values())
|
|
@@ -597,7 +709,7 @@ export function createAcpDriver(support) {
|
|
|
597
709
|
return;
|
|
598
710
|
}
|
|
599
711
|
rpcPending.delete(id);
|
|
600
|
-
const error = new Error(idleMessage ?? `${method} stopped responding`);
|
|
712
|
+
const error = new Error((typeof idleMessage === "function" ? idleMessage() : idleMessage) ?? `${method} stopped responding`);
|
|
601
713
|
Object.assign(error, { acpPromptStall: true });
|
|
602
714
|
reject(error);
|
|
603
715
|
}, idleMs);
|
|
@@ -616,9 +728,13 @@ export function createAcpDriver(support) {
|
|
|
616
728
|
});
|
|
617
729
|
send({ jsonrpc: "2.0", id, method, params });
|
|
618
730
|
});
|
|
731
|
+
let lastInboundAt = Date.now();
|
|
619
732
|
const acp = {
|
|
620
733
|
send,
|
|
621
734
|
request,
|
|
735
|
+
get lastInboundAt() { return lastInboundAt; },
|
|
736
|
+
touch: () => { for (const p of rpcPending.values())
|
|
737
|
+
p.armIdle(); },
|
|
622
738
|
failAll: (error) => {
|
|
623
739
|
for (const p of rpcPending.values()) {
|
|
624
740
|
if (p.timer)
|
|
@@ -916,12 +1032,26 @@ export function createAcpDriver(support) {
|
|
|
916
1032
|
}
|
|
917
1033
|
break;
|
|
918
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
|
+
}
|
|
919
1048
|
}
|
|
920
1049
|
};
|
|
921
1050
|
const session = {
|
|
922
1051
|
child,
|
|
923
1052
|
acp,
|
|
924
1053
|
launch,
|
|
1054
|
+
env,
|
|
925
1055
|
cwd,
|
|
926
1056
|
contractKey,
|
|
927
1057
|
sessionKey: null,
|
|
@@ -957,6 +1087,7 @@ export function createAcpDriver(support) {
|
|
|
957
1087
|
appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(msg) });
|
|
958
1088
|
// Inbound traffic proves the child is alive and making progress,
|
|
959
1089
|
// so every idle deadline restarts; only total silence trips it.
|
|
1090
|
+
lastInboundAt = Date.now();
|
|
960
1091
|
for (const p of rpcPending.values())
|
|
961
1092
|
p.armIdle();
|
|
962
1093
|
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
@@ -1159,7 +1290,7 @@ export function createAcpDriver(support) {
|
|
|
1159
1290
|
// respawns the child, every wire call must reach the live record, so
|
|
1160
1291
|
// nothing captures the connection off it.
|
|
1161
1292
|
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 };
|
|
1293
|
+
const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false, usagePeak: null, usageLast: null, usageCompacted: false };
|
|
1163
1294
|
let acknowledge = () => { };
|
|
1164
1295
|
let rejectStartup = (_error) => { };
|
|
1165
1296
|
const startupAck = turn.startupRecovery ? new Promise((resolve, reject) => {
|
|
@@ -1227,6 +1358,7 @@ export function createAcpDriver(support) {
|
|
|
1227
1358
|
asks,
|
|
1228
1359
|
runningTools: new Set(),
|
|
1229
1360
|
interruptTimer: null,
|
|
1361
|
+
stopQuietWatch: null,
|
|
1230
1362
|
flushAssistantText,
|
|
1231
1363
|
receiveModelVariants,
|
|
1232
1364
|
};
|
|
@@ -1252,6 +1384,7 @@ export function createAcpDriver(support) {
|
|
|
1252
1384
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1253
1385
|
session.current = current;
|
|
1254
1386
|
(async () => {
|
|
1387
|
+
let pendingSplitReceipt = null;
|
|
1255
1388
|
try {
|
|
1256
1389
|
// The handshake is paid once per process, not once per turn. It
|
|
1257
1390
|
// is a function so the establishment retry below can pay it
|
|
@@ -1512,12 +1645,15 @@ export function createAcpDriver(support) {
|
|
|
1512
1645
|
// adapter call) keeps the legacy full-prompt shape.
|
|
1513
1646
|
const halves = promptHalves(turn);
|
|
1514
1647
|
let promptInput = promptTurn;
|
|
1515
|
-
let pendingSplitReceipt = null;
|
|
1516
1648
|
if (halves.stable !== null) {
|
|
1517
1649
|
const receiptKey = JSON.stringify([threadId, sessionId]);
|
|
1518
|
-
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);
|
|
1519
1655
|
promptInput = { ...promptTurn, system: "", text: composed.text };
|
|
1520
|
-
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
1656
|
+
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt, previous: previousReceipt };
|
|
1521
1657
|
}
|
|
1522
1658
|
const text = support.buildPromptText
|
|
1523
1659
|
? support.buildPromptText(promptInput)
|
|
@@ -1538,13 +1674,32 @@ export function createAcpDriver(support) {
|
|
|
1538
1674
|
state.promptSent = true;
|
|
1539
1675
|
acknowledge();
|
|
1540
1676
|
const promptIdleMs = promptIdleTimeoutMs();
|
|
1541
|
-
const
|
|
1542
|
-
|
|
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
|
+
});
|
|
1543
1684
|
if (pendingSplitReceipt) {
|
|
1544
1685
|
// session/prompt resolving is the acceptance boundary: a
|
|
1545
1686
|
// rejected prompt leaves the receipt unwritten, so the next
|
|
1546
|
-
// turn redelivers what this one never received.
|
|
1547
|
-
|
|
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
|
+
}
|
|
1548
1703
|
}
|
|
1549
1704
|
// opencode 1.18.18 reports usage at the result root; grok and
|
|
1550
1705
|
// gemini put it under _meta. Read both rather than lose the count.
|
|
@@ -1597,6 +1752,11 @@ export function createAcpDriver(support) {
|
|
|
1597
1752
|
}
|
|
1598
1753
|
}
|
|
1599
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
|
+
}
|
|
1600
1760
|
if (!state.settled) {
|
|
1601
1761
|
const message = e instanceof Error ? e.message : String(e);
|
|
1602
1762
|
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;
|