openmausbot 0.1.87 → 0.1.89
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/README.md +2 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-BwaMaZEm.js} +1 -1
- package/dist/assets/{arc-C88Blc2v.js → arc-Be-8GKxd.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-Dp_U2Kv1.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-DQjUjYUk.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-Bd3aE4Kc.js} +1 -1
- package/dist/assets/channel-DqY_gMLg.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-BijjTBeJ.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-CMGdwvzW.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-0G09BfsZ.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-FOp3vSHe.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-Dv-20VIn.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CRgrdFcz.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-3hP4f7QB.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-CG98IjlG.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-BrSTC9xG.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-rpQQQDFX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-G6RSafHA.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-CXjv-IM2.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-Dw07MR9f.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-BwPnlTg2.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-Cm6xcMiz.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-CDpogPbE.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-BV8z3Pnc.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-CKCQhOOC.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-FGvJg69C.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-CwATvgnS.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-CH_c3SSC.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-3guZi71R.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BMHphR2n.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-Vm5DkW5x.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-DO-1ct07.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.js → gitGraphDiagram-X574FWY7-nxeksEIN.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-BVPdbq2r.js +583 -0
- package/dist/assets/{index-zRsJERGJ.js → index-Oxk9x45i.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-Dkcvr0p7.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-BwVibkoO.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-ByiBZqgG.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-qqjgXBF3.js} +1 -1
- package/dist/assets/{linear-DBJGPmjT.js → linear-CtJnKe6A.js} +1 -1
- package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-BEiDklrY.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-DfIYHia9.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-CYyRqa1a.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-hOToy_zH.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DKAsNN9I.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-BSA-d8JU.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-CSmlvb-p.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-CD5F6fnG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-JUq5sx6d.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BjIS-Stk.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-DklkUxOg.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CsjIU6be.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-BkFqK7AB.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-70gJB1EU.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-83Tm62dN.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-tTmseoX9.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-R1LBA55a.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +11 -1
- package/dist-server/drivers/agents-proxy.js +277 -22
- package/dist-server/index.js +23722 -20272
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +755 -404
- package/dist-server/pair-cli.js +755 -404
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +43 -1
- package/dist-server/server/automatic-recovery.js +15 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/config.js +34 -3
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +31 -4
- package/dist-server/server/container-computer.js +70 -0
- package/dist-server/server/contracts.js +32 -0
- package/dist-server/server/drivers/acp/core.js +148 -23
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +12 -15
- package/dist-server/server/drivers/agents-call.js +137 -10
- package/dist-server/server/drivers/agents-catalog.js +100 -6
- package/dist-server/server/drivers/boxagent.js +3 -17
- package/dist-server/server/drivers/claude.js +31 -17
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex.js +7 -25
- package/dist-server/server/drivers/openai-chat.js +31 -3
- package/dist-server/server/drivers/pi.js +237 -30
- package/dist-server/server/drivers/prompt-split.js +32 -9
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness-capabilities.js +61 -0
- package/dist-server/server/index.js +1173 -262
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +15 -1
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +57 -0
- package/dist-server/server/presets.js +32 -32
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/routine-requests.js +29 -7
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +1 -0
- package/dist-server/server/store.js +32 -2
- package/dist-server/server/system-prompt.js +37 -2
- package/dist-server/server/team-setup-requests.js +124 -23
- package/dist-server/server/thread-events.js +3 -1
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/usage-ledger.js +7 -1
- package/dist-server/server/webhooks.js +21 -3
- package/dist-server/server/workspace-backup.js +40 -3
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/shared/ask-question.js +19 -0
- package/dist-server/shared/credential-request.js +5 -1
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/src/lib/composer-attachments.js +42 -3
- package/dist-server/vps-container-mcp.js +11 -1
- package/dist-server/workspace-backup.worker.js +19482 -0
- package/package.json +1 -1
- package/dist/assets/channel-taCNRlUU.js +0 -1
- package/dist/assets/index-BwdMAQ6p.css +0 -1
- package/dist/assets/index-ByJennr7.js +0 -581
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
|
@@ -33,6 +33,22 @@ export function serviceSlugFor(tool) {
|
|
|
33
33
|
return null;
|
|
34
34
|
return tool.slice(0, underscore).toLowerCase();
|
|
35
35
|
}
|
|
36
|
+
/** The service a Composio tool name belongs to when the caller knows the
|
|
37
|
+
* real service slugs. Slugs may contain underscores (bland_ai), which the
|
|
38
|
+
* plain first-segment split above cannot see — BLAND_AI_MAKE_CALL splits
|
|
39
|
+
* as "bland". When a known candidate matches the name's prefix, the
|
|
40
|
+
* longest candidate wins (bland_ai over bland), so an underscored service
|
|
41
|
+
* keeps its own tools; with no matching candidate the caller falls back
|
|
42
|
+
* to serviceSlugFor. */
|
|
43
|
+
export function serviceSlugForCandidates(tool, candidates) {
|
|
44
|
+
let match = null;
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (tool.startsWith(candidate.toUpperCase() + "_") && (match === null || candidate.length > match.length)) {
|
|
47
|
+
match = candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return match;
|
|
51
|
+
}
|
|
36
52
|
/** Classify one relayed JSON-RPC frame. Anything that is not a tools/call,
|
|
37
53
|
* or is a discovery/connection/platform meta-tool, passes through. */
|
|
38
54
|
export function connectorCallFromFrame(payload) {
|
|
@@ -99,16 +115,27 @@ function multiExecuteCall(invoked, args) {
|
|
|
99
115
|
}
|
|
100
116
|
/** Judge distinct target names against a bot's grants. grants undefined
|
|
101
117
|
* is the legacy all-tools bot and passes everything; an explicit record —
|
|
102
|
-
* including the empty one — allows only what it names.
|
|
103
|
-
|
|
118
|
+
* including the empty one — allows only what it names. Service keys are
|
|
119
|
+
* read as own properties only: a name like CONSTRUCTOR_X maps to the
|
|
120
|
+
* "constructor" service, and the inherited property must never pose as
|
|
121
|
+
* a grant.
|
|
122
|
+
* candidates are the caller's connected-service slugs: the real backend
|
|
123
|
+
* slugs, so an underscored service (bland_ai) keeps its own tools instead
|
|
124
|
+
* of a plain-prefix grant (bland) capturing them. An empty list means the
|
|
125
|
+
* connected-service catalog was unreachable, and the plain first-segment
|
|
126
|
+
* split stands as the fallback — call-time enforcement degrades open, the
|
|
127
|
+
* same way advertisement filtering does. */
|
|
128
|
+
export function evaluateConnectorTools(names, grants, candidates = []) {
|
|
104
129
|
if (grants === undefined) {
|
|
105
130
|
return { allowed: true, legacy: true, denials: [], rule: "composio" };
|
|
106
131
|
}
|
|
107
132
|
const denials = [];
|
|
108
133
|
let rule = "";
|
|
109
134
|
for (const tool of new Set(names)) {
|
|
110
|
-
const service = serviceSlugFor(tool);
|
|
111
|
-
const grant = service
|
|
135
|
+
const service = serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool);
|
|
136
|
+
const grant = service !== null && Object.hasOwn(grants, service)
|
|
137
|
+
? grants[service]
|
|
138
|
+
: undefined;
|
|
112
139
|
if (grant && (grant.tools === "*" || grant.tools.includes(tool))) {
|
|
113
140
|
if (!rule)
|
|
114
141
|
rule = "connectorTools." + service;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// typing, screenshots, accessibility, or window discovery.
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
|
+
import { lstatSync } from "node:fs";
|
|
10
11
|
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
12
|
import { tmpdir } from "node:os";
|
|
12
13
|
import { join, resolve } from "node:path";
|
|
@@ -68,6 +69,15 @@ export function perBotLocalVmTarget(botId) {
|
|
|
68
69
|
label: digest,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
72
|
+
/** Only provisioning creates this durable directory; idle removal keeps it. */
|
|
73
|
+
export function localVmWorkspaceExists(target) {
|
|
74
|
+
try {
|
|
75
|
+
return lstatSync(target.workspaceDir, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
71
81
|
const LINUX_WHEELS = {
|
|
72
82
|
x86_64: {
|
|
73
83
|
url: "https://files.pythonhosted.org/packages/fa/d7/a43008a328a40c85e7bc706fc20235b9abedc75e28b413817655153157ff/cua_driver-0.20.0-py3-none-manylinux_2_31_x86_64.whl",
|
|
@@ -808,6 +818,66 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
|
|
|
808
818
|
}
|
|
809
819
|
const screenshotStatusCache = new Map();
|
|
810
820
|
const containerMcpPath = SPAWNED_PROXIES.containerMcp;
|
|
821
|
+
const EXEC_DEFAULT_SECONDS = 60;
|
|
822
|
+
const EXEC_MAX_SECONDS = 300;
|
|
823
|
+
const EXEC_OUTPUT_LIMIT = 20_000;
|
|
824
|
+
/** Keep the start and the end: a failure message is nearly always at the end. */
|
|
825
|
+
export function clipExecOutput(text, limit = EXEC_OUTPUT_LIMIT) {
|
|
826
|
+
if (text.length <= limit)
|
|
827
|
+
return text;
|
|
828
|
+
const head = Math.floor(limit / 5);
|
|
829
|
+
return `${text.slice(0, head)}\n… ${text.length - limit} characters omitted …\n${text.slice(text.length - (limit - head))}`;
|
|
830
|
+
}
|
|
831
|
+
const defaultExecRunner = async (command, args, options) => {
|
|
832
|
+
const resolved = resolveCliSpawn(command, args);
|
|
833
|
+
try {
|
|
834
|
+
const { stdout, stderr } = await run(resolved.command, resolved.args, {
|
|
835
|
+
timeout: options.timeout,
|
|
836
|
+
encoding: "utf8",
|
|
837
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
838
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
839
|
+
});
|
|
840
|
+
return { stdout, stderr, code: 0 };
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
const failure = error;
|
|
844
|
+
// A non-zero exit still carries the output; anything else (runtime missing,
|
|
845
|
+
// client killed) is a real failure to run at all.
|
|
846
|
+
if (typeof failure.code === "number") {
|
|
847
|
+
return { stdout: failure.stdout ?? "", stderr: failure.stderr ?? "", code: failure.code };
|
|
848
|
+
}
|
|
849
|
+
throw Object.assign(new Error(failure.killed ? "the command client timed out" : failure.message ?? "could not run the command"), { status: 502 });
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
/** Run one shell command inside a Local VM as the desktop user, in its durable
|
|
853
|
+
* workspace, and return the exit code and text output. Bots use this instead of
|
|
854
|
+
* typing into a terminal window and reading screenshots. The limit is enforced
|
|
855
|
+
* inside the container so a runaway process is really stopped. */
|
|
856
|
+
export async function containerExec(target, command, options = {}) {
|
|
857
|
+
if (!command.trim())
|
|
858
|
+
throw Object.assign(new Error("command is required"), { status: 400 });
|
|
859
|
+
if (command.length > 20_000)
|
|
860
|
+
throw Object.assign(new Error("command is too long"), { status: 400 });
|
|
861
|
+
if (options.timeoutSeconds !== undefined && !Number.isFinite(options.timeoutSeconds)) {
|
|
862
|
+
throw Object.assign(new Error("timeout_seconds must be finite"), { status: 400 });
|
|
863
|
+
}
|
|
864
|
+
const runtime = options.runtime ?? (await containerRuntimeStatus()).runtime;
|
|
865
|
+
if (!runtime)
|
|
866
|
+
throw Object.assign(new Error("No container runtime is available for the Local VM"), { status: 409 });
|
|
867
|
+
const seconds = Math.min(Math.max(Math.floor(options.timeoutSeconds ?? EXEC_DEFAULT_SECONDS), 1), EXEC_MAX_SECONDS);
|
|
868
|
+
const result = await (options.exec ?? defaultExecRunner)(runtime, [
|
|
869
|
+
"exec",
|
|
870
|
+
"-u", "cua",
|
|
871
|
+
"-w", VM_WORKSPACE_GUEST,
|
|
872
|
+
"-e", "HOME=/home/cua",
|
|
873
|
+
"-e", `DISPLAY=${DISPLAY}`,
|
|
874
|
+
target.containerName,
|
|
875
|
+
"timeout", "-k", "5", String(seconds),
|
|
876
|
+
"sh", "-lc", command,
|
|
877
|
+
], { timeout: (seconds + 20) * 1000 });
|
|
878
|
+
const timedOut = result.code === 124 || result.code === 137;
|
|
879
|
+
return { exitCode: result.code, stdout: clipExecOutput(result.stdout), stderr: clipExecOutput(result.stderr), timedOut };
|
|
880
|
+
}
|
|
811
881
|
export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
|
|
812
882
|
return {
|
|
813
883
|
command: process.execPath,
|
|
@@ -12,11 +12,43 @@ export class ProviderError extends Error {
|
|
|
12
12
|
this.code = code;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
/** The driver proved this attempt never reached a prompt or client-side action,
|
|
16
|
+
* and its owned process has stopped. The harness may safely recover it. */
|
|
17
|
+
export class TurnNotStartedError extends Error {
|
|
18
|
+
turnId;
|
|
19
|
+
constructor(turnId, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "TurnNotStartedError";
|
|
22
|
+
this.turnId = turnId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
/** Variants are opaque provider IDs, not the cross-engine effort enum. */
|
|
16
26
|
export function isModelVariant(value) {
|
|
17
27
|
return typeof value === "string" && value.length > 0 && value.length <= 256 &&
|
|
18
28
|
value.trim() === value && !/\p{Cc}/u.test(value);
|
|
19
29
|
}
|
|
30
|
+
/** The picker label for a model id when the catalog row carries no display
|
|
31
|
+
* name: split on the word breaks the catalog treats as separators and
|
|
32
|
+
* capitalize each part ("gpt-5.4-mini" → "Gpt 5.4 Mini"). Each harness
|
|
33
|
+
* passes its own break class so existing labels stay byte-for-byte. */
|
|
34
|
+
export function titleCaseModelId(id, wordBreaks) {
|
|
35
|
+
return id
|
|
36
|
+
.split(wordBreaks)
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
39
|
+
.join(" ");
|
|
40
|
+
}
|
|
41
|
+
/** A picker label that carries a second facet — a display name, a provider
|
|
42
|
+
* host — beside the model id, unless the facet is already what the id says.
|
|
43
|
+
* `redundantWhen` decides what counts as "already said" and `decorate` how
|
|
44
|
+
* the facet is appended, so every harness's labels stay exactly what they
|
|
45
|
+
* were while the shape is written once. */
|
|
46
|
+
export function qualifiedModelLabel(id, qualifier, options = {}) {
|
|
47
|
+
const { redundantWhen = () => false, decorate = (facet) => ` — ${facet}` } = options;
|
|
48
|
+
if (!qualifier || redundantWhen(id, qualifier))
|
|
49
|
+
return id;
|
|
50
|
+
return `${id}${decorate(qualifier)}`;
|
|
51
|
+
}
|
|
20
52
|
let eventCounter = 0;
|
|
21
53
|
export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
|
|
22
54
|
export const newId = () => crypto.randomUUID();
|
|
@@ -40,7 +40,7 @@ import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../proc
|
|
|
40
40
|
export function skipSubscriptionAuthForLocalInject(model) {
|
|
41
41
|
return Boolean(decodeInjectId(model));
|
|
42
42
|
}
|
|
43
|
-
import { newEventId, newId } from "../../contracts.js";
|
|
43
|
+
import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
|
|
44
44
|
import { augmentedPath } from "../../env-path.js";
|
|
45
45
|
import { supportsApprovalMode } from "../../../shared/approval-mode.js";
|
|
46
46
|
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
|
|
@@ -50,6 +50,8 @@ import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
|
50
50
|
import { extractMcpImages } from "../../mcp-tool-images.js";
|
|
51
51
|
import { redactSecretsInText } from "../../redact.js";
|
|
52
52
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
53
|
+
import { sessionIdlePolicy } from "../session-idle.js";
|
|
54
|
+
import { classifyError } from "../retry.js";
|
|
53
55
|
/** ACP vendors put the actionable cause in error.data while keeping the
|
|
54
56
|
* JSON-RPC message generic. Only surface known text fields, never a response
|
|
55
57
|
* body/config dump, and redact before bounding the displayed diagnostic. */
|
|
@@ -80,6 +82,9 @@ const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
|
|
|
80
82
|
const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 300_000); // configureSession's per-request default
|
|
81
83
|
const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
|
|
82
84
|
const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
|
|
85
|
+
/** 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. */
|
|
87
|
+
const ACP_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
83
88
|
// Read lazily (not at import) so a test can shorten the window. Unlike the
|
|
84
89
|
// setup calls above, session/prompt legitimately streams for minutes, so a
|
|
85
90
|
// wall-clock deadline would false-positive: this guard only trips when the
|
|
@@ -94,6 +99,20 @@ const promptIdleTimeoutMs = () => {
|
|
|
94
99
|
const ms = Number(raw);
|
|
95
100
|
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
96
101
|
};
|
|
102
|
+
/** Keep a turn's set of running tool calls in step with the agent's
|
|
103
|
+
* `tool_call` / `tool_call_update` notifications: a call is running from
|
|
104
|
+
* the first update that is not terminal until one that is. A `tool_call`
|
|
105
|
+
* without a status is pending (ACP's default); a `tool_call_update` without
|
|
106
|
+
* one leaves the call as it was. */
|
|
107
|
+
function trackRunningTool(current, update, defaultStatus) {
|
|
108
|
+
if (typeof update.toolCallId !== "string" || !update.toolCallId)
|
|
109
|
+
return;
|
|
110
|
+
const status = update.status ?? defaultStatus;
|
|
111
|
+
if (status === "completed" || status === "failed")
|
|
112
|
+
current.runningTools.delete(update.toolCallId);
|
|
113
|
+
else if (status === "pending" || status === "in_progress")
|
|
114
|
+
current.runningTools.add(update.toolCallId);
|
|
115
|
+
}
|
|
97
116
|
const CLIENT_FILE_MAX_BYTES = 8 * 1024 * 1024;
|
|
98
117
|
function acpVariantOption(result) {
|
|
99
118
|
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.type === "select" && typeof entry.id === "string"
|
|
@@ -212,6 +231,24 @@ export function versionFromProbe(stdout, stderr) {
|
|
|
212
231
|
const err = (stderr ?? "").trim().split(/\r\n|\n|\r/, 1)[0]?.trim() ?? "";
|
|
213
232
|
return err || null;
|
|
214
233
|
}
|
|
234
|
+
/** The first dotted numeric triple in a version banner, or null when there is
|
|
235
|
+
* none. A wrapper that prints its own banner first still parses: the triple
|
|
236
|
+
* is the version wherever it appears. */
|
|
237
|
+
export function parseVersionTriple(value) {
|
|
238
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(value);
|
|
239
|
+
if (!match)
|
|
240
|
+
return null;
|
|
241
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
242
|
+
}
|
|
243
|
+
/** Whether `installed` compares at or above `floor`, componentwise. Every
|
|
244
|
+
* version floor in the tree is a dotted triple. */
|
|
245
|
+
export function versionAtLeast(installed, floor) {
|
|
246
|
+
for (let i = 0; i < 3; i += 1) {
|
|
247
|
+
if (installed[i] !== floor[i])
|
|
248
|
+
return installed[i] > floor[i];
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
215
252
|
export function createAcpDriver(support) {
|
|
216
253
|
const DRIVER_KIND = support.driverKind;
|
|
217
254
|
const SOURCE = support.nativeSource;
|
|
@@ -274,11 +311,24 @@ export function createAcpDriver(support) {
|
|
|
274
311
|
// contract prompts the live session instead of paying the handshake
|
|
275
312
|
// again. An idle session closes after SESSION_IDLE_MS of quiet.
|
|
276
313
|
const sessions = new Map();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
314
|
+
// Closing removes a session from the pool, not its native writer lease.
|
|
315
|
+
// Retain every closing Qwen child until its process tree is gone, even
|
|
316
|
+
// when Stop or a failed turn lets another turn start during cleanup.
|
|
317
|
+
const retiring = new Map();
|
|
318
|
+
const retireChild = async (threadId, child) => {
|
|
319
|
+
let children = retiring.get(threadId);
|
|
320
|
+
if (!children)
|
|
321
|
+
retiring.set(threadId, children = new Set());
|
|
322
|
+
children.add(child);
|
|
323
|
+
const stopped = await killCliTree(child);
|
|
324
|
+
if (stopped) {
|
|
325
|
+
children.delete(child);
|
|
326
|
+
if (!children.size && retiring.get(threadId) === children)
|
|
327
|
+
retiring.delete(threadId);
|
|
328
|
+
}
|
|
329
|
+
return stopped;
|
|
330
|
+
};
|
|
331
|
+
const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("ACP");
|
|
282
332
|
const closeSession = (threadId, why) => {
|
|
283
333
|
const session = sessions.get(threadId);
|
|
284
334
|
if (!session || session.closing)
|
|
@@ -290,16 +340,20 @@ export function createAcpDriver(support) {
|
|
|
290
340
|
// a new turn must never adopt a closing session
|
|
291
341
|
sessions.delete(threadId);
|
|
292
342
|
session.acp.close();
|
|
293
|
-
//
|
|
294
|
-
//
|
|
343
|
+
// EOF is not a guaranteed exit signal. Qwen must be tracked before
|
|
344
|
+
// another turn can resume its history; other agents keep their grace.
|
|
295
345
|
try {
|
|
296
346
|
session.child.stdin.end();
|
|
297
347
|
}
|
|
298
348
|
catch { }
|
|
299
|
-
|
|
300
|
-
void
|
|
301
|
-
|
|
302
|
-
|
|
349
|
+
if (support.restartOnMcpChange)
|
|
350
|
+
void retireChild(threadId, session.child);
|
|
351
|
+
else {
|
|
352
|
+
const kill = setTimeout(() => {
|
|
353
|
+
void killCliTree(session.child);
|
|
354
|
+
}, 5_000);
|
|
355
|
+
kill.unref?.();
|
|
356
|
+
}
|
|
303
357
|
};
|
|
304
358
|
const armIdle = (threadId) => {
|
|
305
359
|
const session = sessions.get(threadId);
|
|
@@ -418,6 +472,7 @@ export function createAcpDriver(support) {
|
|
|
418
472
|
if (!current || current.state.settled)
|
|
419
473
|
return;
|
|
420
474
|
current.state.settled = true;
|
|
475
|
+
current.acknowledge();
|
|
421
476
|
if (current.interruptTimer)
|
|
422
477
|
clearTimeout(current.interruptTimer);
|
|
423
478
|
for (const finish of current.asks.values())
|
|
@@ -503,8 +558,9 @@ export function createAcpDriver(support) {
|
|
|
503
558
|
if (idleTimer)
|
|
504
559
|
clearTimeout(idleTimer);
|
|
505
560
|
idleTimer = setTimeout(() => {
|
|
506
|
-
// Waiting for a person
|
|
507
|
-
|
|
561
|
+
// Waiting for a person, or for a tool the agent is running,
|
|
562
|
+
// is not an unresponsive agent.
|
|
563
|
+
if (session.current?.asks.size || session.current?.runningTools.size) {
|
|
508
564
|
armIdle();
|
|
509
565
|
return;
|
|
510
566
|
}
|
|
@@ -621,6 +677,7 @@ export function createAcpDriver(support) {
|
|
|
621
677
|
// server→client permission request → canonical request.opened,
|
|
622
678
|
// answered fail-closed for the running turn
|
|
623
679
|
const handleServerRequest = (msg, current) => {
|
|
680
|
+
current.state.startupActivity = true;
|
|
624
681
|
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
625
682
|
void handleClientFileRequest(msg);
|
|
626
683
|
return;
|
|
@@ -760,6 +817,9 @@ export function createAcpDriver(support) {
|
|
|
760
817
|
current.receiveModelVariants(p.update);
|
|
761
818
|
return;
|
|
762
819
|
}
|
|
820
|
+
if (current && ["agent_message_chunk", "agent_thought_chunk", "tool_call", "tool_call_update"].includes(p.update?.sessionUpdate)) {
|
|
821
|
+
current.state.startupActivity = true;
|
|
822
|
+
}
|
|
763
823
|
if (!current || !current.state.promptSent)
|
|
764
824
|
return;
|
|
765
825
|
const u = p.update ?? {};
|
|
@@ -793,6 +853,7 @@ export function createAcpDriver(support) {
|
|
|
793
853
|
}
|
|
794
854
|
case "tool_call": {
|
|
795
855
|
current.flushAssistantText();
|
|
856
|
+
trackRunningTool(current, u, "pending");
|
|
796
857
|
emit({
|
|
797
858
|
...base(threadId, current.turnId),
|
|
798
859
|
type: "item.started",
|
|
@@ -805,6 +866,7 @@ export function createAcpDriver(support) {
|
|
|
805
866
|
break;
|
|
806
867
|
}
|
|
807
868
|
case "tool_call_update": {
|
|
869
|
+
trackRunningTool(current, u);
|
|
808
870
|
if (u.status === "completed" || u.status === "failed") {
|
|
809
871
|
current.state.producedItem = true;
|
|
810
872
|
emit({
|
|
@@ -992,8 +1054,8 @@ export function createAcpDriver(support) {
|
|
|
992
1054
|
// transformEnv-policy supports (opencode). mcpServers are session
|
|
993
1055
|
// establishment inputs — they ride session/new and session/load over
|
|
994
1056
|
// the wire — and the harness mints fresh integration bearer tokens
|
|
995
|
-
// every turn
|
|
996
|
-
//
|
|
1057
|
+
// every turn. Most agents apply changes on live load; those that cache
|
|
1058
|
+
// the old MCP clients must resume on a fresh process (see sessionKey).
|
|
997
1059
|
// The env the spawned child actually receives is part of the
|
|
998
1060
|
// contract too, and arrives hashed as envFingerprint for the same
|
|
999
1061
|
// reason.
|
|
@@ -1012,7 +1074,8 @@ export function createAcpDriver(support) {
|
|
|
1012
1074
|
}
|
|
1013
1075
|
const pooled = sessions.get(threadId);
|
|
1014
1076
|
let session;
|
|
1015
|
-
if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey
|
|
1077
|
+
if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey
|
|
1078
|
+
&& (!support.restartOnMcpChange || pooled.sessionKey === sessionKey)) {
|
|
1016
1079
|
// adoption cancels the idle countdown — a running turn is not quiet
|
|
1017
1080
|
if (pooled.idleTimer)
|
|
1018
1081
|
clearTimeout(pooled.idleTimer);
|
|
@@ -1035,7 +1098,13 @@ export function createAcpDriver(support) {
|
|
|
1035
1098
|
// respawns the child, every wire call must reach the live record, so
|
|
1036
1099
|
// nothing captures the connection off it.
|
|
1037
1100
|
const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
|
|
1038
|
-
const state = { settled: false, promptSent: false, text: "", producedItem: false };
|
|
1101
|
+
const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false };
|
|
1102
|
+
let acknowledge = () => { };
|
|
1103
|
+
let rejectStartup = (_error) => { };
|
|
1104
|
+
const startupAck = turn.startupRecovery ? new Promise((resolve, reject) => {
|
|
1105
|
+
acknowledge = () => resolve({ turnId });
|
|
1106
|
+
rejectStartup = reject;
|
|
1107
|
+
}) : null;
|
|
1039
1108
|
const asks = new Map();
|
|
1040
1109
|
const modelOf = (result) => {
|
|
1041
1110
|
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.id === (support.selectModel?.configId ?? "model"));
|
|
@@ -1083,12 +1152,15 @@ export function createAcpDriver(support) {
|
|
|
1083
1152
|
turnConfig,
|
|
1084
1153
|
controlsHost,
|
|
1085
1154
|
state,
|
|
1155
|
+
acknowledge,
|
|
1086
1156
|
asks,
|
|
1157
|
+
runningTools: new Set(),
|
|
1087
1158
|
interruptTimer: null,
|
|
1088
1159
|
flushAssistantText,
|
|
1089
1160
|
receiveModelVariants,
|
|
1090
1161
|
};
|
|
1091
1162
|
const interrupt = () => {
|
|
1163
|
+
state.stopped = true;
|
|
1092
1164
|
if (session.sessionId) {
|
|
1093
1165
|
session.acp.send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: session.sessionId } });
|
|
1094
1166
|
if (current.interruptTimer)
|
|
@@ -1105,7 +1177,7 @@ export function createAcpDriver(support) {
|
|
|
1105
1177
|
closeSession(threadId, "stop");
|
|
1106
1178
|
}
|
|
1107
1179
|
};
|
|
1108
|
-
active.set(threadId, { stop: () => closeSession(threadId, "stop"), interrupt, turnId, asks });
|
|
1180
|
+
active.set(threadId, { stop: () => { state.stopped = true; closeSession(threadId, "stop"); }, interrupt, turnId, asks });
|
|
1109
1181
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1110
1182
|
session.current = current;
|
|
1111
1183
|
(async () => {
|
|
@@ -1116,6 +1188,19 @@ export function createAcpDriver(support) {
|
|
|
1116
1188
|
// Returns whether this runtime accepts image prompts, for the
|
|
1117
1189
|
// prompt phase below.
|
|
1118
1190
|
const handshake = async () => {
|
|
1191
|
+
// Also covers timeout/reset/re-establishment, not just rotated
|
|
1192
|
+
// MCP credentials. Stop remains wired while cleanup is pending.
|
|
1193
|
+
for (const child of retiring.get(threadId) ?? []) {
|
|
1194
|
+
const stopped = await retireChild(threadId, child);
|
|
1195
|
+
if (state.settled || session.closing)
|
|
1196
|
+
throw new Error("session closed");
|
|
1197
|
+
if (!stopped) {
|
|
1198
|
+
closeSession(threadId, "replace-failed");
|
|
1199
|
+
throw new Error(`${support.displayName} could not close its previous tool session. Try again.`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (state.settled || session.closing)
|
|
1203
|
+
throw new Error("session closed");
|
|
1119
1204
|
if (!session.initResult) {
|
|
1120
1205
|
session.initResult = await request("initialize", {
|
|
1121
1206
|
protocolVersion: 1,
|
|
@@ -1320,7 +1405,7 @@ export function createAcpDriver(support) {
|
|
|
1320
1405
|
let pendingSplitReceipt = null;
|
|
1321
1406
|
if (halves.stable !== null) {
|
|
1322
1407
|
const receiptKey = JSON.stringify([threadId, sessionId]);
|
|
1323
|
-
const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn));
|
|
1408
|
+
const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn), ACP_PROMPT_RE_ANCHOR_TURNS);
|
|
1324
1409
|
promptInput = { ...promptTurn, system: "", text: composed.text };
|
|
1325
1410
|
pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
1326
1411
|
}
|
|
@@ -1338,10 +1423,13 @@ export function createAcpDriver(support) {
|
|
|
1338
1423
|
if (turn.variant !== undefined && requestedVariantOption().currentValue !== turn.variant) {
|
|
1339
1424
|
throw new Error(`${support.displayName} changed variant before the prompt`);
|
|
1340
1425
|
}
|
|
1426
|
+
if (state.settled || state.stopped || session.closing)
|
|
1427
|
+
throw new Error("turn stopped");
|
|
1341
1428
|
state.promptSent = true;
|
|
1429
|
+
acknowledge();
|
|
1342
1430
|
const promptIdleMs = promptIdleTimeoutMs();
|
|
1343
|
-
const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND}
|
|
1344
|
-
"
|
|
1431
|
+
const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND} sent nothing for ${Math.round(promptIdleMs / 1000)} s with no tool running, so the turn was stopped as stuck. ` +
|
|
1432
|
+
"Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).");
|
|
1345
1433
|
if (pendingSplitReceipt) {
|
|
1346
1434
|
// session/prompt resolving is the acceptance boundary: a
|
|
1347
1435
|
// rejected prompt leaves the receipt unwritten, so the next
|
|
@@ -1387,6 +1475,35 @@ export function createAcpDriver(support) {
|
|
|
1387
1475
|
// fallback for existing ACP supports.
|
|
1388
1476
|
const needsAuth = code === "invalid_credentials" || code === "inactive_subscription"
|
|
1389
1477
|
|| message === support.loginNote;
|
|
1478
|
+
const failure = classifyError({ text: message });
|
|
1479
|
+
const transientStartup = failure.transient || (failure.reason === "unknown" && code === "upstream_outage");
|
|
1480
|
+
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);
|
|
1481
|
+
if (turn.startupRecovery && transientStartup && !needsAuth && !denied && (!code || code === "upstream_outage")
|
|
1482
|
+
&& ![-32700, -32600, -32601, -32602].includes(e?.code)
|
|
1483
|
+
&& !state.promptSent && !state.startupActivity && !state.producedItem && !state.text
|
|
1484
|
+
&& !state.stopped && !session.closing && !asks.size && !current.runningTools.size) {
|
|
1485
|
+
// Quiesce before killing: the child's close event must not
|
|
1486
|
+
// publish a terminal completion before the harness can recover.
|
|
1487
|
+
state.settled = true;
|
|
1488
|
+
session.current = null;
|
|
1489
|
+
if (current.interruptTimer)
|
|
1490
|
+
clearTimeout(current.interruptTimer);
|
|
1491
|
+
closeSession(threadId, "startup-recovery");
|
|
1492
|
+
const stopped = await killCliTree(session.child).catch(() => false);
|
|
1493
|
+
if (current.interruptTimer)
|
|
1494
|
+
clearTimeout(current.interruptTimer);
|
|
1495
|
+
active.delete(threadId);
|
|
1496
|
+
if (stopped && !state.stopped) {
|
|
1497
|
+
rejectStartup(new TurnNotStartedError(turnId, message));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
acknowledge();
|
|
1501
|
+
if (!state.stopped)
|
|
1502
|
+
emit({ ...base(threadId, turnId), type: "runtime.error", message });
|
|
1503
|
+
emit({ ...base(threadId, turnId), type: "turn.completed", ok: state.stopped,
|
|
1504
|
+
stopReason: state.stopped ? "cancelled" : "rpc_error", cost: null });
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1390
1507
|
emit({
|
|
1391
1508
|
...base(threadId, turnId),
|
|
1392
1509
|
type: "runtime.error",
|
|
@@ -1406,7 +1523,7 @@ export function createAcpDriver(support) {
|
|
|
1406
1523
|
}
|
|
1407
1524
|
}
|
|
1408
1525
|
})();
|
|
1409
|
-
return { turnId };
|
|
1526
|
+
return startupAck ?? { turnId };
|
|
1410
1527
|
};
|
|
1411
1528
|
const snapshot = async () => {
|
|
1412
1529
|
const env = childEnv();
|
|
@@ -1463,6 +1580,10 @@ export function createAcpDriver(support) {
|
|
|
1463
1580
|
// idle pooled sessions have no running turn — close them too
|
|
1464
1581
|
for (const threadId of Array.from(sessions.keys()))
|
|
1465
1582
|
closeSession(threadId, "stopAll");
|
|
1583
|
+
for (const [threadId, children] of retiring) {
|
|
1584
|
+
for (const child of children)
|
|
1585
|
+
void retireChild(threadId, child);
|
|
1586
|
+
}
|
|
1466
1587
|
},
|
|
1467
1588
|
onEvent: (listener) => {
|
|
1468
1589
|
listeners.add(listener);
|
|
@@ -1474,6 +1595,10 @@ export function createAcpDriver(support) {
|
|
|
1474
1595
|
stop();
|
|
1475
1596
|
for (const threadId of Array.from(sessions.keys()))
|
|
1476
1597
|
closeSession(threadId, "dispose");
|
|
1598
|
+
for (const [threadId, children] of retiring) {
|
|
1599
|
+
for (const child of children)
|
|
1600
|
+
void retireChild(threadId, child);
|
|
1601
|
+
}
|
|
1477
1602
|
listeners.clear();
|
|
1478
1603
|
},
|
|
1479
1604
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { titleCaseModelId } from "../../contracts.js";
|
|
1
2
|
import { execCli } from "../../procs.js";
|
|
2
3
|
import { createAcpDriver } from "./core.js";
|
|
3
4
|
/** Translate an argv `--model` slug into the id this ACP session will accept.
|
|
@@ -86,13 +87,8 @@ function asRecord(value) {
|
|
|
86
87
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
87
88
|
}
|
|
88
89
|
function labelFor(id, explicit) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return id
|
|
92
|
-
.split(/[-_./]+/g)
|
|
93
|
-
.filter(Boolean)
|
|
94
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
95
|
-
.join(" ");
|
|
90
|
+
const trimmed = explicit?.trim();
|
|
91
|
+
return trimmed || titleCaseModelId(id, /[-_./]+/);
|
|
96
92
|
}
|
|
97
93
|
function pushModel(options, seen, id, label, custom = false) {
|
|
98
94
|
if (!SLUG.test(id) || seen.has(id))
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// options set over the wire (session/set_model, session/set_mode), which is
|
|
17
17
|
// why both live in configureSession() below and NOT in spawnArgs.
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
19
|
import { join } from "node:path";
|
|
20
|
+
import { userHome } from "../../env-path.js";
|
|
21
21
|
import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
|
|
22
22
|
import { createAcpDriver } from "./core.js";
|
|
23
23
|
// FACTORY_HOME_OVERRIDE replaces the HOME the CLI resolves, NOT the data root:
|
|
@@ -34,7 +34,7 @@ import { createAcpDriver } from "./core.js";
|
|
|
34
34
|
// out. Names read from the 0.196.0 binary.
|
|
35
35
|
const AUTH_FILES = ["auth.v2.file", "auth.v2.loginkeychain", "auth.v2.keyring"];
|
|
36
36
|
function authFilePaths(env) {
|
|
37
|
-
const home = env.FACTORY_HOME_OVERRIDE || env
|
|
37
|
+
const home = env.FACTORY_HOME_OVERRIDE || userHome(env);
|
|
38
38
|
return AUTH_FILES.map((name) => join(home, ".factory", name));
|
|
39
39
|
}
|
|
40
40
|
const INJECT_ID_PREFIX = "custom:openmausbot-";
|
|
@@ -42,9 +42,6 @@ export function droidInjectId(host, model) {
|
|
|
42
42
|
const safe = `${host}-${model}`.replace(/[^a-zA-Z0-9._+-]+/g, "-").replace(/-+/g, "-");
|
|
43
43
|
return `${INJECT_ID_PREFIX}${safe}`;
|
|
44
44
|
}
|
|
45
|
-
function factoryHome(env) {
|
|
46
|
-
return env.FACTORY_HOME_OVERRIDE || env.HOME || env.USERPROFILE || homedir();
|
|
47
|
-
}
|
|
48
45
|
/** Upsert a BYOK custom model so session/set_model can reach the local host. */
|
|
49
46
|
export function ensureDroidInjectModel(modelId, env = process.env) {
|
|
50
47
|
const inject = decodeInjectId(modelId);
|
|
@@ -54,7 +51,7 @@ export function ensureDroidInjectModel(modelId, env = process.env) {
|
|
|
54
51
|
if (!host)
|
|
55
52
|
return modelId;
|
|
56
53
|
const id = droidInjectId(inject.host, inject.model);
|
|
57
|
-
const dir = join(
|
|
54
|
+
const dir = join(env.FACTORY_HOME_OVERRIDE || userHome(env), ".factory");
|
|
58
55
|
mkdirSync(dir, { recursive: true });
|
|
59
56
|
const path = join(dir, "settings.json");
|
|
60
57
|
let settings = {};
|
|
@@ -105,7 +102,7 @@ export function applyDroidLocalAuthEnv(env, modelId) {
|
|
|
105
102
|
env.FACTORY_API_KEY = "openmausbot-local";
|
|
106
103
|
}
|
|
107
104
|
function readSettings(env) {
|
|
108
|
-
return JSON.parse(readFileSync(join(
|
|
105
|
+
return JSON.parse(readFileSync(join(env.FACTORY_HOME_OVERRIDE || userHome(env), ".factory", "settings.json"), "utf8"));
|
|
109
106
|
}
|
|
110
107
|
async function resolveModels(env) {
|
|
111
108
|
let catalog = MODELS;
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
// the ACP flag + auth method ids follow the published Gemini CLI ACP contract
|
|
27
27
|
// and should be re-verified end-to-end once the CLI is present.
|
|
28
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
-
import { homedir } from "node:os";
|
|
30
29
|
import { join } from "node:path";
|
|
30
|
+
import { userHome } from "../../env-path.js";
|
|
31
31
|
import { createAcpDriver } from "./core.js";
|
|
32
32
|
// Prefer an explicit key method, then personal OAuth, then Vertex — but fall
|
|
33
33
|
// back to whatever the CLI advertises so a new method id still works.
|
|
@@ -37,9 +37,9 @@ const AUTH_PREFERENCE = ["gemini-api-key", "oauth-personal", "vertex-ai"];
|
|
|
37
37
|
* an enterprise licence may still redeem. A stale consumer credential —
|
|
38
38
|
* the common case since 2026-06-18 — reads as not signed in, which is the
|
|
39
39
|
* truthful answer for an engine that would fail its first turn anyway. */
|
|
40
|
-
function liveOauthCredential() {
|
|
40
|
+
function liveOauthCredential(env) {
|
|
41
41
|
try {
|
|
42
|
-
const creds = JSON.parse(readFileSync(join(
|
|
42
|
+
const creds = JSON.parse(readFileSync(join(userHome(env), ".gemini", "oauth_creds.json"), "utf8"));
|
|
43
43
|
if (typeof creds.access_token !== "string" || creds.access_token.trim().length === 0)
|
|
44
44
|
return false;
|
|
45
45
|
const hasRefresh = typeof creds.refresh_token === "string" && creds.refresh_token.trim().length > 0;
|
|
@@ -62,7 +62,7 @@ const nonBlank = (value) => Boolean(value?.trim());
|
|
|
62
62
|
export function geminiIsAuthenticated(env) {
|
|
63
63
|
return (nonBlank(env.GEMINI_API_KEY) ||
|
|
64
64
|
nonBlank(env.GOOGLE_API_KEY) ||
|
|
65
|
-
(existsSync(join(
|
|
65
|
+
(existsSync(join(userHome(env), ".gemini", "oauth_creds.json")) && liveOauthCredential(env)));
|
|
66
66
|
}
|
|
67
67
|
/** Gemini CLI's approval modes, passed through: `--approval-mode auto_edit`
|
|
68
68
|
* approves file edits, `--yolo` approves everything; Gemini has no
|