openmausbot 0.1.89 → 0.1.91
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/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14810 -0
- package/dist-server/container-mcp.js +136 -36
- package/dist-server/drivers/agents-proxy.js +31 -10
- package/dist-server/index.js +48251 -44124
- package/dist-server/openmausbot.js +628 -292
- package/dist-server/pair-cli.js +628 -292
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +381 -334
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +121 -0
- package/dist-server/server/cloud-home.js +241 -0
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +156 -9
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +25 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +178 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1462 -576
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +42 -3
- package/dist-server/server/surface.js +14 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +12 -7
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +136 -36
- package/dist-server/workspace-backup.worker.js +165 -35
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
|
+
import { boatCredential, boatProviderApi } from "../included-services.js";
|
|
2
3
|
import { appendNative } from "./native.js";
|
|
3
4
|
import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
|
|
5
|
+
// "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
|
|
4
6
|
const DRIVER_KIND = "boxAgent";
|
|
5
|
-
// overridable so tests and a dev backend can be pointed at instead of the live provider
|
|
6
|
-
const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
|
|
7
7
|
const MODELS = {
|
|
8
8
|
default: "claude-fable-5",
|
|
9
9
|
options: [
|
|
@@ -15,14 +15,19 @@ const MODELS = {
|
|
|
15
15
|
/** Any fence whose info string names the ask protocol, even when its body
|
|
16
16
|
* does not parse: the marker for "the model tried to ask and failed". */
|
|
17
17
|
const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
|
|
18
|
-
/** The
|
|
18
|
+
/** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
|
|
19
19
|
* prime-agent, kimi). Which one a model id belongs to comes from the public
|
|
20
20
|
* catalog, `GET /api/provider-models` at the API root: an object keyed by
|
|
21
21
|
* harness, each with its `models`. A bot that arrives here from another engine
|
|
22
|
-
* carries that engine's model id, so this is what lets it keep its model.
|
|
22
|
+
* carries that engine's model id, so this is what lets it keep its model. The
|
|
23
|
+
* root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
|
|
23
24
|
let catalog = null;
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let catalogRoot = "";
|
|
26
|
+
async function loadCatalog(api) {
|
|
27
|
+
const root = api.replace(/\/api\/box\/v1\/?$/, "");
|
|
28
|
+
if (catalog && catalogRoot === root)
|
|
29
|
+
return;
|
|
30
|
+
catalogRoot = root;
|
|
26
31
|
catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
|
|
27
32
|
.then((res) => (res.ok ? res.json() : null))
|
|
28
33
|
.catch(() => null);
|
|
@@ -44,7 +49,7 @@ function decodeConfig(raw) {
|
|
|
44
49
|
...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
|
-
export const
|
|
52
|
+
export const BoatAgentDriver = {
|
|
48
53
|
driverKind: DRIVER_KIND,
|
|
49
54
|
metadata: { displayName: "Computer", supportsMultipleInstances: false },
|
|
50
55
|
models: MODELS,
|
|
@@ -52,7 +57,11 @@ export const BoxAgentDriver = {
|
|
|
52
57
|
defaultConfig: () => decodeConfig({}),
|
|
53
58
|
async create(input) {
|
|
54
59
|
const { instanceId, config } = input;
|
|
55
|
-
|
|
60
|
+
// The person's own token (instanceConfigs injects it), else Cloud Pro's
|
|
61
|
+
// included one, with the one base URL each may be sent to. Resolved per
|
|
62
|
+
// request, like every other Boat call.
|
|
63
|
+
const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
|
|
64
|
+
const account = () => boatCredential(ownToken);
|
|
56
65
|
const listeners = new Set();
|
|
57
66
|
const active = new Map();
|
|
58
67
|
/** One open ask per thread: the turn-held transport's pending card. The
|
|
@@ -74,9 +83,10 @@ export const BoxAgentDriver = {
|
|
|
74
83
|
createdAt: new Date().toISOString(),
|
|
75
84
|
});
|
|
76
85
|
const api = async (path, opts = {}) => {
|
|
77
|
-
const
|
|
86
|
+
const credential = account();
|
|
87
|
+
const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
|
|
78
88
|
...opts,
|
|
79
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...opts.headers },
|
|
89
|
+
headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
|
|
80
90
|
signal: opts.signal ?? AbortSignal.timeout(30_000),
|
|
81
91
|
});
|
|
82
92
|
const body = await res.json().catch(() => null);
|
|
@@ -89,7 +99,7 @@ export const BoxAgentDriver = {
|
|
|
89
99
|
const { threadId } = turn;
|
|
90
100
|
const computer = turn.integrations?.computer;
|
|
91
101
|
const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
|
|
92
|
-
if (!
|
|
102
|
+
if (!account())
|
|
93
103
|
throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
|
|
94
104
|
if (!boxId) {
|
|
95
105
|
throw new Error("this bot has no computer yet — open the Computer panel and provision one");
|
|
@@ -112,15 +122,14 @@ export const BoxAgentDriver = {
|
|
|
112
122
|
.filter((s) => s !== undefined)
|
|
113
123
|
.join("\n");
|
|
114
124
|
const postPrompt = async (promptText) => {
|
|
115
|
-
|
|
116
|
-
await loadCatalog();
|
|
125
|
+
await loadCatalog(account()?.api ?? boatProviderApi());
|
|
117
126
|
const started = await api(`/boxes/${boxId}/prompt`, {
|
|
118
127
|
method: "POST",
|
|
119
128
|
body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
|
|
120
129
|
});
|
|
121
130
|
appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
|
|
122
131
|
// real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
|
|
123
|
-
// id:<
|
|
132
|
+
// id:<boat id>} — never fall back to the bare id, it's the boat's
|
|
124
133
|
return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
|
|
125
134
|
};
|
|
126
135
|
const promptId = await postPrompt(prompt);
|
|
@@ -149,7 +158,7 @@ export const BoxAgentDriver = {
|
|
|
149
158
|
const seen = new Set();
|
|
150
159
|
let lastText = "";
|
|
151
160
|
let pendingText = "";
|
|
152
|
-
/** Why the
|
|
161
|
+
/** Why the boat could not answer (login expired, model refused, …). */
|
|
153
162
|
let problem = null;
|
|
154
163
|
/** Emit unflushed deltas as assistant_text and reset pendingText.
|
|
155
164
|
* The omb-ask block is protocol, not prose: it streamed raw (the
|
|
@@ -229,7 +238,7 @@ export const BoxAgentDriver = {
|
|
|
229
238
|
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
230
239
|
}
|
|
231
240
|
// The turn is still open, so this prompt continues it: same turnId,
|
|
232
|
-
// same accounting, and the answer reaches the
|
|
241
|
+
// same accounting, and the answer reaches the boat the way the ask
|
|
233
242
|
// contract promised — Q:/A: blocks, capped for echo.
|
|
234
243
|
const continuation = [
|
|
235
244
|
capAnswerEcho(answerWithoutPreamble(reply)),
|
|
@@ -238,7 +247,7 @@ export const BoxAgentDriver = {
|
|
|
238
247
|
].join("\n");
|
|
239
248
|
const nextPromptId = await postPrompt(continuation);
|
|
240
249
|
// Stop can land while the continuation POST is in flight: the
|
|
241
|
-
// interrupt inside cancel() then hits a
|
|
250
|
+
// interrupt inside cancel() then hits a boat with no active run,
|
|
242
251
|
// and the continuation would start after it. Interrupt the run
|
|
243
252
|
// that just started before ending the turn.
|
|
244
253
|
if (cancelled) {
|
|
@@ -247,9 +256,9 @@ export const BoxAgentDriver = {
|
|
|
247
256
|
}
|
|
248
257
|
return await settleRun(nextPromptId);
|
|
249
258
|
};
|
|
250
|
-
/** Poll one
|
|
259
|
+
/** Poll one boat run to its settle. */
|
|
251
260
|
const settleRun = async (runPromptId) => {
|
|
252
|
-
const startedAt = Date.now(); // one 30-min ceiling per
|
|
261
|
+
const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
|
|
253
262
|
lastText = "";
|
|
254
263
|
pendingText = "";
|
|
255
264
|
problem = null;
|
|
@@ -354,8 +363,8 @@ export const BoxAgentDriver = {
|
|
|
354
363
|
return { turnId };
|
|
355
364
|
};
|
|
356
365
|
const snapshot = async () => {
|
|
357
|
-
if (!
|
|
358
|
-
return { state: "unavailable", reason: 'no
|
|
366
|
+
if (!account()) {
|
|
367
|
+
return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
|
|
359
368
|
}
|
|
360
369
|
try {
|
|
361
370
|
await api("/me");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AntigravityDriver } from "./antigravity.js";
|
|
2
|
-
import {
|
|
2
|
+
import { BoatAgentDriver } from "./boatagent.js";
|
|
3
3
|
import { ClaudeDriver } from "./claude.js";
|
|
4
4
|
import { CodexDriver } from "./codex.js";
|
|
5
5
|
import { GrokDriver } from "./grok.js";
|
|
@@ -32,7 +32,7 @@ export const BUILT_IN_DRIVERS = [
|
|
|
32
32
|
ClaudeDriver,
|
|
33
33
|
CodexDriver,
|
|
34
34
|
AntigravityDriver,
|
|
35
|
-
|
|
35
|
+
BoatAgentDriver,
|
|
36
36
|
MinimaxDriver,
|
|
37
37
|
MistralDriver,
|
|
38
38
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isolatedRemoteCommand, runCommand,
|
|
1
|
+
import { isolatedRemoteCommand, runCommand, screenshotBoat } from "../boat.js";
|
|
2
2
|
import { createControlClient, CONTROL_REFUSAL_PLAIN } from "../control-client.js";
|
|
3
3
|
const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
|
|
4
4
|
const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
|
|
@@ -19,7 +19,7 @@ const buttons = { left: 1, middle: 2, right: 3 };
|
|
|
19
19
|
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
20
20
|
/** Uses only the leased descriptor supplied by the harness. Schema validation
|
|
21
21
|
* and user approval happen in the shared chat tool executor before this call. */
|
|
22
|
-
export class
|
|
22
|
+
export class ChatBoatClient {
|
|
23
23
|
closed = false;
|
|
24
24
|
control;
|
|
25
25
|
descriptor;
|
|
@@ -45,7 +45,7 @@ export class ChatBoxClient {
|
|
|
45
45
|
const config = { box: { token: this.descriptor.token } };
|
|
46
46
|
const id = this.descriptor.boxId;
|
|
47
47
|
if (name === "screenshot") {
|
|
48
|
-
const shot = await
|
|
48
|
+
const shot = await screenshotBoat(config, "", id, { signal, nativeSize: true });
|
|
49
49
|
signal.throwIfAborted();
|
|
50
50
|
return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
|
|
51
51
|
}
|
|
@@ -7,7 +7,7 @@ import { stripControlPlaneEnv } from "../config.js";
|
|
|
7
7
|
import { augmentedPath } from "../env-path.js";
|
|
8
8
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
9
9
|
import { chatImage } from "./chat-images.js";
|
|
10
|
-
import {
|
|
10
|
+
import { ChatBoatClient } from "./chat-boat-tools.js";
|
|
11
11
|
/** The transport cannot safely continue this turn. A dispatched operation may
|
|
12
12
|
* already have taken effect, so callers must not retry it through a new round. */
|
|
13
13
|
export class ChatToolSessionError extends Error {
|
|
@@ -314,7 +314,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
314
314
|
throw aborted();
|
|
315
315
|
// Every mounted MCP server can return images when the caller enables
|
|
316
316
|
// image delivery, including custom servers. Text stays bounded below.
|
|
317
|
-
const client = "boxId" in descriptor ? new
|
|
317
|
+
const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
|
|
318
318
|
clients.push(client);
|
|
319
319
|
return { name, client, tools: await client.tools(signal) };
|
|
320
320
|
}));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Integrations become MCP servers on the CLI:
|
|
8
8
|
// - Composio Sessions (connected apps → tools) over streamable HTTP
|
|
9
9
|
// - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
|
|
10
|
-
// — screenshot/exec/open_url, the CUA-on-the-
|
|
10
|
+
// — screenshot/exec/open_url, the CUA-on-the-boat bridge
|
|
11
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
12
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { createServer as createNetServer } from "node:net";
|
|
@@ -328,6 +328,7 @@ export const STATIC_CLAUDE_MODELS = {
|
|
|
328
328
|
{ id: "claude-fable-5", label: "Claude Fable 5" },
|
|
329
329
|
{ id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
|
|
330
330
|
{ id: "claude-opus-5", label: "Claude Opus 5" },
|
|
331
|
+
{ id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", contextWindow: 1_000_000 },
|
|
331
332
|
{ id: "claude-sonnet-5", label: "Claude Sonnet 5" },
|
|
332
333
|
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
333
334
|
],
|
|
@@ -435,7 +436,7 @@ function askQuestions(ask) {
|
|
|
435
436
|
function askSummary(ask) {
|
|
436
437
|
const questions = askQuestions(ask);
|
|
437
438
|
if (questions)
|
|
438
|
-
return askQuestionSummary(questions)
|
|
439
|
+
return askQuestionSummary(questions);
|
|
439
440
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
440
441
|
}
|
|
441
442
|
/** Where the hook helper reads this thread's current turn token. Stable per
|
|
@@ -752,6 +753,109 @@ function firstText(content) {
|
|
|
752
753
|
}
|
|
753
754
|
return "";
|
|
754
755
|
}
|
|
756
|
+
/** A turn's own cost from the CLI's total_cost_usd, which is not a per-turn
|
|
757
|
+
* figure: it is "cumulative across turns in streaming-input sessions — each
|
|
758
|
+
* result carries the running total so far" (2.1.282), and a retained process
|
|
759
|
+
* runs turn after turn. So a turn costs the growth since the total its
|
|
760
|
+
* process reported for the turn before — or, for a process's first turn,
|
|
761
|
+
* since the total the CLI restored on --resume (see restoredCostBase). With
|
|
762
|
+
* no known start (null) the turn keeps its whole figure. A total that went
|
|
763
|
+
* down is not the same count, so it is taken whole too rather than booked as
|
|
764
|
+
* a negative cost. Rounding to 1e-10 USD removes only the float noise of the
|
|
765
|
+
* subtraction. */
|
|
766
|
+
export function turnCostFromRunningTotal(total, previous) {
|
|
767
|
+
if (total === null)
|
|
768
|
+
return null;
|
|
769
|
+
if (previous === null || total < previous)
|
|
770
|
+
return total;
|
|
771
|
+
return Number((total - previous).toFixed(10));
|
|
772
|
+
}
|
|
773
|
+
export function claudeCostSnapshot(total, modelUsage) {
|
|
774
|
+
if (typeof total !== "number" || !Number.isFinite(total))
|
|
775
|
+
return null;
|
|
776
|
+
const count = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
777
|
+
const models = {};
|
|
778
|
+
if (modelUsage && typeof modelUsage === "object" && !Array.isArray(modelUsage)) {
|
|
779
|
+
for (const [model, raw] of Object.entries(modelUsage)) {
|
|
780
|
+
if (!raw || typeof raw !== "object")
|
|
781
|
+
continue;
|
|
782
|
+
const u = raw;
|
|
783
|
+
models[model] = [count(u.inputTokens), count(u.cacheReadInputTokens), count(u.cacheCreationInputTokens), count(u.outputTokens)];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return { total, models };
|
|
787
|
+
}
|
|
788
|
+
/** The running total a resumed session already carried before this
|
|
789
|
+
* process's first turn. On --resume the CLI (2.1.282) restores the session's
|
|
790
|
+
* cost from an earlier state — not always the latest one this driver saw —
|
|
791
|
+
* so that turn's total_cost_usd and modelUsage include the earlier turns.
|
|
792
|
+
* The restored state is the earlier state that sits inside the new counts
|
|
793
|
+
* and leaves exactly this turn's own usage: in one model (usage leaves out
|
|
794
|
+
* side calls such as a Haiku title) or summed over all models (a turn split
|
|
795
|
+
* between two); nothing restored is 0. When no state fits exactly — the CLI
|
|
796
|
+
* saved work that never reported a result, like an interrupted turn — the
|
|
797
|
+
* latest state inside the new counts stands, so that work is booked once,
|
|
798
|
+
* with this turn. Either way the latest state wins, not the highest total:
|
|
799
|
+
* a resume that went back to an older state leaves later, lower totals. */
|
|
800
|
+
export function restoredCostBase(earlier, current, usage) {
|
|
801
|
+
const turn = [usage.input, usage.cacheRead, usage.cacheWrite, usage.output];
|
|
802
|
+
const nothing = { total: 0, models: {} };
|
|
803
|
+
let exact = null;
|
|
804
|
+
let inside = 0;
|
|
805
|
+
// oldest first: the session's states in the order they were recorded
|
|
806
|
+
for (const state of [nothing, ...earlier]) {
|
|
807
|
+
const within = Object.entries(state.models).every(([model, counts]) => counts.every((n, i) => n <= (current.models[model]?.[i] ?? 0)));
|
|
808
|
+
if (!within)
|
|
809
|
+
continue;
|
|
810
|
+
inside = state.total;
|
|
811
|
+
const growth = Object.entries(current.models).map(([model, counts]) => counts.map((n, i) => n - (state.models[model]?.[i] ?? 0)));
|
|
812
|
+
const isTurn = (counts) => counts.every((n, i) => n === turn[i]);
|
|
813
|
+
const summed = turn.map((_, i) => growth.reduce((sum, counts) => sum + counts[i], 0));
|
|
814
|
+
if (growth.some(isTurn) || isTurn(summed))
|
|
815
|
+
exact = state.total;
|
|
816
|
+
}
|
|
817
|
+
return exact ?? inside;
|
|
818
|
+
}
|
|
819
|
+
/** Each Claude session's latest cost states, so the first turn after a
|
|
820
|
+
* --resume can tell what the CLI restored — after an app restart too. Small
|
|
821
|
+
* by design: a few states for the most recent sessions. */
|
|
822
|
+
const COST_HISTORY_FILE = join(DATA_DIR, "claude-cost-history.json");
|
|
823
|
+
const COST_HISTORY_SESSIONS = 100;
|
|
824
|
+
const COST_HISTORY_STATES = 8;
|
|
825
|
+
function isCostSnapshot(value) {
|
|
826
|
+
if (!value || typeof value !== "object")
|
|
827
|
+
return false;
|
|
828
|
+
const { total, models } = value;
|
|
829
|
+
return typeof total === "number" && !!models && typeof models === "object" &&
|
|
830
|
+
Object.values(models).every((counts) => Array.isArray(counts) && counts.length === 4 && counts.every((n) => typeof n === "number"));
|
|
831
|
+
}
|
|
832
|
+
function readCostHistory() {
|
|
833
|
+
try {
|
|
834
|
+
const parsed = JSON.parse(readFileSync(COST_HISTORY_FILE, "utf8"));
|
|
835
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
836
|
+
return {};
|
|
837
|
+
return Object.fromEntries(Object.entries(parsed).map(([id, states]) => [id, Array.isArray(states) ? states.filter(isCostSnapshot) : []]));
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return {};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function recordCostState(sessionId, state) {
|
|
844
|
+
const history = readCostHistory();
|
|
845
|
+
const states = [...(history[sessionId] ?? []), state].slice(-COST_HISTORY_STATES);
|
|
846
|
+
// most recent session last, so the oldest ones are dropped first
|
|
847
|
+
delete history[sessionId];
|
|
848
|
+
history[sessionId] = states;
|
|
849
|
+
const ids = Object.keys(history);
|
|
850
|
+
for (const id of ids.slice(0, Math.max(0, ids.length - COST_HISTORY_SESSIONS)))
|
|
851
|
+
delete history[id];
|
|
852
|
+
try {
|
|
853
|
+
writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600 });
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// a lost state only means a later resume keeps its whole figure
|
|
857
|
+
}
|
|
858
|
+
}
|
|
755
859
|
/** Claude's stream-json input accepts the same image source blocks as the
|
|
756
860
|
* Anthropic Messages API. Keep the old string form for text-only turns so a
|
|
757
861
|
* CLI update cannot disturb the overwhelmingly common path. */
|
|
@@ -1155,7 +1259,7 @@ export const ClaudeDriver = {
|
|
|
1155
1259
|
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
|
|
1156
1260
|
allowed.push("mcp__ogb");
|
|
1157
1261
|
// The MCP config carries credentials — a Composio consumer key in a
|
|
1158
|
-
// header, the
|
|
1262
|
+
// header, the boat token in the computer proxy's env, the comms token in
|
|
1159
1263
|
// the agents proxy's env. On argv every one of those is world-readable
|
|
1160
1264
|
// through `ps` for the life of the turn, to any local process. The CLI
|
|
1161
1265
|
// accepts a FILE for this flag, so the secrets go in a 0600 file that
|
|
@@ -1414,11 +1518,12 @@ export const ClaudeDriver = {
|
|
|
1414
1518
|
idleTimer: null,
|
|
1415
1519
|
closing: false,
|
|
1416
1520
|
stderr: "",
|
|
1521
|
+
costTotal: undefined,
|
|
1417
1522
|
};
|
|
1418
1523
|
sessions.set(threadId, session);
|
|
1419
1524
|
// settles the TURN, not the process: the CLI stays for the next
|
|
1420
1525
|
// message until it has been quiet for SESSION_IDLE_MS
|
|
1421
|
-
const settle = (ok, stopReason,
|
|
1526
|
+
const settle = (ok, stopReason, total = null, usage) => {
|
|
1422
1527
|
const t = session.turn;
|
|
1423
1528
|
if (!t || t.settled)
|
|
1424
1529
|
return;
|
|
@@ -1450,11 +1555,31 @@ export const ClaudeDriver = {
|
|
|
1450
1555
|
// retry resumes on a fresh process; healthy sibling sessions stay warm.
|
|
1451
1556
|
if (stopReason === "update_required")
|
|
1452
1557
|
closeSession(threadId, "update required");
|
|
1558
|
+
// `total` is the CLI's running total for this process; the harness
|
|
1559
|
+
// books turn.completed.cost as this turn's own spend
|
|
1560
|
+
const cost = turnCostFromRunningTotal(total, session.costTotal ?? null);
|
|
1561
|
+
if (total !== null)
|
|
1562
|
+
session.costTotal = total;
|
|
1453
1563
|
emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
|
|
1454
1564
|
if (session.child.exitCode === null && !session.closing)
|
|
1455
1565
|
armIdle(threadId);
|
|
1456
1566
|
};
|
|
1457
1567
|
const currentTurnId = () => session.turn?.turnId ?? turnId;
|
|
1568
|
+
// The process's first result with a cost says what --resume restored,
|
|
1569
|
+
// which its turns are measured from; every result is kept for a later
|
|
1570
|
+
// resume. A result without one (an API error) decides nothing yet.
|
|
1571
|
+
const noteCostState = (total, modelUsage, usage) => {
|
|
1572
|
+
const state = claudeCostSnapshot(total, modelUsage);
|
|
1573
|
+
if (!state)
|
|
1574
|
+
return;
|
|
1575
|
+
if (session.costTotal === undefined) {
|
|
1576
|
+
session.costTotal = session.sessionId
|
|
1577
|
+
? restoredCostBase(readCostHistory()[session.sessionId] ?? [], state, usage)
|
|
1578
|
+
: null;
|
|
1579
|
+
}
|
|
1580
|
+
if (session.sessionId)
|
|
1581
|
+
recordCostState(session.sessionId, state);
|
|
1582
|
+
};
|
|
1458
1583
|
const handleLine = (line) => {
|
|
1459
1584
|
if (session.closing)
|
|
1460
1585
|
return;
|
|
@@ -1575,11 +1700,19 @@ export const ClaudeDriver = {
|
|
|
1575
1700
|
// and deny approvals while that user turn is still running.
|
|
1576
1701
|
if (o.origin?.kind === "task-notification")
|
|
1577
1702
|
break;
|
|
1578
|
-
// result.usage is this
|
|
1579
|
-
//
|
|
1580
|
-
// are billed (at the cache rate)
|
|
1581
|
-
// they
|
|
1582
|
-
//
|
|
1703
|
+
// result.usage is this turn's own figure, "per-turn in
|
|
1704
|
+
// streaming-input sessions" (2.1.282) even on a retained process.
|
|
1705
|
+
// cache reads count as input: they are billed (at the cache rate)
|
|
1706
|
+
// and they fill the window — but they are reported separately
|
|
1707
|
+
// too, so the UI can show how much of the figure was context
|
|
1708
|
+
// re-read rather than new text. total_cost_usd is instead the
|
|
1709
|
+
// process's running total; settle() books this turn's share.
|
|
1710
|
+
noteCostState(o.total_cost_usd, o.modelUsage, {
|
|
1711
|
+
input: o.usage?.input_tokens || 0,
|
|
1712
|
+
cacheRead: o.usage?.cache_read_input_tokens || 0,
|
|
1713
|
+
cacheWrite: o.usage?.cache_creation_input_tokens || 0,
|
|
1714
|
+
output: o.usage?.output_tokens || 0,
|
|
1715
|
+
});
|
|
1583
1716
|
settle(o.is_error !== true, session.turn?.authFailed
|
|
1584
1717
|
? "auth_required"
|
|
1585
1718
|
: session.turn?.updateRequired
|
|
@@ -1877,10 +2010,11 @@ export const ClaudeDriver = {
|
|
|
1877
2010
|
* summaries can contain paths, commands, or secrets, so the generic
|
|
1878
2011
|
* `claude -p "prompt"` shape is not safe for review. No tools or MCP
|
|
1879
2012
|
* servers are mounted in this isolated process. */
|
|
1880
|
-
const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
|
|
1881
|
-
const
|
|
2013
|
+
const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
|
|
2014
|
+
const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
|
|
2015
|
+
const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
|
|
1882
2016
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1883
|
-
env: environment(
|
|
2017
|
+
env: environment(model),
|
|
1884
2018
|
});
|
|
1885
2019
|
let stdout = "";
|
|
1886
2020
|
let stderr = "";
|
|
@@ -1919,6 +2053,37 @@ export const ClaudeDriver = {
|
|
|
1919
2053
|
});
|
|
1920
2054
|
child.on("error", (error) => finish(error));
|
|
1921
2055
|
child.on("close", (code) => {
|
|
2056
|
+
if (settled)
|
|
2057
|
+
return;
|
|
2058
|
+
if (onUsage) {
|
|
2059
|
+
try {
|
|
2060
|
+
const result = JSON.parse(stdout);
|
|
2061
|
+
if (!result || result.type !== "result")
|
|
2062
|
+
throw new Error("Claude text generation returned no result");
|
|
2063
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
2064
|
+
const cachedInput = count(result.usage?.cache_read_input_tokens);
|
|
2065
|
+
const inputs = [count(result.usage?.input_tokens), cachedInput, count(result.usage?.cache_creation_input_tokens)]
|
|
2066
|
+
.filter((value) => value !== undefined);
|
|
2067
|
+
const models = result.modelUsage && typeof result.modelUsage === "object" && !Array.isArray(result.modelUsage)
|
|
2068
|
+
? Object.keys(result.modelUsage) : [];
|
|
2069
|
+
onUsage({
|
|
2070
|
+
model: models.length === 1 ? models[0] : models.find(candidate => candidate === model || candidate.startsWith(`${model}-`)) ?? model,
|
|
2071
|
+
input: inputs.length ? inputs.reduce((sum, value) => sum + value, 0) : undefined,
|
|
2072
|
+
output: count(result.usage?.output_tokens),
|
|
2073
|
+
cachedInput,
|
|
2074
|
+
costUsd: count(result.total_cost_usd),
|
|
2075
|
+
});
|
|
2076
|
+
if (result.is_error === true)
|
|
2077
|
+
throw new Error(typeof result.result === "string" && result.result.trim() ? result.result : stderr.trim() || "Claude text generation failed");
|
|
2078
|
+
if (typeof result.result !== "string")
|
|
2079
|
+
throw new Error("Claude text generation returned no text");
|
|
2080
|
+
stdout = result.result;
|
|
2081
|
+
}
|
|
2082
|
+
catch (error) {
|
|
2083
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1922
2087
|
if (code === 0)
|
|
1923
2088
|
finish();
|
|
1924
2089
|
else
|
|
@@ -2000,7 +2165,7 @@ export const ClaudeDriver = {
|
|
|
2000
2165
|
return () => listeners.delete(listener);
|
|
2001
2166
|
},
|
|
2002
2167
|
},
|
|
2003
|
-
generateText: (prompt, options) => generateReview(prompt, options?.signal),
|
|
2168
|
+
generateText: (prompt, options) => generateReview(prompt, options?.signal, options?.onUsage),
|
|
2004
2169
|
reviewPermission: generateReview,
|
|
2005
2170
|
dispose: async () => {
|
|
2006
2171
|
try {
|
|
@@ -11,7 +11,7 @@ export function appendNative(threadId, entry) {
|
|
|
11
11
|
const file = join(NATIVE_DIR, `${threadId}.ndjson`);
|
|
12
12
|
try {
|
|
13
13
|
// The session-setup messages carry the credentials the agent is handed —
|
|
14
|
-
// the
|
|
14
|
+
// the boat and comms tokens ride inside session/new's mcpServers env, and
|
|
15
15
|
// an MCP header can carry a Composio key. These files are ordinary
|
|
16
16
|
// 0644 files people paste into bug reports, so values are masked while
|
|
17
17
|
// the shape stays intact.
|
|
@@ -64,7 +64,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
64
64
|
turnId,
|
|
65
65
|
createdAt: new Date().toISOString(),
|
|
66
66
|
});
|
|
67
|
-
const complete = async (messages, model, stream, signal, onDelta, tools = []) => {
|
|
67
|
+
const complete = async (messages, model, stream, signal, onDelta, tools = [], onUsage) => {
|
|
68
68
|
// Idle timer that is renewed on every received chunk during streaming
|
|
69
69
|
const timeoutController = new AbortController();
|
|
70
70
|
let idleTimer = null;
|
|
@@ -99,6 +99,17 @@ export function createOpenAIChatRuntime(options) {
|
|
|
99
99
|
}
|
|
100
100
|
if (!stream || response.headers.get("content-type")?.includes("application/json")) {
|
|
101
101
|
const json = await response.json();
|
|
102
|
+
if (onUsage) {
|
|
103
|
+
activeSignal.throwIfAborted();
|
|
104
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
105
|
+
onUsage({
|
|
106
|
+
model: typeof json.model === "string" && json.model.trim() ? json.model : model,
|
|
107
|
+
input: count(json.usage?.prompt_tokens),
|
|
108
|
+
output: count(json.usage?.completion_tokens),
|
|
109
|
+
cachedInput: count(json.usage?.prompt_tokens_details?.cached_tokens),
|
|
110
|
+
costUsd: count(json.usage?.cost),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
102
113
|
const bodyError = providerError(json);
|
|
103
114
|
if (bodyError)
|
|
104
115
|
throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
|
|
@@ -619,7 +630,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
619
630
|
provider: options.driverKind,
|
|
620
631
|
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
|
|
621
632
|
// Same gate as cloudComputerMcp: with tools off the runtime cannot
|
|
622
|
-
// mount the leased
|
|
633
|
+
// mount the leased Boat descriptor either. The fleet invariant test
|
|
623
634
|
// pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
|
|
624
635
|
usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
625
636
|
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
@@ -661,9 +672,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
661
672
|
return () => listeners.delete(listener);
|
|
662
673
|
},
|
|
663
674
|
},
|
|
664
|
-
generateText: async (prompt, { signal } = {}) => {
|
|
675
|
+
generateText: async (prompt, { signal, onUsage } = {}) => {
|
|
665
676
|
const model = options.generateModel?.() ?? options.models().default;
|
|
666
|
-
const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal);
|
|
677
|
+
const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal, undefined, [], onUsage);
|
|
667
678
|
if (toolCalls.length)
|
|
668
679
|
throw new ChatProtocolError("provider returned tool calls to a text-only helper");
|
|
669
680
|
return text.trim() ? text : reasoning;
|
|
@@ -465,7 +465,7 @@ export const PiDriver = {
|
|
|
465
465
|
ensurePiInjectModel(turn.model, { ...process.env, ...input.environment });
|
|
466
466
|
}
|
|
467
467
|
// integrations → stdio MCP servers for the pi-mcp-extension. The config
|
|
468
|
-
// carries credentials (
|
|
468
|
+
// carries credentials (boat token, composio key, comms token), so it goes
|
|
469
469
|
// into a 0600 temp file removed when the turn settles — never on argv.
|
|
470
470
|
const mcpServers = buildMcpServers(turn);
|
|
471
471
|
let mcpTempDir = null;
|
|
@@ -476,7 +476,7 @@ export const PiDriver = {
|
|
|
476
476
|
}
|
|
477
477
|
catch (err) {
|
|
478
478
|
// A failed write must not leave the temp dir behind — a partial file
|
|
479
|
-
// could still hold the
|
|
479
|
+
// could still hold the boat token / composio key / comms token.
|
|
480
480
|
try {
|
|
481
481
|
rmSync(mcpTempDir, { recursive: true, force: true });
|
|
482
482
|
}
|
|
@@ -488,7 +488,7 @@ export const PiDriver = {
|
|
|
488
488
|
}
|
|
489
489
|
const childArgs = mcpServers ? [...PI_ARGS, "-e", SPAWNED_PROXIES.piMcpExtension] : PI_ARGS;
|
|
490
490
|
// spawnCli can throw synchronously (unresolvable CLI); if it does, the
|
|
491
|
-
// 0600 temp file with the
|
|
491
|
+
// 0600 temp file with the boat token / composio key / comms token must
|
|
492
492
|
// not be left on disk — settle() never runs because no child existed.
|
|
493
493
|
const child = (() => {
|
|
494
494
|
try {
|
|
@@ -66,12 +66,14 @@ export function parseGroupGoalDecision(text) {
|
|
|
66
66
|
}
|
|
67
67
|
return { visibleText, decision: null };
|
|
68
68
|
}
|
|
69
|
-
/** Explicit room lead
|
|
70
|
-
* first active member. A goal run never recruits
|
|
69
|
+
/** Explicit room lead (or an Auto room's fallback) wins. Otherwise prefer
|
|
70
|
+
* an in-room Chief, then the first active member. A goal run never recruits
|
|
71
|
+
* somebody outside the room. */
|
|
71
72
|
export function selectGroupGoalCoordinator(members, responder) {
|
|
72
73
|
const active = members.filter((member) => !member.hidden);
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const lead = responder.kind === "member" ? responder.botId : responder.kind === "auto" ? responder.fallbackBotId : undefined;
|
|
75
|
+
if (lead) {
|
|
76
|
+
const explicit = active.find((member) => member.id === lead);
|
|
75
77
|
if (explicit)
|
|
76
78
|
return explicit;
|
|
77
79
|
}
|
|
@@ -20,6 +20,8 @@ const ENVELOPE = {
|
|
|
20
20
|
skillAuthoring: true,
|
|
21
21
|
sharedComputers: true,
|
|
22
22
|
voiceNotes: true,
|
|
23
|
+
// Not a tool it adds: a Cloud home only hides its Local VM shell.
|
|
24
|
+
cloudHome: false,
|
|
23
25
|
botId: "",
|
|
24
26
|
};
|
|
25
27
|
/** Extra card lines for an engine switch, empty when nothing changes. */
|
|
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.js";
|
|
5
|
+
import { cloudHomeConfigured } from "./cloud-home.js";
|
|
5
6
|
import { hostedWorkspaceConfiguration } from "./enterprise.js";
|
|
6
7
|
export const HOSTED_MODEL_POLICY_HEADER = "X-Omb-Hosted-Model-Policy";
|
|
7
8
|
export const HOSTED_MODEL_SETUP_ERROR = "No company models are assigned to this workspace. Ask your administrator to enable model access in Admin.";
|
|
@@ -15,10 +16,14 @@ const catalogSchema = z.object({
|
|
|
15
16
|
}).strict();
|
|
16
17
|
const providerFor = { claude: "anthropic", codex: "openai", opencode: "openrouter" };
|
|
17
18
|
/** Operator-only policy. An ordinary desktop has neither input and keeps its
|
|
18
|
-
* personal providers. Partial policy configuration must never enable them.
|
|
19
|
+
* personal providers. Partial policy configuration must never enable them.
|
|
20
|
+
* A Cloud home never routes to a model gateway: it ignores both inputs
|
|
21
|
+
* (cloud-home.ts), and its person signs in with their own engines. */
|
|
19
22
|
export function hostedModelPolicy(dataDirectory, env = process.env) {
|
|
20
23
|
if (env.OMB_HOSTED_MODELS === undefined && env.OMB_HOSTED_MODEL_TOKEN === undefined)
|
|
21
24
|
return null;
|
|
25
|
+
if (cloudHomeConfigured(env))
|
|
26
|
+
return null;
|
|
22
27
|
const hosted = hostedWorkspaceConfiguration(env);
|
|
23
28
|
if (!hosted?.portalMembership || env.OMB_DESKTOP_PARENT === "1" || !/^omb_workspace_[A-Za-z0-9_-]{43}$/.test(env.OMB_HOSTED_MODEL_TOKEN ?? "")) {
|
|
24
29
|
throw new Error("Hosted model access requires complete portal-managed configuration.");
|