openmausbot 0.1.89 → 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/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-q73qM-At.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
- package/dist/assets/channel-BCuyHjFV.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.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-Oxk9x45i.js → index-G1PHKGyh.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-Dwc3Hrru.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-Dc4OePBL.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.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 +52 -4
- package/dist-server/drivers/agents-proxy.js +20 -10
- package/dist-server/index.js +8985 -6461
- package/dist-server/openmausbot.js +327 -145
- package/dist-server/pair-cli.js +327 -145
- 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} +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/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 +115 -6
- package/dist-server/server/container-computer.js +13 -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 +5 -4
- 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} +13 -12
- 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 +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/index.js +1029 -479
- 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 +5 -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/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 +9 -0
- package/dist-server/vps-container-mcp.js +52 -4
- package/dist-server/workspace-backup.worker.js +52 -4
- 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
|
@@ -69,6 +69,19 @@ export function perBotLocalVmTarget(botId) {
|
|
|
69
69
|
label: digest,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
/** A pool-mode seat (issue #1654): one of N desktops shared by every
|
|
73
|
+
* conversation, addressed by seat index. Identities stay disjoint from the
|
|
74
|
+
* shared singleton and from per-bot digests, so discovery, labels, and lease
|
|
75
|
+
* lanes never confuse the three modes. */
|
|
76
|
+
export function poolLocalVmTarget(seat) {
|
|
77
|
+
return {
|
|
78
|
+
key: `pool:${seat}`,
|
|
79
|
+
containerName: `${CONTAINER}-p${seat}`,
|
|
80
|
+
workspaceDir: join(DATA_DIR, "vm-homes", `pool-${seat}`),
|
|
81
|
+
viewerPort: null,
|
|
82
|
+
label: `pool-${seat}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
72
85
|
/** Only provisioning creates this durable directory; idle removal keeps it. */
|
|
73
86
|
export function localVmWorkspaceExists(target) {
|
|
74
87
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The proxy-side half of computer control. The harness keeps the record of
|
|
2
2
|
// who is driving (server/computer-control.ts); the per-turn computer
|
|
3
3
|
// processes consult it through this client before acting, because the
|
|
4
|
-
// action paths themselves never traverse the harness — a
|
|
5
|
-
// straight to the
|
|
4
|
+
// action paths themselves never traverse the harness — a boat click goes
|
|
5
|
+
// straight to the boat's REST API, and a Local VM / VPS click rides a
|
|
6
6
|
// transparent stdio bridge into Cua Driver.
|
|
7
7
|
//
|
|
8
8
|
// Failure posture: CLOSED once configured. A missing/expired turn capability
|
|
@@ -259,8 +259,10 @@ export function pendingDelegationSnapshot() {
|
|
|
259
259
|
})));
|
|
260
260
|
}
|
|
261
261
|
/** How many handoffs one turn may queue. Small on purpose: this is the only
|
|
262
|
-
* thing standing between a confused bot and a fan-out of real turns.
|
|
263
|
-
|
|
262
|
+
* thing standing between a confused bot and a fan-out of real turns. Six,
|
|
263
|
+
* not four: a lead running a weekly check-in over a five-member team is the
|
|
264
|
+
* ordinary case, and hitting the cap there silently dropped one teammate. */
|
|
265
|
+
const MAX_QUEUED_PER_THREAD = 6;
|
|
264
266
|
/** Validate and enqueue a delegation. Pushes a "Delegated to @B: reason"
|
|
265
267
|
* chip to the source thread so the user can see what was queued. */
|
|
266
268
|
export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from.threadId) {
|
|
@@ -838,7 +840,13 @@ export function buildDelegationFailurePrompt(targetName, reason) {
|
|
|
838
840
|
"Take over: tell the user what failed in plain terms, then decide the next step — retry with a narrower task, do the work yourself, or propose an alternative. Do not re-delegate the exact same task unchanged.",
|
|
839
841
|
].join("\n\n");
|
|
840
842
|
}
|
|
841
|
-
|
|
843
|
+
// Must not be smaller than MAX_QUEUED_PER_THREAD: every delegate reply is
|
|
844
|
+
// one wake, so a fan-out that the queue cap allows has to be resumable
|
|
845
|
+
// within the window. Three failed a five-member weekly check-in whose
|
|
846
|
+
// replies all landed inside four minutes ("Delegation follow-up limit
|
|
847
|
+
// reached"), while the same routine passed a week earlier only because the
|
|
848
|
+
// replies happened to spread over twenty.
|
|
849
|
+
export const DELEGATION_WAKE_MAX_PER_WINDOW = 6;
|
|
842
850
|
export const DELEGATION_WAKE_WINDOW_MS = 5 * 60 * 1000;
|
|
843
851
|
/** Bounded auto-wake budget per source thread. A delegation completion
|
|
844
852
|
* wakes the source; if that source re-delegates and the new completion
|
|
@@ -43,7 +43,7 @@ export function skipSubscriptionAuthForLocalInject(model) {
|
|
|
43
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
|
-
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
|
|
46
|
+
import { MAX_QUESTION_TEXT, parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
|
|
47
47
|
import { appendNative } from "../native.js";
|
|
48
48
|
import { acpPermissionCommand, permissionLaunchCwd } from "../permission-command.js";
|
|
49
49
|
import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
@@ -275,7 +275,7 @@ export function createAcpDriver(support) {
|
|
|
275
275
|
};
|
|
276
276
|
const allowedCredentials = new Set(support.credentialEnv ?? []);
|
|
277
277
|
// two lists, one rule: foreign PROVIDER keys must not flip a CLI's
|
|
278
|
-
// billing off its own login, and WORKSPACE credentials (
|
|
278
|
+
// billing off its own login, and WORKSPACE credentials (boat token,
|
|
279
279
|
// voice key, …) are the harness's secrets — riding along in
|
|
280
280
|
// `...process.env` is not a grant. A driver keeps only what its
|
|
281
281
|
// credentialEnv allowlist names.
|
|
@@ -439,7 +439,7 @@ export function createAcpDriver(support) {
|
|
|
439
439
|
}
|
|
440
440
|
// The bot's computer, mounted exactly like the Claude driver does:
|
|
441
441
|
// host and sandbox Cua connections expose Cua Driver's own MCP server.
|
|
442
|
-
// (A cloud
|
|
442
|
+
// (A cloud boat is not mounted here at all: a cloud turn runs ON the boat.)
|
|
443
443
|
if (turn.integrations?.localComputer) {
|
|
444
444
|
const local = turn.integrations.localComputer;
|
|
445
445
|
servers.push({
|
|
@@ -722,7 +722,8 @@ export function createAcpDriver(support) {
|
|
|
722
722
|
}
|
|
723
723
|
const tool = kind === "execute" ? "shell" : kind === "edit" ? "edit" : kind || "tool";
|
|
724
724
|
const isShellCommand = !isQuestion && kind === "execute" && !/^mcp(?:__|[.:])/i.test(String(toolCall.title ?? ""));
|
|
725
|
-
const
|
|
725
|
+
const rawSummary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool);
|
|
726
|
+
const summary = rawSummary.slice(0, isQuestion ? MAX_QUESTION_TEXT : 200);
|
|
726
727
|
// One structured question beside the flat choices: the richer card
|
|
727
728
|
// renders from it while older clients keep answering through
|
|
728
729
|
// `choices`. Built once here so the emit and the answer path can
|
|
@@ -278,7 +278,7 @@ function routineFields(args) {
|
|
|
278
278
|
const runOn = destination(args.run_on ?? args.runOn);
|
|
279
279
|
const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
|
|
280
280
|
if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
|
|
281
|
-
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the
|
|
281
|
+
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the Boat-hosted agent. Legacy "cloud" also means Boat.' };
|
|
282
282
|
}
|
|
283
283
|
if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
284
284
|
return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
|
|
@@ -982,11 +982,14 @@ export async function callTool(name, args, context) {
|
|
|
982
982
|
if (!routineId || !action) {
|
|
983
983
|
return { text: "propose_routine_action needs a routine_id and supported action.", isError: true };
|
|
984
984
|
}
|
|
985
|
+
const forBotId = String(args.for_bot_id ?? "").trim();
|
|
985
986
|
const body = {
|
|
986
987
|
fromBotId: BOT_ID,
|
|
987
988
|
fromThreadId: THREAD_ID,
|
|
988
989
|
action,
|
|
989
990
|
routineId,
|
|
991
|
+
// JSON.stringify drops the key entirely when no target was named
|
|
992
|
+
...(forBotId ? { forBotId } : {}),
|
|
990
993
|
};
|
|
991
994
|
if (action === "update") {
|
|
992
995
|
if (!jsonRecord(args.changes)) {
|
|
@@ -1098,6 +1101,7 @@ export async function callTool(name, args, context) {
|
|
|
1098
1101
|
action: args.action,
|
|
1099
1102
|
text: args.text,
|
|
1100
1103
|
oldText: args.old_text,
|
|
1104
|
+
...(typeof args.until === "string" && args.until.trim() ? { until: args.until.trim() } : {}),
|
|
1101
1105
|
}),
|
|
1102
1106
|
});
|
|
1103
1107
|
if (r.error || r.ok !== true) {
|
|
@@ -128,8 +128,9 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
128
128
|
schedule: ROUTINE_SCHEDULE_SCHEMA,
|
|
129
129
|
run_on: {
|
|
130
130
|
type: "string",
|
|
131
|
+
// "box" is Boat's historical run_on destination id (agents wire contract).
|
|
131
132
|
enum: ["maus", "box"],
|
|
132
|
-
description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the
|
|
133
|
+
description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Boat-hosted runner; it requires Boat setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
|
|
133
134
|
},
|
|
134
135
|
timeout_minutes: {
|
|
135
136
|
type: "integer",
|
|
@@ -293,7 +294,7 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
293
294
|
description: "Choose where this conversation does computer work. Call with no arguments to inspect actual available choices and the current place. For a task needing computer interaction, select the requested place, or auto to choose a suitable configured computer without asking the user to use menus. OpenMausBot reuses an existing computer first; with a configured provider it can start or provision one when needed. Do not provision for ordinary chat or just to inspect availability. A pending result means end this turn immediately: OpenMausBot updates the conversation selector and resumes the original request with that computer's real tools. Do not use the old tools after requesting a switch, repeat the task, or claim the action is done. This cannot change permissions, override Off, or switch a teammate/routine/channel.",
|
|
294
295
|
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
295
296
|
surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
|
|
296
|
-
description: "auto = suitable configured computer, cloud = remote
|
|
297
|
+
description: "auto = suitable configured computer, cloud = remote Boat/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
|
|
297
298
|
} },
|
|
298
299
|
},
|
|
299
300
|
{
|
|
@@ -523,6 +524,7 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
523
524
|
action: { type: "string", enum: ["append", "replace", "remove", "supersede"] },
|
|
524
525
|
text: { type: "string", minLength: 1, description: "Non-blank new text for append, replace, or supersede: the fact itself, without a date or bullet. Omit for remove; use remove to delete a passage." },
|
|
525
526
|
old_text: { type: "string", minLength: 1, description: "Exact unique existing passage for replace, supersede, or remove. Omit for append." },
|
|
527
|
+
until: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", description: "Optional, for append or supersede: YYYY-MM-DD, the last day a temporary fact holds (an exam this weekend, a trip next week). After that day the entry is hidden from your memory." },
|
|
526
528
|
},
|
|
527
529
|
required: ["action"],
|
|
528
530
|
},
|
|
@@ -614,12 +616,16 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
614
616
|
},
|
|
615
617
|
{
|
|
616
618
|
name: "propose_routine_action",
|
|
617
|
-
description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id." + PROPOSAL_OUTCOME,
|
|
619
|
+
description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id. If the user asks to change ANOTHER bot's routine and that bot is in your section, call list_bots and pass that bot's id as for_bot_id; the routine keeps its owner and every run keeps that bot's engine and permissions." + PROPOSAL_OUTCOME,
|
|
618
620
|
inputSchema: {
|
|
619
621
|
type: "object",
|
|
620
622
|
additionalProperties: false,
|
|
621
623
|
properties: {
|
|
622
624
|
routine_id: { type: "string", minLength: 1, description: "Routine id from list_routines." },
|
|
625
|
+
for_bot_id: {
|
|
626
|
+
type: "string",
|
|
627
|
+
description: "Only when the requested change targets ANOTHER bot's routine and that bot is in your section: that bot's id from list_bots. Learn the routine id from that bot's own routines (it can run list_routines). Omit to change one of your own routines.",
|
|
628
|
+
},
|
|
623
629
|
action: {
|
|
624
630
|
type: "string",
|
|
625
631
|
enum: ["update", "pause", "resume", "run_now", "delete"],
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
2
|
import { appendNative } from "./native.js";
|
|
3
3
|
import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
|
|
4
|
+
// "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
|
|
4
5
|
const DRIVER_KIND = "boxAgent";
|
|
5
6
|
// overridable so tests and a dev backend can be pointed at instead of the live provider
|
|
6
|
-
const
|
|
7
|
+
const BOAT_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
|
|
7
8
|
const MODELS = {
|
|
8
9
|
default: "claude-fable-5",
|
|
9
10
|
options: [
|
|
@@ -15,14 +16,14 @@ const MODELS = {
|
|
|
15
16
|
/** Any fence whose info string names the ask protocol, even when its body
|
|
16
17
|
* does not parse: the marker for "the model tried to ask and failed". */
|
|
17
18
|
const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
|
|
18
|
-
/** The
|
|
19
|
+
/** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
|
|
19
20
|
* prime-agent, kimi). Which one a model id belongs to comes from the public
|
|
20
21
|
* catalog, `GET /api/provider-models` at the API root: an object keyed by
|
|
21
22
|
* harness, each with its `models`. A bot that arrives here from another engine
|
|
22
23
|
* carries that engine's model id, so this is what lets it keep its model. */
|
|
23
24
|
let catalog = null;
|
|
24
25
|
async function loadCatalog() {
|
|
25
|
-
const root =
|
|
26
|
+
const root = BOAT_API.replace(/\/api\/box\/v1\/?$/, "");
|
|
26
27
|
catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
|
|
27
28
|
.then((res) => (res.ok ? res.json() : null))
|
|
28
29
|
.catch(() => null);
|
|
@@ -44,7 +45,7 @@ function decodeConfig(raw) {
|
|
|
44
45
|
...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
|
-
export const
|
|
48
|
+
export const BoatAgentDriver = {
|
|
48
49
|
driverKind: DRIVER_KIND,
|
|
49
50
|
metadata: { displayName: "Computer", supportsMultipleInstances: false },
|
|
50
51
|
models: MODELS,
|
|
@@ -74,7 +75,7 @@ export const BoxAgentDriver = {
|
|
|
74
75
|
createdAt: new Date().toISOString(),
|
|
75
76
|
});
|
|
76
77
|
const api = async (path, opts = {}) => {
|
|
77
|
-
const res = await fetch(`${
|
|
78
|
+
const res = await fetch(`${BOAT_API}${path}`, {
|
|
78
79
|
...opts,
|
|
79
80
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...opts.headers },
|
|
80
81
|
signal: opts.signal ?? AbortSignal.timeout(30_000),
|
|
@@ -120,7 +121,7 @@ export const BoxAgentDriver = {
|
|
|
120
121
|
});
|
|
121
122
|
appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
|
|
122
123
|
// real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
|
|
123
|
-
// id:<
|
|
124
|
+
// id:<boat id>} — never fall back to the bare id, it's the boat's
|
|
124
125
|
return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
|
|
125
126
|
};
|
|
126
127
|
const promptId = await postPrompt(prompt);
|
|
@@ -149,7 +150,7 @@ export const BoxAgentDriver = {
|
|
|
149
150
|
const seen = new Set();
|
|
150
151
|
let lastText = "";
|
|
151
152
|
let pendingText = "";
|
|
152
|
-
/** Why the
|
|
153
|
+
/** Why the boat could not answer (login expired, model refused, …). */
|
|
153
154
|
let problem = null;
|
|
154
155
|
/** Emit unflushed deltas as assistant_text and reset pendingText.
|
|
155
156
|
* The omb-ask block is protocol, not prose: it streamed raw (the
|
|
@@ -229,7 +230,7 @@ export const BoxAgentDriver = {
|
|
|
229
230
|
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
230
231
|
}
|
|
231
232
|
// The turn is still open, so this prompt continues it: same turnId,
|
|
232
|
-
// same accounting, and the answer reaches the
|
|
233
|
+
// same accounting, and the answer reaches the boat the way the ask
|
|
233
234
|
// contract promised — Q:/A: blocks, capped for echo.
|
|
234
235
|
const continuation = [
|
|
235
236
|
capAnswerEcho(answerWithoutPreamble(reply)),
|
|
@@ -238,7 +239,7 @@ export const BoxAgentDriver = {
|
|
|
238
239
|
].join("\n");
|
|
239
240
|
const nextPromptId = await postPrompt(continuation);
|
|
240
241
|
// Stop can land while the continuation POST is in flight: the
|
|
241
|
-
// interrupt inside cancel() then hits a
|
|
242
|
+
// interrupt inside cancel() then hits a boat with no active run,
|
|
242
243
|
// and the continuation would start after it. Interrupt the run
|
|
243
244
|
// that just started before ending the turn.
|
|
244
245
|
if (cancelled) {
|
|
@@ -247,9 +248,9 @@ export const BoxAgentDriver = {
|
|
|
247
248
|
}
|
|
248
249
|
return await settleRun(nextPromptId);
|
|
249
250
|
};
|
|
250
|
-
/** Poll one
|
|
251
|
+
/** Poll one boat run to its settle. */
|
|
251
252
|
const settleRun = async (runPromptId) => {
|
|
252
|
-
const startedAt = Date.now(); // one 30-min ceiling per
|
|
253
|
+
const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
|
|
253
254
|
lastText = "";
|
|
254
255
|
pendingText = "";
|
|
255
256
|
problem = null;
|
|
@@ -355,7 +356,7 @@ export const BoxAgentDriver = {
|
|
|
355
356
|
};
|
|
356
357
|
const snapshot = async () => {
|
|
357
358
|
if (!token) {
|
|
358
|
-
return { state: "unavailable", reason: 'no
|
|
359
|
+
return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
|
|
359
360
|
}
|
|
360
361
|
try {
|
|
361
362
|
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";
|
|
@@ -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.
|