openmausbot 0.1.88 → 0.1.90
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 +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-qd9VD3GV.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
- package/dist/assets/{arc-3RwkUjRe.js → arc-q73qM-At.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CFKSJS7a.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CG1rEfWD.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-CBW9N664.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
- package/dist/assets/channel-BCuyHjFV.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-2mRfTksB.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-Cv1dpErj.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B7IACJp-.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-An0hZtYa.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BcJB2nEm.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CV_W7TN7.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Bcs_kwnA.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-lrx_DxFP.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DabHlLGM.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BnHj6P7d.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CfLp6UDi.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Dm4dbVA1.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-B9mH-PID.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DDc_NVQJ.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-lwr8Ip8-.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BLLe6dmI.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-6eUIq4lz.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-O-pRN7VQ.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BietvGAI.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Bd2Cl62X.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-B6iDy69W.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DQVHfKe0.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BWdVp2tQ.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-WLE6rKB4.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BbjS-4gn.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dpy2QSGq.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
- package/dist/assets/index-CRvo-DS2.css +1 -0
- package/dist/assets/index-CWYGlA63.js +588 -0
- package/dist/assets/{index-CIaHn5NS.js → index-G1PHKGyh.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-D_jKWlKe.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DLJohSuK.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DPOFj0qN.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BI7v-xpK.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
- package/dist/assets/{linear-C2pWPSxO.js → linear-Dwc3Hrru.js} +1 -1
- package/dist/assets/{mermaid.core-CR-j5WQU.js → mermaid.core-Dc4OePBL.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-lz9GvXRh.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-Dl4_ia61.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DWuT3n88.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DXWe9Jx7.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-COY1IBVY.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DnyVuhsQ.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BSc4NdGK.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-kqEU2EGq.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BvB4bkWD.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BIrqNv7y.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CFnalQUE.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CsSWJnSQ.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DkAmJ1EE.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C3vd9drL.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-j26CM-pi.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BYu1Q_at.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14795 -0
- package/dist-server/container-mcp.js +60 -4
- package/dist-server/drivers/agents-proxy.js +20 -10
- package/dist-server/index.js +48924 -46153
- package/dist-server/openmausbot.js +544 -253
- package/dist-server/pair-cli.js +544 -253
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/automatic-recovery.js +15 -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} +330 -328
- 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/cli-api-setup.js +0 -1
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +103 -0
- package/dist-server/server/cloud-home.js +218 -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 +124 -6
- package/dist-server/server/container-computer.js +23 -0
- package/dist-server/server/contracts.js +10 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +129 -21
- package/dist-server/server/drivers/acp/qwen.js +3 -0
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +9 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +14 -13
- 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 +177 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +19 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/harness-capabilities.js +18 -3
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/index.js +1163 -530
- 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/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/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/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +25 -1
- package/dist-server/server/surface.js +1 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-computers.js +2 -2
- 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 +1 -1
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/webhooks.js +21 -3
- 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/credential-request.js +3 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +26 -0
- package/dist-server/vps-container-mcp.js +60 -4
- package/dist-server/workspace-backup.worker.js +60 -4
- package/package.json +1 -1
- package/dist/assets/channel-CnRnO3tF.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-CHnODCr2.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +0 -8
|
@@ -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";
|
|
@@ -435,7 +435,7 @@ function askQuestions(ask) {
|
|
|
435
435
|
function askSummary(ask) {
|
|
436
436
|
const questions = askQuestions(ask);
|
|
437
437
|
if (questions)
|
|
438
|
-
return askQuestionSummary(questions)
|
|
438
|
+
return askQuestionSummary(questions);
|
|
439
439
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
440
440
|
}
|
|
441
441
|
/** Where the hook helper reads this thread's current turn token. Stable per
|
|
@@ -752,6 +752,109 @@ function firstText(content) {
|
|
|
752
752
|
}
|
|
753
753
|
return "";
|
|
754
754
|
}
|
|
755
|
+
/** A turn's own cost from the CLI's total_cost_usd, which is not a per-turn
|
|
756
|
+
* figure: it is "cumulative across turns in streaming-input sessions — each
|
|
757
|
+
* result carries the running total so far" (2.1.282), and a retained process
|
|
758
|
+
* runs turn after turn. So a turn costs the growth since the total its
|
|
759
|
+
* process reported for the turn before — or, for a process's first turn,
|
|
760
|
+
* since the total the CLI restored on --resume (see restoredCostBase). With
|
|
761
|
+
* no known start (null) the turn keeps its whole figure. A total that went
|
|
762
|
+
* down is not the same count, so it is taken whole too rather than booked as
|
|
763
|
+
* a negative cost. Rounding to 1e-10 USD removes only the float noise of the
|
|
764
|
+
* subtraction. */
|
|
765
|
+
export function turnCostFromRunningTotal(total, previous) {
|
|
766
|
+
if (total === null)
|
|
767
|
+
return null;
|
|
768
|
+
if (previous === null || total < previous)
|
|
769
|
+
return total;
|
|
770
|
+
return Number((total - previous).toFixed(10));
|
|
771
|
+
}
|
|
772
|
+
export function claudeCostSnapshot(total, modelUsage) {
|
|
773
|
+
if (typeof total !== "number" || !Number.isFinite(total))
|
|
774
|
+
return null;
|
|
775
|
+
const count = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
776
|
+
const models = {};
|
|
777
|
+
if (modelUsage && typeof modelUsage === "object" && !Array.isArray(modelUsage)) {
|
|
778
|
+
for (const [model, raw] of Object.entries(modelUsage)) {
|
|
779
|
+
if (!raw || typeof raw !== "object")
|
|
780
|
+
continue;
|
|
781
|
+
const u = raw;
|
|
782
|
+
models[model] = [count(u.inputTokens), count(u.cacheReadInputTokens), count(u.cacheCreationInputTokens), count(u.outputTokens)];
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return { total, models };
|
|
786
|
+
}
|
|
787
|
+
/** The running total a resumed session already carried before this
|
|
788
|
+
* process's first turn. On --resume the CLI (2.1.282) restores the session's
|
|
789
|
+
* cost from an earlier state — not always the latest one this driver saw —
|
|
790
|
+
* so that turn's total_cost_usd and modelUsage include the earlier turns.
|
|
791
|
+
* The restored state is the earlier state that sits inside the new counts
|
|
792
|
+
* and leaves exactly this turn's own usage: in one model (usage leaves out
|
|
793
|
+
* side calls such as a Haiku title) or summed over all models (a turn split
|
|
794
|
+
* between two); nothing restored is 0. When no state fits exactly — the CLI
|
|
795
|
+
* saved work that never reported a result, like an interrupted turn — the
|
|
796
|
+
* latest state inside the new counts stands, so that work is booked once,
|
|
797
|
+
* with this turn. Either way the latest state wins, not the highest total:
|
|
798
|
+
* a resume that went back to an older state leaves later, lower totals. */
|
|
799
|
+
export function restoredCostBase(earlier, current, usage) {
|
|
800
|
+
const turn = [usage.input, usage.cacheRead, usage.cacheWrite, usage.output];
|
|
801
|
+
const nothing = { total: 0, models: {} };
|
|
802
|
+
let exact = null;
|
|
803
|
+
let inside = 0;
|
|
804
|
+
// oldest first: the session's states in the order they were recorded
|
|
805
|
+
for (const state of [nothing, ...earlier]) {
|
|
806
|
+
const within = Object.entries(state.models).every(([model, counts]) => counts.every((n, i) => n <= (current.models[model]?.[i] ?? 0)));
|
|
807
|
+
if (!within)
|
|
808
|
+
continue;
|
|
809
|
+
inside = state.total;
|
|
810
|
+
const growth = Object.entries(current.models).map(([model, counts]) => counts.map((n, i) => n - (state.models[model]?.[i] ?? 0)));
|
|
811
|
+
const isTurn = (counts) => counts.every((n, i) => n === turn[i]);
|
|
812
|
+
const summed = turn.map((_, i) => growth.reduce((sum, counts) => sum + counts[i], 0));
|
|
813
|
+
if (growth.some(isTurn) || isTurn(summed))
|
|
814
|
+
exact = state.total;
|
|
815
|
+
}
|
|
816
|
+
return exact ?? inside;
|
|
817
|
+
}
|
|
818
|
+
/** Each Claude session's latest cost states, so the first turn after a
|
|
819
|
+
* --resume can tell what the CLI restored — after an app restart too. Small
|
|
820
|
+
* by design: a few states for the most recent sessions. */
|
|
821
|
+
const COST_HISTORY_FILE = join(DATA_DIR, "claude-cost-history.json");
|
|
822
|
+
const COST_HISTORY_SESSIONS = 100;
|
|
823
|
+
const COST_HISTORY_STATES = 8;
|
|
824
|
+
function isCostSnapshot(value) {
|
|
825
|
+
if (!value || typeof value !== "object")
|
|
826
|
+
return false;
|
|
827
|
+
const { total, models } = value;
|
|
828
|
+
return typeof total === "number" && !!models && typeof models === "object" &&
|
|
829
|
+
Object.values(models).every((counts) => Array.isArray(counts) && counts.length === 4 && counts.every((n) => typeof n === "number"));
|
|
830
|
+
}
|
|
831
|
+
function readCostHistory() {
|
|
832
|
+
try {
|
|
833
|
+
const parsed = JSON.parse(readFileSync(COST_HISTORY_FILE, "utf8"));
|
|
834
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
835
|
+
return {};
|
|
836
|
+
return Object.fromEntries(Object.entries(parsed).map(([id, states]) => [id, Array.isArray(states) ? states.filter(isCostSnapshot) : []]));
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return {};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function recordCostState(sessionId, state) {
|
|
843
|
+
const history = readCostHistory();
|
|
844
|
+
const states = [...(history[sessionId] ?? []), state].slice(-COST_HISTORY_STATES);
|
|
845
|
+
// most recent session last, so the oldest ones are dropped first
|
|
846
|
+
delete history[sessionId];
|
|
847
|
+
history[sessionId] = states;
|
|
848
|
+
const ids = Object.keys(history);
|
|
849
|
+
for (const id of ids.slice(0, Math.max(0, ids.length - COST_HISTORY_SESSIONS)))
|
|
850
|
+
delete history[id];
|
|
851
|
+
try {
|
|
852
|
+
writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600 });
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// a lost state only means a later resume keeps its whole figure
|
|
856
|
+
}
|
|
857
|
+
}
|
|
755
858
|
/** Claude's stream-json input accepts the same image source blocks as the
|
|
756
859
|
* Anthropic Messages API. Keep the old string form for text-only turns so a
|
|
757
860
|
* CLI update cannot disturb the overwhelmingly common path. */
|
|
@@ -1155,7 +1258,7 @@ export const ClaudeDriver = {
|
|
|
1155
1258
|
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
|
|
1156
1259
|
allowed.push("mcp__ogb");
|
|
1157
1260
|
// The MCP config carries credentials — a Composio consumer key in a
|
|
1158
|
-
// header, the
|
|
1261
|
+
// header, the boat token in the computer proxy's env, the comms token in
|
|
1159
1262
|
// the agents proxy's env. On argv every one of those is world-readable
|
|
1160
1263
|
// through `ps` for the life of the turn, to any local process. The CLI
|
|
1161
1264
|
// accepts a FILE for this flag, so the secrets go in a 0600 file that
|
|
@@ -1414,11 +1517,12 @@ export const ClaudeDriver = {
|
|
|
1414
1517
|
idleTimer: null,
|
|
1415
1518
|
closing: false,
|
|
1416
1519
|
stderr: "",
|
|
1520
|
+
costTotal: undefined,
|
|
1417
1521
|
};
|
|
1418
1522
|
sessions.set(threadId, session);
|
|
1419
1523
|
// settles the TURN, not the process: the CLI stays for the next
|
|
1420
1524
|
// message until it has been quiet for SESSION_IDLE_MS
|
|
1421
|
-
const settle = (ok, stopReason,
|
|
1525
|
+
const settle = (ok, stopReason, total = null, usage) => {
|
|
1422
1526
|
const t = session.turn;
|
|
1423
1527
|
if (!t || t.settled)
|
|
1424
1528
|
return;
|
|
@@ -1450,11 +1554,31 @@ export const ClaudeDriver = {
|
|
|
1450
1554
|
// retry resumes on a fresh process; healthy sibling sessions stay warm.
|
|
1451
1555
|
if (stopReason === "update_required")
|
|
1452
1556
|
closeSession(threadId, "update required");
|
|
1557
|
+
// `total` is the CLI's running total for this process; the harness
|
|
1558
|
+
// books turn.completed.cost as this turn's own spend
|
|
1559
|
+
const cost = turnCostFromRunningTotal(total, session.costTotal ?? null);
|
|
1560
|
+
if (total !== null)
|
|
1561
|
+
session.costTotal = total;
|
|
1453
1562
|
emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
|
|
1454
1563
|
if (session.child.exitCode === null && !session.closing)
|
|
1455
1564
|
armIdle(threadId);
|
|
1456
1565
|
};
|
|
1457
1566
|
const currentTurnId = () => session.turn?.turnId ?? turnId;
|
|
1567
|
+
// The process's first result with a cost says what --resume restored,
|
|
1568
|
+
// which its turns are measured from; every result is kept for a later
|
|
1569
|
+
// resume. A result without one (an API error) decides nothing yet.
|
|
1570
|
+
const noteCostState = (total, modelUsage, usage) => {
|
|
1571
|
+
const state = claudeCostSnapshot(total, modelUsage);
|
|
1572
|
+
if (!state)
|
|
1573
|
+
return;
|
|
1574
|
+
if (session.costTotal === undefined) {
|
|
1575
|
+
session.costTotal = session.sessionId
|
|
1576
|
+
? restoredCostBase(readCostHistory()[session.sessionId] ?? [], state, usage)
|
|
1577
|
+
: null;
|
|
1578
|
+
}
|
|
1579
|
+
if (session.sessionId)
|
|
1580
|
+
recordCostState(session.sessionId, state);
|
|
1581
|
+
};
|
|
1458
1582
|
const handleLine = (line) => {
|
|
1459
1583
|
if (session.closing)
|
|
1460
1584
|
return;
|
|
@@ -1575,11 +1699,19 @@ export const ClaudeDriver = {
|
|
|
1575
1699
|
// and deny approvals while that user turn is still running.
|
|
1576
1700
|
if (o.origin?.kind === "task-notification")
|
|
1577
1701
|
break;
|
|
1578
|
-
// result.usage is this
|
|
1579
|
-
//
|
|
1580
|
-
// are billed (at the cache rate)
|
|
1581
|
-
// they
|
|
1582
|
-
//
|
|
1702
|
+
// result.usage is this turn's own figure, "per-turn in
|
|
1703
|
+
// streaming-input sessions" (2.1.282) even on a retained process.
|
|
1704
|
+
// cache reads count as input: they are billed (at the cache rate)
|
|
1705
|
+
// and they fill the window — but they are reported separately
|
|
1706
|
+
// too, so the UI can show how much of the figure was context
|
|
1707
|
+
// re-read rather than new text. total_cost_usd is instead the
|
|
1708
|
+
// process's running total; settle() books this turn's share.
|
|
1709
|
+
noteCostState(o.total_cost_usd, o.modelUsage, {
|
|
1710
|
+
input: o.usage?.input_tokens || 0,
|
|
1711
|
+
cacheRead: o.usage?.cache_read_input_tokens || 0,
|
|
1712
|
+
cacheWrite: o.usage?.cache_creation_input_tokens || 0,
|
|
1713
|
+
output: o.usage?.output_tokens || 0,
|
|
1714
|
+
});
|
|
1583
1715
|
settle(o.is_error !== true, session.turn?.authFailed
|
|
1584
1716
|
? "auth_required"
|
|
1585
1717
|
: session.turn?.updateRequired
|
|
@@ -1877,10 +2009,11 @@ export const ClaudeDriver = {
|
|
|
1877
2009
|
* summaries can contain paths, commands, or secrets, so the generic
|
|
1878
2010
|
* `claude -p "prompt"` shape is not safe for review. No tools or MCP
|
|
1879
2011
|
* servers are mounted in this isolated process. */
|
|
1880
|
-
const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
|
|
1881
|
-
const
|
|
2012
|
+
const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
|
|
2013
|
+
const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
|
|
2014
|
+
const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
|
|
1882
2015
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1883
|
-
env: environment(
|
|
2016
|
+
env: environment(model),
|
|
1884
2017
|
});
|
|
1885
2018
|
let stdout = "";
|
|
1886
2019
|
let stderr = "";
|
|
@@ -1919,6 +2052,37 @@ export const ClaudeDriver = {
|
|
|
1919
2052
|
});
|
|
1920
2053
|
child.on("error", (error) => finish(error));
|
|
1921
2054
|
child.on("close", (code) => {
|
|
2055
|
+
if (settled)
|
|
2056
|
+
return;
|
|
2057
|
+
if (onUsage) {
|
|
2058
|
+
try {
|
|
2059
|
+
const result = JSON.parse(stdout);
|
|
2060
|
+
if (!result || result.type !== "result")
|
|
2061
|
+
throw new Error("Claude text generation returned no result");
|
|
2062
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
2063
|
+
const cachedInput = count(result.usage?.cache_read_input_tokens);
|
|
2064
|
+
const inputs = [count(result.usage?.input_tokens), cachedInput, count(result.usage?.cache_creation_input_tokens)]
|
|
2065
|
+
.filter((value) => value !== undefined);
|
|
2066
|
+
const models = result.modelUsage && typeof result.modelUsage === "object" && !Array.isArray(result.modelUsage)
|
|
2067
|
+
? Object.keys(result.modelUsage) : [];
|
|
2068
|
+
onUsage({
|
|
2069
|
+
model: models.length === 1 ? models[0] : models.find(candidate => candidate === model || candidate.startsWith(`${model}-`)) ?? model,
|
|
2070
|
+
input: inputs.length ? inputs.reduce((sum, value) => sum + value, 0) : undefined,
|
|
2071
|
+
output: count(result.usage?.output_tokens),
|
|
2072
|
+
cachedInput,
|
|
2073
|
+
costUsd: count(result.total_cost_usd),
|
|
2074
|
+
});
|
|
2075
|
+
if (result.is_error === true)
|
|
2076
|
+
throw new Error(typeof result.result === "string" && result.result.trim() ? result.result : stderr.trim() || "Claude text generation failed");
|
|
2077
|
+
if (typeof result.result !== "string")
|
|
2078
|
+
throw new Error("Claude text generation returned no text");
|
|
2079
|
+
stdout = result.result;
|
|
2080
|
+
}
|
|
2081
|
+
catch (error) {
|
|
2082
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1922
2086
|
if (code === 0)
|
|
1923
2087
|
finish();
|
|
1924
2088
|
else
|
|
@@ -2000,7 +2164,7 @@ export const ClaudeDriver = {
|
|
|
2000
2164
|
return () => listeners.delete(listener);
|
|
2001
2165
|
},
|
|
2002
2166
|
},
|
|
2003
|
-
generateText: (prompt, options) => generateReview(prompt, options?.signal),
|
|
2167
|
+
generateText: (prompt, options) => generateReview(prompt, options?.signal, options?.onUsage),
|
|
2004
2168
|
reviewPermission: generateReview,
|
|
2005
2169
|
dispose: async () => {
|
|
2006
2170
|
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)}`);
|
|
@@ -617,7 +628,11 @@ export function createOpenAIChatRuntime(options) {
|
|
|
617
628
|
: { state: "unavailable", reason: options.unavailableReason },
|
|
618
629
|
adapter: {
|
|
619
630
|
provider: options.driverKind,
|
|
620
|
-
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
|
|
631
|
+
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
|
|
632
|
+
// Same gate as cloudComputerMcp: with tools off the runtime cannot
|
|
633
|
+
// mount the leased Boat descriptor either. The fleet invariant test
|
|
634
|
+
// pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
|
|
635
|
+
usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
621
636
|
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
622
637
|
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
|
|
623
638
|
// The runtime owns the whole tool loop, so it can always take a
|
|
@@ -657,9 +672,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
657
672
|
return () => listeners.delete(listener);
|
|
658
673
|
},
|
|
659
674
|
},
|
|
660
|
-
generateText: async (prompt, { signal } = {}) => {
|
|
675
|
+
generateText: async (prompt, { signal, onUsage } = {}) => {
|
|
661
676
|
const model = options.generateModel?.() ?? options.models().default;
|
|
662
|
-
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);
|
|
663
678
|
if (toolCalls.length)
|
|
664
679
|
throw new ChatProtocolError("provider returned tool calls to a text-only helper");
|
|
665
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 {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// uses one envelope profile and lets the per-driver capability decide the
|
|
8
8
|
// delta; the tool names themselves come from the real catalog, so this
|
|
9
9
|
// never drifts from what a turn is actually shown.
|
|
10
|
-
import { APPROVAL_MODES, supportsApprovalMode } from "../shared/approval-mode.js";
|
|
10
|
+
import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
|
|
11
11
|
import { availableTools } from "./drivers/agents-catalog.js";
|
|
12
12
|
/** The widest turn the harness can build: every optional integration on and
|
|
13
13
|
* a coordinating (room) turn, where the room-only coordination tools exist.
|
|
@@ -35,8 +35,8 @@ export function harnessCapabilityLines(from, to) {
|
|
|
35
35
|
: "Loses peer coordination and every team tool: " + names.length + " tools including " + coordination.join(", ") + " go away.");
|
|
36
36
|
}
|
|
37
37
|
if (from.driverKind !== to.driverKind) {
|
|
38
|
-
const lost = APPROVAL_MODES.filter((mode) =>
|
|
39
|
-
const gained = APPROVAL_MODES.filter((mode) => !
|
|
38
|
+
const lost = APPROVAL_MODES.filter((mode) => driverKindSupportsApprovalMode(from.driverKind, mode) && !driverKindSupportsApprovalMode(to.driverKind, mode));
|
|
39
|
+
const gained = APPROVAL_MODES.filter((mode) => !driverKindSupportsApprovalMode(from.driverKind, mode) && driverKindSupportsApprovalMode(to.driverKind, mode));
|
|
40
40
|
if (lost.length)
|
|
41
41
|
lines.push("Loses approval levels: " + lost.join(", ") + ".");
|
|
42
42
|
if (gained.length)
|
|
@@ -44,3 +44,18 @@ export function harnessCapabilityLines(from, to) {
|
|
|
44
44
|
}
|
|
45
45
|
return lines;
|
|
46
46
|
}
|
|
47
|
+
/** Binds the registry to the approval-support check so each gate passes its
|
|
48
|
+
* model selection — or a driver view it already holds — and stops
|
|
49
|
+
* unwrapping registry.cliTarget(...)?.driverKind at every call site. The
|
|
50
|
+
* lookup prefers shadow state, so a mid-reconfig instance answers with the
|
|
51
|
+
* driver it would actually run. */
|
|
52
|
+
export function createApprovalModeSupport(registry) {
|
|
53
|
+
return function supportsApprovalMode(target, mode) {
|
|
54
|
+
const driverKind = target === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: "driverKind" in target
|
|
57
|
+
? target.driverKind
|
|
58
|
+
: registry.cliTarget(target.instanceId)?.driverKind;
|
|
59
|
+
return driverKindSupportsApprovalMode(driverKind, mode);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -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.");
|