openmausbot 0.1.96 → 0.1.97
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-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -1
- package/dist-server/drivers/agents-proxy.js +5 -5
- package/dist-server/index.js +2379 -1596
- package/dist-server/openmausbot.js +857 -1012
- package/dist-server/pair-cli.js +857 -1012
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +1 -1
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +7 -11
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +1 -1
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +3 -3
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/openai-chat.js +98 -6
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/index.js +555 -279
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/store.js +125 -1
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/package.json +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The option a request picks, and whether it is the computer this turn
|
|
2
|
+
* already has. `current` is the turn's mounted place. `startsOnFirstCall`
|
|
3
|
+
* says that place's computer is the one this turn's first computer call
|
|
4
|
+
* creates or wakes (the bot's own cloud computer): it is this turn's before
|
|
5
|
+
* it is up, so asking for it, or for Auto, keeps it instead of restarting
|
|
6
|
+
* the request somewhere else. */
|
|
7
|
+
export function pickComputer(options, requested, current, startsOnFirstCall) {
|
|
8
|
+
const isCurrent = (option) => option.surface === current && (option.ready || (startsOnFirstCall && option.available));
|
|
9
|
+
const option = requested === "auto"
|
|
10
|
+
? options.find(isCurrent)
|
|
11
|
+
?? options.find((candidate) => candidate.ready && candidate.surface === "vm")
|
|
12
|
+
?? options.find((candidate) => candidate.ready)
|
|
13
|
+
?? options.find((candidate) => candidate.canStart)
|
|
14
|
+
?? options.find((candidate) => candidate.canCreate && candidate.surface === "vm")
|
|
15
|
+
?? options.find((candidate) => candidate.canCreate)
|
|
16
|
+
: options.find((candidate) => candidate.surface === requested);
|
|
17
|
+
return { option, current: option !== undefined && isCurrent(option) };
|
|
18
|
+
}
|
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
|
|
9
9
|
import { writeFileAtomic } from "./atomic.js";
|
|
10
|
+
import { withoutComputerEngine } from "./computer-engine-removal.js";
|
|
10
11
|
import { newBotDefaultsSchema } from "./new-bot-defaults.js";
|
|
11
12
|
import { EFFORT_LEVELS } from "../shared/wire.js";
|
|
12
13
|
import { isModelVariant } from "./contracts.js";
|
|
@@ -1443,10 +1444,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
|
1443
1444
|
}
|
|
1444
1445
|
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
1445
1446
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
1446
|
-
* Grok driver reads XAI_API_KEY
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1447
|
+
* Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
|
|
1448
|
+
* other engine brings its own login, so handing it a key it never uses would
|
|
1449
|
+
* only put that key in the environment of an unrelated child process. The
|
|
1450
|
+
* Boat token reaches no engine at all: the harness alone talks to Boat. */
|
|
1450
1451
|
function injectedEnvironment(cfg, instanceId, driver) {
|
|
1451
1452
|
const environment = new Map();
|
|
1452
1453
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
@@ -1485,11 +1486,6 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1485
1486
|
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1486
1487
|
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1487
1488
|
}
|
|
1488
|
-
// driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
|
|
1489
|
-
// Only the person's own token: without one the driver itself falls back
|
|
1490
|
-
// to Cloud Pro's included token, which never enters an environment map.
|
|
1491
|
-
if (driver === "boxAgent" && cfg.box?.token)
|
|
1492
|
-
environment.set("BOX_TOKEN", cfg.box.token);
|
|
1493
1489
|
if (driver === "opencodeGo") {
|
|
1494
1490
|
// Keys for OpenCode's other providers, under the names OpenCode reads.
|
|
1495
1491
|
for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
|
|
@@ -1551,7 +1547,6 @@ export function instanceConfigs(cfg) {
|
|
|
1551
1547
|
chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
|
|
1552
1548
|
antigravity: { driver: "antigravityAgent" },
|
|
1553
1549
|
opencodeGo: { driver: "opencodeGo" },
|
|
1554
|
-
computer: { driver: "boxAgent" },
|
|
1555
1550
|
openaiCompat: { driver: "openai-compat" },
|
|
1556
1551
|
mistral: { driver: "mistral" },
|
|
1557
1552
|
cerebras: { driver: "cerebras" },
|
|
@@ -1578,7 +1573,8 @@ export function instanceConfigs(cfg) {
|
|
|
1578
1573
|
...CUSTOM_ONLY,
|
|
1579
1574
|
};
|
|
1580
1575
|
const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
|
|
1581
|
-
|
|
1576
|
+
// A fleet saved before the Computer engine was removed may still name it.
|
|
1577
|
+
const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
|
|
1582
1578
|
// Product fleets pick up newly shipped engines. A one-off test/shadow map
|
|
1583
1579
|
// (no claude/grok/codex) is left exactly as written.
|
|
1584
1580
|
if (configured &&
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A connected-app call refused for a missing OAuth permission (MOCA-273).
|
|
2
|
+
//
|
|
3
|
+
// Google answers 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT when the account's grant
|
|
4
|
+
// lacks a scope the action needs, such as gmail.settings.basic for a filter.
|
|
5
|
+
// Composio's default auth config for a toolkit never asks for that scope, so
|
|
6
|
+
// reconnecting cannot fix it and a bot that only sees the 403 retries or
|
|
7
|
+
// asks the person to reconnect. The fix is the person's own auth config in
|
|
8
|
+
// their Composio project, which OpenMausBot already prefers
|
|
9
|
+
// (listCustomAuthConfigs). This appends that explanation to the tool result,
|
|
10
|
+
// leaving the provider's own error untouched.
|
|
11
|
+
const SCOPE_ERROR = /ACCESS_TOKEN_SCOPE_INSUFFICIENT|insufficient authentication scopes|insufficientPermissions/i;
|
|
12
|
+
/** The Google scope a known tool needs, when the tool name says which. */
|
|
13
|
+
function requiredScope(slug) {
|
|
14
|
+
if (!slug.startsWith("GMAIL_"))
|
|
15
|
+
return null;
|
|
16
|
+
if (/FORWARD|SEND_AS|DELEGATE/.test(slug))
|
|
17
|
+
return "https://www.googleapis.com/auth/gmail.settings.sharing";
|
|
18
|
+
if (/FILTER|VACATION|AUTO_REPLY|IMAP|POP|LANGUAGE/.test(slug))
|
|
19
|
+
return "https://www.googleapis.com/auth/gmail.settings.basic";
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function appName(slug) {
|
|
23
|
+
const toolkit = slug.split("_")[0] ?? "";
|
|
24
|
+
if (toolkit === "GMAIL")
|
|
25
|
+
return "Gmail";
|
|
26
|
+
if (toolkit.startsWith("GOOGLE"))
|
|
27
|
+
return "Google";
|
|
28
|
+
return toolkit.charAt(0) + toolkit.slice(1).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/** The note a bot reads after its call was refused for a missing permission. */
|
|
31
|
+
export function scopeHint(slugs) {
|
|
32
|
+
const slug = slugs.find((candidate) => requiredScope(candidate)) ?? slugs[0] ?? "";
|
|
33
|
+
const app = slug ? appName(slug) : "This app";
|
|
34
|
+
const scope = slug ? requiredScope(slug) : null;
|
|
35
|
+
const permission = scope ? `the ${scope} permission` : "the permission this action needs";
|
|
36
|
+
return `OpenMausBot note: ${app} refused this because the connected account has not granted ${permission}. ` +
|
|
37
|
+
`Reconnecting will not add it: the default Composio connection never asks for it. ` +
|
|
38
|
+
`To allow it, the person creates their own ${app} auth config in their Composio project that includes ${permission}, ` +
|
|
39
|
+
`then reconnects ${app} under Connected apps; OpenMausBot uses that config automatically. ` +
|
|
40
|
+
`Tell the person this. Do not retry this call.`;
|
|
41
|
+
}
|
|
42
|
+
/** Adds the note to one JSON-RPC frame whose tool result is a scope refusal. */
|
|
43
|
+
function annotateFrame(frame, slugs) {
|
|
44
|
+
const result = frame?.result;
|
|
45
|
+
if (!result || !Array.isArray(result.content))
|
|
46
|
+
return false;
|
|
47
|
+
const refused = result.content.some((item) => item && typeof item === "object" && typeof item.text === "string" &&
|
|
48
|
+
SCOPE_ERROR.test(item.text));
|
|
49
|
+
if (!refused)
|
|
50
|
+
return false;
|
|
51
|
+
result.content.push({ type: "text", text: scopeHint(slugs) });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The relayed MCP response, with the note added when a tool call came back
|
|
56
|
+
* refused for a missing OAuth permission. Anything else, including a body it
|
|
57
|
+
* cannot read, is returned byte for byte.
|
|
58
|
+
*/
|
|
59
|
+
export function withScopeHint(bytes, contentType, slugs) {
|
|
60
|
+
const text = new TextDecoder().decode(bytes);
|
|
61
|
+
if (!SCOPE_ERROR.test(text))
|
|
62
|
+
return bytes;
|
|
63
|
+
try {
|
|
64
|
+
if (/text\/event-stream/i.test(contentType)) {
|
|
65
|
+
let changed = false;
|
|
66
|
+
const lines = text.split("\n").map((line) => {
|
|
67
|
+
if (!line.startsWith("data:"))
|
|
68
|
+
return line;
|
|
69
|
+
const frame = JSON.parse(line.slice(5));
|
|
70
|
+
if (!annotateFrame(frame, slugs))
|
|
71
|
+
return line;
|
|
72
|
+
changed = true;
|
|
73
|
+
return `data: ${JSON.stringify(frame)}`;
|
|
74
|
+
});
|
|
75
|
+
return changed ? new TextEncoder().encode(lines.join("\n")) : bytes;
|
|
76
|
+
}
|
|
77
|
+
const frame = JSON.parse(text);
|
|
78
|
+
return annotateFrame(frame, slugs) ? new TextEncoder().encode(JSON.stringify(frame)) : bytes;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return bytes;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
2
|
+
/** The ready rule: an engine can run a turn now when it is available, the
|
|
3
|
+
* organisation allows it, and it is not signed out. A custom endpoint brings
|
|
4
|
+
* its own credential, so its sign-in state does not count. */
|
|
5
|
+
export function readyToRun(instance, context = {}) {
|
|
6
|
+
return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined &&
|
|
7
|
+
(instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
8
|
+
}
|
|
9
|
+
/** The engine a new bot gets, and the one a bot moved off a removed engine
|
|
10
|
+
* gets (computer-engine-removal.ts): one picker for both.
|
|
11
|
+
*
|
|
12
|
+
* A saved choice is intentional: an unavailable provider or removed model
|
|
13
|
+
* sends new bots to setup instead of silently changing their provider.
|
|
14
|
+
* Without one, an engine that can run a turn now wins (Claude first), then the
|
|
15
|
+
* first available one. On an enrolled desktop a working personal engine
|
|
16
|
+
* still beats a Company one, so billing stays the person's explicit choice. */
|
|
4
17
|
export function selectDefaultModelSelection(instances, preferred, context = {}) {
|
|
5
18
|
if (preferred) {
|
|
6
19
|
const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
|
|
@@ -22,19 +35,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
|
|
|
22
35
|
}
|
|
23
36
|
// The organisation's policy is inert (no refusal) unless this desktop is enrolled.
|
|
24
37
|
const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
|
|
38
|
+
const ready = available.filter((instance) => readyToRun(instance, context));
|
|
25
39
|
const company = context.company;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// can run a turn now; a working personal engine still wins, so billing
|
|
31
|
-
// stays the person's explicit choice.
|
|
32
|
-
const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
33
|
-
pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
pick = claudeFirst(available);
|
|
37
|
-
}
|
|
40
|
+
const personal = company && instances.some((instance) => company(instance.instanceId))
|
|
41
|
+
? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
|
|
42
|
+
: undefined;
|
|
43
|
+
const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
38
44
|
return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
|
|
39
45
|
}
|
|
40
46
|
/** Complete a new bot's selection with the workspace's new-bot effort. An
|
|
@@ -195,6 +195,8 @@ export function _loadPending() {
|
|
|
195
195
|
}
|
|
196
196
|
if (item.oneWay === true)
|
|
197
197
|
loaded.oneWay = true;
|
|
198
|
+
if (item.completionOwner === "external")
|
|
199
|
+
loaded.completionOwner = "external";
|
|
198
200
|
return [loaded];
|
|
199
201
|
});
|
|
200
202
|
if (items.length)
|
|
@@ -401,7 +403,7 @@ onSettled) {
|
|
|
401
403
|
if (outcome === "settled" && stillQueued) {
|
|
402
404
|
const receipt = findDelegationReceipt(item.id);
|
|
403
405
|
try {
|
|
404
|
-
if (receipt &&
|
|
406
|
+
if (receipt && wakesSource(item))
|
|
405
407
|
onSettled?.(receipt);
|
|
406
408
|
}
|
|
407
409
|
catch (error) {
|
|
@@ -445,6 +447,10 @@ function appendDeliveryMessage(bus, sourceThreadId, item, message) {
|
|
|
445
447
|
return;
|
|
446
448
|
bus.store.appendMessage(threadId, message);
|
|
447
449
|
}
|
|
450
|
+
/** A settled handoff resumes the bot that queued it, unless its outcome
|
|
451
|
+
* belongs elsewhere: to a one-way send's recipient, or to an external
|
|
452
|
+
* runtime that polls for the receipt. */
|
|
453
|
+
const wakesSource = (item) => !item.oneWay && item.completionOwner !== "external";
|
|
448
454
|
const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
|
|
449
455
|
/** Past the busy-hold cap — the tighter bound that fires while its target is
|
|
450
456
|
* still busy, long before the 24-hour window. */
|
|
@@ -528,15 +534,15 @@ export function expireStaleDelegations(bus, now, onSettled) {
|
|
|
528
534
|
expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
|
|
529
535
|
const receipt = findDelegationReceipt(item.id);
|
|
530
536
|
if (receipt)
|
|
531
|
-
expired.push({ receipt,
|
|
537
|
+
expired.push({ receipt, wake: wakesSource(item) });
|
|
532
538
|
}
|
|
533
539
|
}
|
|
534
540
|
if (!expired.length)
|
|
535
541
|
return 0;
|
|
536
542
|
savePending();
|
|
537
|
-
for (const { receipt,
|
|
543
|
+
for (const { receipt, wake } of expired) {
|
|
538
544
|
try {
|
|
539
|
-
if (
|
|
545
|
+
if (wake)
|
|
540
546
|
onSettled?.(receipt);
|
|
541
547
|
}
|
|
542
548
|
catch (error) {
|
|
@@ -717,7 +723,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
717
723
|
const prefixed = item.targetThreadId
|
|
718
724
|
? item.message
|
|
719
725
|
: `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
|
|
720
|
-
await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true);
|
|
726
|
+
await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true, item.completionOwner);
|
|
721
727
|
return "dispatched";
|
|
722
728
|
}
|
|
723
729
|
/** "Is the target free to take this handoff right now?" Both shapes ask for
|
|
@@ -109,6 +109,7 @@ export function recordHanded(state, order, received) {
|
|
|
109
109
|
return {
|
|
110
110
|
...(state.session === undefined ? {} : { session: state.session }),
|
|
111
111
|
...(state.config === undefined ? {} : { config: state.config }),
|
|
112
|
+
...(state.effort === undefined ? {} : { effort: state.effort }),
|
|
112
113
|
...(state.omitted === undefined ? {} : { omitted: state.omitted }),
|
|
113
114
|
...(through === undefined ? {} : { through }),
|
|
114
115
|
ids: sorted,
|
|
@@ -241,10 +242,14 @@ export class Handoffs {
|
|
|
241
242
|
? pending.resumeCursor === undefined ? pending.started : pending.recovery
|
|
242
243
|
: existing === undefined ? pending.resumed : undefined;
|
|
243
244
|
if (!start) {
|
|
245
|
+
if (typeof pending.effort === "string" && existing?.session === session && existing.effort !== pending.effort) {
|
|
246
|
+
this.store.write(pending.botId, threadId, pending.instanceId, { ...existing, effort: pending.effort });
|
|
247
|
+
}
|
|
244
248
|
this.add(threadId, pending, [...pending.placed, ...pending.carried]);
|
|
245
249
|
return;
|
|
246
250
|
}
|
|
247
|
-
this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(
|
|
251
|
+
this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(pending.effort === undefined ? {} : { effort: pending.effort }),
|
|
252
|
+
...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
|
|
248
253
|
}
|
|
249
254
|
add(threadId, pending, received) {
|
|
250
255
|
const session = pending.session ?? pending.resumeCursor;
|
|
@@ -350,7 +350,7 @@ export function createAcpDriver(support) {
|
|
|
350
350
|
}
|
|
351
351
|
// The operator's own secrets are outside any driver's allowlist.
|
|
352
352
|
stripControlPlaneEnv(env);
|
|
353
|
-
support.transformEnv?.(env, activeConfig, instanceId);
|
|
353
|
+
support.transformEnv?.(env, activeConfig, instanceId, input.environment);
|
|
354
354
|
return env;
|
|
355
355
|
};
|
|
356
356
|
let models = support.models;
|
|
@@ -432,8 +432,6 @@ export function resetOpenCodeModelCache() {
|
|
|
432
432
|
/** Compatibility export for older tests/imports while the product migrates
|
|
433
433
|
* from the Go-only name. */
|
|
434
434
|
export const resetOpenCodeGoModelCache = resetOpenCodeModelCache;
|
|
435
|
-
/** The names live in config.ts, beside the keys the owner saves for OpenCode. */
|
|
436
|
-
export { OPENCODE_PROVIDER_ENV };
|
|
437
435
|
/** Whether OpenCode may read provider keys from the server's own
|
|
438
436
|
* environment. Only when that environment is the person's own shell: not on a
|
|
439
437
|
* Cloud home, a hosted team workspace or an organisation-managed desktop, and
|
|
@@ -453,23 +451,16 @@ let providerKeysAllowed = () => openCodeProviderKeysAllowed({
|
|
|
453
451
|
export function setOpenCodeProviderKeyPolicy(allowed) {
|
|
454
452
|
providerKeysAllowed = allowed;
|
|
455
453
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
*
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
function withholdProviderKeysWhenManaged(env) {
|
|
454
|
+
/** Where OpenCode may not read the server's own environment, a provider key
|
|
455
|
+
* reaches it only through its instance environment: a key the owner saved
|
|
456
|
+
* in Settings (config.ts injectedEnvironment), which is theirs on any
|
|
457
|
+
* server. The same name riding along in the server's env stays out. */
|
|
458
|
+
function withholdProviderKeysWhenManaged(env, _config, _instanceId, instanceEnvironment) {
|
|
463
459
|
if (providerKeysAllowed())
|
|
464
460
|
return;
|
|
465
|
-
const own = ownProviderKeys();
|
|
466
461
|
for (const key of OPENCODE_PROVIDER_ENV) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// name still stays out.
|
|
470
|
-
if (own[key] !== undefined && env[key] === own[key])
|
|
471
|
-
continue;
|
|
472
|
-
delete env[key];
|
|
462
|
+
if (!Object.hasOwn(instanceEnvironment, key))
|
|
463
|
+
delete env[key];
|
|
473
464
|
}
|
|
474
465
|
}
|
|
475
466
|
function opencodeConfigDir(env) {
|
|
@@ -766,8 +757,10 @@ function allowOwnedDirectories(env, botId, cwd) {
|
|
|
766
757
|
/** Account failures in plain words, with the fix, naming the provider that
|
|
767
758
|
* refused: with provider keys and `opencode auth login` a model may be
|
|
768
759
|
* OpenRouter's or Anthropic's, not Zen's. Each stays under the 160
|
|
769
|
-
* characters a chat error row shows.
|
|
770
|
-
|
|
760
|
+
* characters a chat error row shows. Zen and Go take the key saved in
|
|
761
|
+
* Settings. On a Cloud the owner has no terminal for `opencode auth login`:
|
|
762
|
+
* another provider's key is saved under Keys for other OpenCode providers. */
|
|
763
|
+
export function describeOpenCodeAccountError(code, model, where = { cloudHome: cloudHomeConfigured() }) {
|
|
771
764
|
const provider = model && model.includes("/") ? model.slice(0, model.indexOf("/")) : "";
|
|
772
765
|
const zen = provider === "opencode";
|
|
773
766
|
const go = provider === "opencode-go";
|
|
@@ -776,8 +769,10 @@ export function describeOpenCodeAccountError(code, model) {
|
|
|
776
769
|
switch (code) {
|
|
777
770
|
case "invalid_credentials":
|
|
778
771
|
return zen || go || !provider
|
|
779
|
-
? "OpenCode rejected its key, or has none for this model.
|
|
780
|
-
:
|
|
772
|
+
? "OpenCode rejected its key, or has none for this model. Add or replace the OpenCode key in Settings → API keys."
|
|
773
|
+
: where.cloudHome
|
|
774
|
+
? `OpenCode's ${name} key for this model is missing or was rejected. Save it under Keys for other OpenCode providers in Settings → API keys.`
|
|
775
|
+
: `OpenCode's ${name} key for this model is missing or was rejected. Fix it with \`opencode auth login\`, or choose another model.`;
|
|
781
776
|
case "insufficient_funds":
|
|
782
777
|
return zen || !provider
|
|
783
778
|
? "Your OpenCode Zen balance has run out. Add credit at opencode.ai, or choose one of Zen's free models for this bot."
|
|
@@ -812,7 +807,7 @@ const support = (loadCatalog) => ({
|
|
|
812
807
|
models: NO_MODELS,
|
|
813
808
|
defaultCli: "opencode",
|
|
814
809
|
nativeSource: "opencode.acp",
|
|
815
|
-
loginNote: "OpenCode has no usable models
|
|
810
|
+
loginNote: "OpenCode has no usable models. Add an OpenCode key in Settings → API keys.",
|
|
816
811
|
install: {
|
|
817
812
|
command: {
|
|
818
813
|
darwin: "npm install -g opencode-ai",
|
|
@@ -259,7 +259,7 @@ function routineFields(args) {
|
|
|
259
259
|
const runOn = destination(args.run_on ?? args.runOn);
|
|
260
260
|
const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
|
|
261
261
|
if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
|
|
262
|
-
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the bot’s
|
|
262
|
+
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the bot’s cloud computer. Legacy "cloud" also means box.' };
|
|
263
263
|
}
|
|
264
264
|
if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
265
265
|
return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
|
|
@@ -132,7 +132,7 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
132
132
|
type: "string",
|
|
133
133
|
// "box" is Boat's historical run_on destination id (agents wire contract).
|
|
134
134
|
enum: ["maus", "box"],
|
|
135
|
-
description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box runs on the bot's
|
|
135
|
+
description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box runs on the bot's cloud computer, same model; it needs cloud computers set up and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
|
|
136
136
|
},
|
|
137
137
|
timeout_minutes: {
|
|
138
138
|
type: "integer",
|
|
@@ -303,7 +303,7 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
303
303
|
description: "Choose where this conversation does computer work. Call with no arguments to inspect actual available choices and the current place. For a task needing computer interaction, select the requested place, or auto to choose a suitable configured computer without asking the user to use menus. OpenMausBot reuses an existing computer first; with a configured provider it can start or provision one when needed. Do not provision for ordinary chat or just to inspect availability. A pending result means end this turn immediately: OpenMausBot updates the conversation selector and resumes the original request with that computer's real tools. Do not use the old tools after requesting a switch, repeat the task, or claim the action is done. This cannot change permissions, override Off, or switch a teammate/routine/channel.",
|
|
304
304
|
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
305
305
|
surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
|
|
306
|
-
description: "auto = suitable configured computer, cloud =
|
|
306
|
+
description: "auto = suitable configured computer, cloud = the bot's cloud computer or self-hosted VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
|
|
307
307
|
} },
|
|
308
308
|
},
|
|
309
309
|
{
|
|
@@ -854,7 +854,7 @@ const WATCHER_TOOL_NAMES = new Set(["create_options_card"]);
|
|
|
854
854
|
const LOCAL_VM_TOOL_NAMES = new Set(["vm_exec"]);
|
|
855
855
|
const CLOUD_HOME_SURFACE = {
|
|
856
856
|
type: "string", enum: ["auto", "cloud", "browser"],
|
|
857
|
-
description: "auto = suitable configured computer, cloud =
|
|
857
|
+
description: "auto = suitable configured computer, cloud = the bot's cloud computer, browser = built-in browser. Omit to list. This server runs in the cloud: the user's own computer and a Local VM are not places here.",
|
|
858
858
|
};
|
|
859
859
|
/** The tools one turn is shown, exactly as tools/list serializes them. */
|
|
860
860
|
export function availableTools(profile) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { AntigravityDriver } from "./antigravity.js";
|
|
2
|
-
import { BoatAgentDriver } from "./boatagent.js";
|
|
3
2
|
import { ClaudeDriver } from "./claude.js";
|
|
4
3
|
import { CodexDriver } from "./codex.js";
|
|
5
4
|
import { GrokDriver } from "./grok.js";
|
|
@@ -33,7 +32,6 @@ export const BUILT_IN_DRIVERS = [
|
|
|
33
32
|
ClaudeDriver,
|
|
34
33
|
CodexDriver,
|
|
35
34
|
AntigravityDriver,
|
|
36
|
-
BoatAgentDriver,
|
|
37
35
|
MinimaxDriver,
|
|
38
36
|
MistralDriver,
|
|
39
37
|
CerebrasDriver,
|
|
@@ -1145,8 +1145,20 @@ export const ClaudeDriver = {
|
|
|
1145
1145
|
// logical turn's stop handle in `active` while it sets up, so Stop is
|
|
1146
1146
|
// never a silent no-op between two CLI processes of the same turn.
|
|
1147
1147
|
const relaunch = logicalTurnId !== undefined;
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
1148
|
+
if (!relaunch) {
|
|
1149
|
+
// Stop is acknowledged at once and the process tree reaped after it
|
|
1150
|
+
// (taskkill is asynchronous on Windows); the stopped turn leaves
|
|
1151
|
+
// `active` only when its close is handled. A Stop that lands while
|
|
1152
|
+
// a turn is still starting lets the harness send the next message
|
|
1153
|
+
// before then. That turn waits for the stopped process to be gone,
|
|
1154
|
+
// never overlapping its helpers, instead of being refused.
|
|
1155
|
+
const stopped = sessions.get(threadId);
|
|
1156
|
+
if (stopped?.turn?.stopRequested && active.get(threadId)?.turnId === stopped.turn.turnId &&
|
|
1157
|
+
await killCliTree(stopped.child))
|
|
1158
|
+
await stopped.closed;
|
|
1159
|
+
if (active.has(threadId))
|
|
1160
|
+
throw new Error("a turn is already running on this thread");
|
|
1161
|
+
}
|
|
1150
1162
|
// A bot-level mode is authoritative for this turn. In particular, an
|
|
1151
1163
|
// old provider instance may still be configured with
|
|
1152
1164
|
// `bypassPermissions`; Ask/Auto must restore Claude's interactive
|
|
@@ -1459,7 +1471,7 @@ export const ClaudeDriver = {
|
|
|
1459
1471
|
if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
1460
1472
|
if (live.idleTimer)
|
|
1461
1473
|
clearTimeout(live.idleTimer);
|
|
1462
|
-
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
|
|
1474
|
+
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, awaitingInit: true, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
|
|
1463
1475
|
active.set(threadId, { stop: () => {
|
|
1464
1476
|
if (live.turn)
|
|
1465
1477
|
live.turn.stopRequested = true;
|
|
@@ -1475,20 +1487,13 @@ export const ClaudeDriver = {
|
|
|
1475
1487
|
: claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
|
|
1476
1488
|
live.volatile = volatile;
|
|
1477
1489
|
const running = live.turn;
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1490
|
+
// A warm process can end between turns just as the next one is
|
|
1491
|
+
// written: this driver learns of an exit only when the event loop
|
|
1492
|
+
// gets to it (later still on Windows), so the check above can find it
|
|
1493
|
+
// live and the write then fail. Nothing was submitted; its close
|
|
1494
|
+
// resumes this same turn on a fresh process (see `awaitingInit`).
|
|
1495
|
+
if (!(await writeUser(live, threadId, message)) && !running?.stopRequested)
|
|
1482
1496
|
closeSession(threadId, "stdin write failed");
|
|
1483
|
-
retryState.delete(threadId);
|
|
1484
|
-
if (mcpConfigPath) {
|
|
1485
|
-
try {
|
|
1486
|
-
rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
|
|
1487
|
-
}
|
|
1488
|
-
catch { }
|
|
1489
|
-
}
|
|
1490
|
-
throw new Error("claude session stdin is not writable");
|
|
1491
|
-
}
|
|
1492
1497
|
// the MCP config was for the first spawn; nothing to clean here
|
|
1493
1498
|
if (mcpConfigPath) {
|
|
1494
1499
|
try {
|
|
@@ -1630,7 +1635,9 @@ export const ClaudeDriver = {
|
|
|
1630
1635
|
cleanupUnownedLaunch();
|
|
1631
1636
|
throw error;
|
|
1632
1637
|
}
|
|
1638
|
+
let markClosed;
|
|
1633
1639
|
const session = {
|
|
1640
|
+
closed: new Promise((resolve) => { markClosed = resolve; }),
|
|
1634
1641
|
child,
|
|
1635
1642
|
broker,
|
|
1636
1643
|
mcpConfigPath,
|
|
@@ -1749,6 +1756,8 @@ export const ClaudeDriver = {
|
|
|
1749
1756
|
break;
|
|
1750
1757
|
}
|
|
1751
1758
|
session.sawInit = true;
|
|
1759
|
+
if (session.turn)
|
|
1760
|
+
session.turn.awaitingInit = false;
|
|
1752
1761
|
session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
|
|
1753
1762
|
if (typeof o.session_id === "string")
|
|
1754
1763
|
session.sessionId = o.session_id;
|
|
@@ -2051,11 +2060,18 @@ export const ClaudeDriver = {
|
|
|
2051
2060
|
const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
|
|
2052
2061
|
const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
|
|
2053
2062
|
const verdict = classifyError({ exitCode: code, stderr: message });
|
|
2063
|
+
// A process kept warm from an earlier turn ended before it
|
|
2064
|
+
// announced this one: it never took the prompt. That is a session
|
|
2065
|
+
// ending between turns, not a failed turn, so the same turn
|
|
2066
|
+
// resumes the session on a fresh process at once — no retry row,
|
|
2067
|
+
// no retry budget spent. (A fresh process is never retained, so
|
|
2068
|
+
// this happens at most once per turn.)
|
|
2069
|
+
const endedBeforeTurn = session.turn.awaitingInit === true;
|
|
2054
2070
|
if (!retry.cancelled &&
|
|
2055
|
-
code !== 0 &&
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2071
|
+
(endedBeforeTurn || (code !== 0 &&
|
|
2072
|
+
verdict.transient &&
|
|
2073
|
+
!session.turn.sawStreamDelta &&
|
|
2074
|
+
retry.attempt < RETRY_MAX_ATTEMPTS - 1))) {
|
|
2059
2075
|
// the CLI is gone but the TURN continues: keep the thread busy,
|
|
2060
2076
|
// emit no terminal event, and relaunch after the backoff. The
|
|
2061
2077
|
// `active` entry STAYS — it is what makes an interrupt during
|
|
@@ -2077,15 +2093,18 @@ export const ClaudeDriver = {
|
|
|
2077
2093
|
}
|
|
2078
2094
|
sessions.delete(threadId);
|
|
2079
2095
|
session.turn = null;
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2096
|
+
let delayMs = 0;
|
|
2097
|
+
if (!endedBeforeTurn) {
|
|
2098
|
+
retry.attempt++;
|
|
2099
|
+
delayMs = computeBackoff(retry.attempt - 1);
|
|
2100
|
+
emit({
|
|
2101
|
+
...base(threadId, turnId),
|
|
2102
|
+
type: "turn.retrying",
|
|
2103
|
+
attempt: retry.attempt,
|
|
2104
|
+
delayMs,
|
|
2105
|
+
reason: verdict.reason,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2089
2108
|
void (async () => {
|
|
2090
2109
|
const wait = interruptibleDelay(delayMs * retryScale, retryAbort.signal);
|
|
2091
2110
|
await wait.promise;
|
|
@@ -2225,7 +2244,7 @@ export const ClaudeDriver = {
|
|
|
2225
2244
|
};
|
|
2226
2245
|
child.on("close", (code) => {
|
|
2227
2246
|
session.finishClose = () => finalizeClose(code);
|
|
2228
|
-
void session.finishClose();
|
|
2247
|
+
void session.finishClose().finally(markClosed);
|
|
2229
2248
|
});
|
|
2230
2249
|
const stop = () => {
|
|
2231
2250
|
if (session.turn)
|
|
@@ -1711,8 +1711,8 @@ export const CodexDriver = {
|
|
|
1711
1711
|
// back. The app-server offers no way to clear a level either:
|
|
1712
1712
|
// "" is rejected outright and thread/start takes no effort at
|
|
1713
1713
|
// all. So a thread keeps the last level it was sent until it is
|
|
1714
|
-
// sent another
|
|
1715
|
-
// thread
|
|
1714
|
+
// sent another; back on Default, the harness gives a thread that
|
|
1715
|
+
// holds a level a new thread instead of resuming it (server/index.ts).
|
|
1716
1716
|
...(turn.effort ? { effort: turn.effort } : {}),
|
|
1717
1717
|
});
|
|
1718
1718
|
};
|