openmausbot 0.1.95 → 0.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +26 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +108 -49
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +4664 -4644
- package/dist-server/openmausbot.js +1425 -1437
- package/dist-server/pair-cli.js +1425 -1437
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +20 -5
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +71 -87
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +340 -167
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +13 -16
- package/dist-server/server/cloud-home.js +3 -13
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +99 -75
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +14 -7
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/acp/opencode-go.js +19 -23
- package/dist-server/server/drivers/agents-call.js +47 -64
- package/dist-server/server/drivers/agents-catalog.js +69 -3
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +7 -6
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +113 -11
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +942 -980
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +40 -6
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +6 -5
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +126 -2
- package/dist-server/server/surface.js +40 -64
- package/dist-server/server/system-prompt.js +23 -13
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/workspace.js +3 -4
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -71
- package/dist-server/shared/cloud-computer.js +10 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +22 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/drivers/boatagent.js +0 -443
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{c as r,s as e}from"./flowDiagram-KWPJA3E3-Bdw9DqEl.js";import{_ as a}from"./mermaid.core-C6yU5FSS.js";import"./chunk-5VM5RSS4-BmVYJ0f8.js";import"./chunk-XXDRQBXY-CPqxINpA.js";import"./chunk-WEXAMYUT-C7ZvuguH.js";import"./chunk-GWA4HPMP-DFYSb6J6.js";import"./channel-DS2NbT5x.js";import"./index-X_5ydXIE.js";var o=a(t=>`${e(t)}
|
|
2
|
-
.swimlane.cluster:not([data-color-id]) rect {
|
|
3
|
-
stroke: ${t.clusterBorder} !important;
|
|
4
|
-
}
|
|
5
|
-
[data-look="neo"].cluster rect {
|
|
6
|
-
filter: none;
|
|
7
|
-
}
|
|
8
|
-
`,"getStyles"),s=o,u=r({styles:s});export{u as diagram};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** Quiet-window policy for releasing a held seat while its turn lives.
|
|
2
|
-
*
|
|
3
|
-
* The policy is deliberately synchronous and clock-injected: like the
|
|
4
|
-
* claim map it serves, expiry is evaluated when a claim is touched
|
|
5
|
-
* (claim, blocker, owns) rather than by a timer, so the server's single
|
|
6
|
-
* thread decides every transition and tests pin time exactly. Slice
|
|
7
|
-
* #1655 reuses this window math for its own idle release; keep this
|
|
8
|
-
* module free of server imports. */
|
|
9
|
-
/** Default quiet window: no screen activity for this long releases the seat. */
|
|
10
|
-
export const COMPUTER_CLAIM_QUIET_MS = 90_000;
|
|
11
|
-
/** Default reclaim window: the previous holder re-claims directly for this long. */
|
|
12
|
-
export const COMPUTER_CLAIM_RECLAIM_MS = 10 * 60_000;
|
|
13
|
-
export class IdleReleasePolicy {
|
|
14
|
-
quietMs;
|
|
15
|
-
reclaimMs;
|
|
16
|
-
constructor(quietMs = COMPUTER_CLAIM_QUIET_MS, reclaimMs = COMPUTER_CLAIM_RECLAIM_MS) {
|
|
17
|
-
if (!Number.isFinite(quietMs) || quietMs <= 0)
|
|
18
|
-
throw new Error("Idle release quiet window must be positive");
|
|
19
|
-
if (!Number.isFinite(reclaimMs) || reclaimMs <= 0)
|
|
20
|
-
throw new Error("Idle release reclaim window must be positive");
|
|
21
|
-
this.quietMs = quietMs;
|
|
22
|
-
this.reclaimMs = reclaimMs;
|
|
23
|
-
}
|
|
24
|
-
/** True once the seat has been quiet for the full window. Screen-poller
|
|
25
|
-
* frames never refresh the activity clock, so they cannot keep a seat. */
|
|
26
|
-
quietElapsed(lastActivityAt, now) {
|
|
27
|
-
return now - lastActivityAt >= this.quietMs;
|
|
28
|
-
}
|
|
29
|
-
/** True while a released seat's previous holder still re-claims directly. */
|
|
30
|
-
reclaimLive(releasedAt, now) {
|
|
31
|
-
return now < releasedAt + this.reclaimMs;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/** Consent, cost, and idle-stop policy for overflowing a local computer
|
|
2
|
-
* wait onto a per-second-billed cloud seat (#1655).
|
|
3
|
-
*
|
|
4
|
-
* Like server/claim-idle.ts this module is deliberately synchronous and
|
|
5
|
-
* clock-injected: the server decides every transition on its single
|
|
6
|
-
* thread and tests pin time exactly. Nothing here starts or stops a
|
|
7
|
-
* machine — the wiring does, and only when the decision says to. Keep
|
|
8
|
-
* this module free of server imports. */
|
|
9
|
-
import { computerWaitDuration } from "./computer-wait.js";
|
|
10
|
-
/** Default idle stop: a cloud seat no real screen work has touched for
|
|
11
|
-
* this long is stopped (archived), because billing runs per second while
|
|
12
|
-
* it runs. "A few idle minutes" per the issue. */
|
|
13
|
-
export const CLOUD_SEAT_IDLE_STOP_MS = 5 * 60_000;
|
|
14
|
-
/** Retry cadence for an idle stop that failed (#1655): the first retry
|
|
15
|
-
* after one minute, doubling per failure up to ten. A failed stop keeps
|
|
16
|
-
* its lease — the seat still bills — so the sweep always retries instead
|
|
17
|
-
* of abandoning a running machine. */
|
|
18
|
-
export const CLOUD_SEAT_SLEEP_RETRY_MS = 60_000;
|
|
19
|
-
export const CLOUD_SEAT_SLEEP_RETRY_MAX_MS = 10 * 60_000;
|
|
20
|
-
export function cloudOverflowAction(situation) {
|
|
21
|
-
if (!situation.featureEnabled || !situation.cloudConfigured || situation.perSecondCostUsd === null)
|
|
22
|
-
return { kind: "off" };
|
|
23
|
-
if (situation.started)
|
|
24
|
-
return { kind: "none" };
|
|
25
|
-
if (situation.consented)
|
|
26
|
-
return { kind: "start" };
|
|
27
|
-
if (situation.offered)
|
|
28
|
-
return { kind: "none" };
|
|
29
|
-
return { kind: "offer" };
|
|
30
|
-
}
|
|
31
|
-
/** Per-conversation consent plus the offer bookkeeping that keeps the
|
|
32
|
-
* card to one per conversation until it is revoked. A thread the operator
|
|
33
|
-
* allowlisted in config carries standing consent and never needs the
|
|
34
|
-
* card. */
|
|
35
|
-
export class CloudOverflowConsent {
|
|
36
|
-
grants = new Map();
|
|
37
|
-
offers = new Map();
|
|
38
|
-
revocations = new Set();
|
|
39
|
-
/** Consent for this conversation. With `perSecondCostUsd`, a card
|
|
40
|
-
* consent counts only at the rate its card showed — an allowlisted
|
|
41
|
-
* thread carries the operator's standing consent, and the operator is
|
|
42
|
-
* also the one who sets the rate. */
|
|
43
|
-
consented(threadId, allowlistedThreads = new Set(), perSecondCostUsd) {
|
|
44
|
-
if (this.revocations.has(threadId))
|
|
45
|
-
return false;
|
|
46
|
-
if (allowlistedThreads.has(threadId))
|
|
47
|
-
return true;
|
|
48
|
-
const grant = this.grants.get(threadId);
|
|
49
|
-
if (!grant)
|
|
50
|
-
return false;
|
|
51
|
-
return perSecondCostUsd === undefined || grant.perSecondCostUsd === perSecondCostUsd;
|
|
52
|
-
}
|
|
53
|
-
/** `perSecondCostUsd` is the rate the answered card showed. */
|
|
54
|
-
grant(threadId, perSecondCostUsd, now = Date.now()) {
|
|
55
|
-
this.revocations.delete(threadId);
|
|
56
|
-
this.grants.set(threadId, { at: now, perSecondCostUsd });
|
|
57
|
-
}
|
|
58
|
-
/** Revoking also clears the offered mark, so a later wait in the same
|
|
59
|
-
* conversation may ask again instead of meeting silence. */
|
|
60
|
-
revoke(threadId) {
|
|
61
|
-
const had = this.grants.delete(threadId);
|
|
62
|
-
this.revocations.add(threadId);
|
|
63
|
-
this.offers.delete(threadId);
|
|
64
|
-
return had;
|
|
65
|
-
}
|
|
66
|
-
/** A card is outstanding for this conversation — at the given rate
|
|
67
|
-
* when one is supplied, so a config change re-offers with the new
|
|
68
|
-
* price instead of meeting silence at the old one. */
|
|
69
|
-
offered(threadId, perSecondCostUsd) {
|
|
70
|
-
const offer = this.offers.get(threadId);
|
|
71
|
-
if (!offer)
|
|
72
|
-
return false;
|
|
73
|
-
return perSecondCostUsd === undefined || offer.perSecondCostUsd === perSecondCostUsd;
|
|
74
|
-
}
|
|
75
|
-
/** The rate the outstanding card showed, or null when no card is out. */
|
|
76
|
-
offeredRate(threadId) {
|
|
77
|
-
return this.offers.get(threadId)?.perSecondCostUsd ?? null;
|
|
78
|
-
}
|
|
79
|
-
markOffered(threadId, perSecondCostUsd, now = Date.now()) {
|
|
80
|
-
this.offers.set(threadId, { at: now, perSecondCostUsd });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** One running cloud seat started by overflow. `touch` is called only
|
|
84
|
-
* for real computer tool completions on that seat — screen-poller frames
|
|
85
|
-
* never reach it, exactly like #1653's activity clock, so preview
|
|
86
|
-
* traffic cannot keep a paid machine awake. */
|
|
87
|
-
export class CloudSeatLease {
|
|
88
|
-
botId;
|
|
89
|
-
threadId;
|
|
90
|
-
/** The claim generation that started the seat: a stop releases the Box
|
|
91
|
-
* claim for exactly this owner, never a newer turn's. */
|
|
92
|
-
generation;
|
|
93
|
-
startedAt;
|
|
94
|
-
idleStopMs;
|
|
95
|
-
lastActivityAt;
|
|
96
|
-
sleepRetryNotBefore = 0;
|
|
97
|
-
sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
|
|
98
|
-
constructor(init) {
|
|
99
|
-
const idleStopMs = init.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
100
|
-
if (!Number.isFinite(idleStopMs) || idleStopMs <= 0)
|
|
101
|
-
throw new Error("Cloud seat idle stop window must be positive");
|
|
102
|
-
this.botId = init.botId;
|
|
103
|
-
this.threadId = init.threadId;
|
|
104
|
-
this.generation = init.generation;
|
|
105
|
-
this.startedAt = init.now ?? Date.now();
|
|
106
|
-
this.lastActivityAt = this.startedAt;
|
|
107
|
-
this.idleStopMs = idleStopMs;
|
|
108
|
-
}
|
|
109
|
-
touch(now = Date.now()) {
|
|
110
|
-
this.lastActivityAt = now;
|
|
111
|
-
// Real work earns a fresh stop attempt: the backoff only paces
|
|
112
|
-
// retries against an idle seat that keeps failing to sleep.
|
|
113
|
-
this.sleepRetryNotBefore = 0;
|
|
114
|
-
this.sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
|
|
115
|
-
}
|
|
116
|
-
/** An idle stop may run now — true until a failure defers it. */
|
|
117
|
-
sleepDue(now = Date.now()) {
|
|
118
|
-
return now >= this.sleepRetryNotBefore;
|
|
119
|
-
}
|
|
120
|
-
/** The idle stop failed: keep the lease and back off, bounded, so the
|
|
121
|
-
* sweep retries gently instead of hammering a sick Box — and never
|
|
122
|
-
* abandons a seat that still bills. */
|
|
123
|
-
deferSleep(now = Date.now()) {
|
|
124
|
-
this.sleepRetryNotBefore = now + this.sleepRetryMs;
|
|
125
|
-
this.sleepRetryMs = Math.min(this.sleepRetryMs * 2, CLOUD_SEAT_SLEEP_RETRY_MAX_MS);
|
|
126
|
-
}
|
|
127
|
-
idleFor(now = Date.now()) {
|
|
128
|
-
return Math.max(0, now - this.lastActivityAt);
|
|
129
|
-
}
|
|
130
|
-
idleElapsed(now = Date.now()) {
|
|
131
|
-
return now - this.lastActivityAt >= this.idleStopMs;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Dollars per second at rate-card precision: enough places for small
|
|
135
|
-
* per-second rates to stay visible, trailing zeros trimmed but never
|
|
136
|
-
* past two decimals. */
|
|
137
|
-
export function formatPerSecondUsd(perSecondCostUsd) {
|
|
138
|
-
let text = perSecondCostUsd.toFixed(6).replace(/0+$/, "");
|
|
139
|
-
if (text.endsWith("."))
|
|
140
|
-
text += "00";
|
|
141
|
-
else {
|
|
142
|
-
const decimals = text.length - text.indexOf(".") - 1;
|
|
143
|
-
if (decimals < 2)
|
|
144
|
-
text += "0".repeat(2 - decimals);
|
|
145
|
-
}
|
|
146
|
-
return "$" + text;
|
|
147
|
-
}
|
|
148
|
-
/** The consent card (#1655): the per-second cost sits beside the local
|
|
149
|
-
* wait picture before any choice is made, and nothing starts without an
|
|
150
|
-
* explicit consent the person still has to give. */
|
|
151
|
-
export function cloudOverflowOfferText(opts) {
|
|
152
|
-
const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
153
|
-
const estimate = opts.waitEstimateMs === undefined ? "" : ` Recent local waits here have taken about ${computerWaitDuration(opts.waitEstimateMs)}.`;
|
|
154
|
-
return `This computer is busy. Its work can overflow to a cloud computer for ${formatPerSecondUsd(opts.perSecondCostUsd)} per second.${estimate} Nothing starts without your consent — allow cloud overflow for this conversation to start it; the cloud computer stops after ${computerWaitDuration(idleStopMs)} idle.`;
|
|
155
|
-
}
|
|
156
|
-
export function cloudSeatStartedText(opts) {
|
|
157
|
-
const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
158
|
-
return `Cloud computer started at ${formatPerSecondUsd(opts.perSecondCostUsd)} per second. It stops automatically after ${computerWaitDuration(idleStopMs)} idle, and the local wait continues until this computer is free.`;
|
|
159
|
-
}
|
|
160
|
-
export function cloudSeatStoppedText(idleMs) {
|
|
161
|
-
return `Cloud computer stopped after ${computerWaitDuration(idleMs)} idle — billing pauses while it sleeps.`;
|
|
162
|
-
}
|
|
163
|
-
export function cloudOverflowConsentText(allowed, idleStopMs = CLOUD_SEAT_IDLE_STOP_MS) {
|
|
164
|
-
return allowed
|
|
165
|
-
? `Cloud overflow allowed for this conversation. A waiting turn may start the cloud computer; it stops after ${computerWaitDuration(idleStopMs)} idle.`
|
|
166
|
-
: "Cloud overflow consent revoked. Waits stay local, and no cloud computer starts without a new consent.";
|
|
167
|
-
}
|
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { newEventId, newId } from "../contracts.js";
|
|
2
|
-
import { boatCredential, boatProviderApi } from "../included-services.js";
|
|
3
|
-
import { appendNative } from "./native.js";
|
|
4
|
-
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
5
|
-
import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
|
|
6
|
-
// "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
|
|
7
|
-
const DRIVER_KIND = "boxAgent";
|
|
8
|
-
const MODELS = {
|
|
9
|
-
default: "claude-fable-5",
|
|
10
|
-
options: [
|
|
11
|
-
{ id: "claude-fable-5", label: "Claude Fable 5 · on the box" },
|
|
12
|
-
{ id: "sonnet", label: "Claude Sonnet · on the box" },
|
|
13
|
-
{ id: "gpt-5.4", label: "GPT-5.4 (Codex) · on the box" },
|
|
14
|
-
],
|
|
15
|
-
};
|
|
16
|
-
/** Any fence whose info string names the ask protocol, even when its body
|
|
17
|
-
* does not parse: the marker for "the model tried to ask and failed". */
|
|
18
|
-
const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
|
|
19
|
-
/** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
|
|
20
|
-
* prime-agent, kimi). Which one a model id belongs to comes from the public
|
|
21
|
-
* catalog, `GET /api/provider-models` at the API root: an object keyed by
|
|
22
|
-
* harness, each with its `models`. A bot that arrives here from another engine
|
|
23
|
-
* carries that engine's model id, so this is what lets it keep its model. The
|
|
24
|
-
* root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
|
|
25
|
-
let catalog = null;
|
|
26
|
-
let catalogRoot = "";
|
|
27
|
-
async function loadCatalog(api) {
|
|
28
|
-
const root = api.replace(/\/api\/box\/v1\/?$/, "");
|
|
29
|
-
if (catalog && catalogRoot === root)
|
|
30
|
-
return;
|
|
31
|
-
catalogRoot = root;
|
|
32
|
-
catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
|
|
33
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
34
|
-
.catch(() => null);
|
|
35
|
-
}
|
|
36
|
-
const providerFor = (model) => {
|
|
37
|
-
const slash = model.indexOf("/");
|
|
38
|
-
if (slash > 0 && catalog?.[model.slice(0, slash)])
|
|
39
|
-
return { provider: model.slice(0, slash), model: model.slice(slash + 1) };
|
|
40
|
-
for (const [provider, harness] of Object.entries(catalog ?? {})) {
|
|
41
|
-
if (harness.models?.some((m) => m?.id === model))
|
|
42
|
-
return { provider, model };
|
|
43
|
-
}
|
|
44
|
-
return { provider: model.startsWith("gpt") ? "codex" : "claude-code", model };
|
|
45
|
-
};
|
|
46
|
-
/** Boat answers a prompt it cannot run with 409 provider_not_configured: the
|
|
47
|
-
* Boat has no AI sign-in for the harness the model needs. Say that, and the
|
|
48
|
-
* one thing the person can do about it here. */
|
|
49
|
-
export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The Computer engine has no AI sign-in on its cloud computer. Choose another engine in this bot's settings.";
|
|
50
|
-
/** An OMB Cloud's included Boat account has no agent sign-ins and never
|
|
51
|
-
* will: the operator's account must not run (or pay for) customers' models. */
|
|
52
|
-
export const COMPUTER_ENGINE_CLOUD_UNAVAILABLE = "The Computer engine isn't available on OpenMaus Cloud — choose another engine.";
|
|
53
|
-
/** The words of a Boat error envelope ({code, message, error:{code,
|
|
54
|
-
* message}}), never a bare code when Boat sent a sentence. */
|
|
55
|
-
export function boatErrorMessage(body, status) {
|
|
56
|
-
const code = body?.code ?? body?.error?.code;
|
|
57
|
-
if (code === "provider_not_configured")
|
|
58
|
-
return PROVIDER_NOT_CONFIGURED_MESSAGE;
|
|
59
|
-
const message = body?.message ?? body?.error?.message ?? (typeof body?.error === "string" ? body.error : undefined) ?? code;
|
|
60
|
-
return typeof message === "string" && message.trim() ? message : `box HTTP ${status}`;
|
|
61
|
-
}
|
|
62
|
-
function decodeConfig(raw) {
|
|
63
|
-
const o = (raw ?? {});
|
|
64
|
-
return {
|
|
65
|
-
pollMs: typeof o.pollMs === "number" ? o.pollMs : 2500,
|
|
66
|
-
...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export const BoatAgentDriver = {
|
|
70
|
-
driverKind: DRIVER_KIND,
|
|
71
|
-
metadata: { displayName: "Computer", supportsMultipleInstances: false },
|
|
72
|
-
models: MODELS,
|
|
73
|
-
decodeConfig,
|
|
74
|
-
defaultConfig: () => decodeConfig({}),
|
|
75
|
-
async create(input) {
|
|
76
|
-
const { instanceId, config } = input;
|
|
77
|
-
// The person's own token (instanceConfigs injects it), else Cloud Pro's
|
|
78
|
-
// included one, with the one base URL each may be sent to. Resolved per
|
|
79
|
-
// request, like every other Boat call.
|
|
80
|
-
const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
|
|
81
|
-
const account = () => boatCredential(ownToken);
|
|
82
|
-
const listeners = new Set();
|
|
83
|
-
const active = new Map();
|
|
84
|
-
/** One open ask per thread: the turn-held transport's pending card. The
|
|
85
|
-
* thread is busy for exactly as long as the ask is open, so a second
|
|
86
|
-
* block can never race the first. */
|
|
87
|
-
const heldAsks = new Map();
|
|
88
|
-
/** Threads whose last run ended with an unparseable omb-ask fence: the
|
|
89
|
-
* next prompt carries the correction so the ask is never lost silently. */
|
|
90
|
-
const malformedAsks = new Set();
|
|
91
|
-
const emit = (event) => {
|
|
92
|
-
for (const l of Array.from(listeners))
|
|
93
|
-
l(event);
|
|
94
|
-
};
|
|
95
|
-
const base = (threadId, turnId) => ({
|
|
96
|
-
eventId: newEventId(),
|
|
97
|
-
provider: DRIVER_KIND,
|
|
98
|
-
threadId,
|
|
99
|
-
turnId,
|
|
100
|
-
createdAt: new Date().toISOString(),
|
|
101
|
-
});
|
|
102
|
-
const api = async (path, opts = {}) => {
|
|
103
|
-
const credential = account();
|
|
104
|
-
const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
|
|
105
|
-
...opts,
|
|
106
|
-
headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
|
|
107
|
-
signal: opts.signal ?? AbortSignal.timeout(30_000),
|
|
108
|
-
});
|
|
109
|
-
const body = await res.json().catch(() => null);
|
|
110
|
-
if (!res.ok || body?.ok === false)
|
|
111
|
-
throw new Error(boatErrorMessage(body, res.status));
|
|
112
|
-
return body;
|
|
113
|
-
};
|
|
114
|
-
const sendTurn = async (turn) => {
|
|
115
|
-
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
116
|
-
const { threadId } = turn;
|
|
117
|
-
const computer = turn.integrations?.computer;
|
|
118
|
-
const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
|
|
119
|
-
if (!account())
|
|
120
|
-
throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
|
|
121
|
-
if (account()?.included)
|
|
122
|
-
throw new Error(COMPUTER_ENGINE_CLOUD_UNAVAILABLE);
|
|
123
|
-
if (!boxId) {
|
|
124
|
-
throw new Error("this bot has no computer yet — open the Computer panel and provision one");
|
|
125
|
-
}
|
|
126
|
-
if (active.has(threadId))
|
|
127
|
-
throw new Error("a turn is already running on this thread");
|
|
128
|
-
const turnId = newId();
|
|
129
|
-
const model = turn.model || MODELS.default;
|
|
130
|
-
const correction = malformedAsks.delete(threadId)
|
|
131
|
-
? "Your previous omb-ask block was malformed or empty, so the person never saw it. Ask again with a valid fenced omb-ask JSON block, or ask in plain words."
|
|
132
|
-
: "";
|
|
133
|
-
const prompt = [
|
|
134
|
-
turn.system,
|
|
135
|
-
"You are working on the assigned cloud computer — use its desktop, Chrome, and shell within the access described above.",
|
|
136
|
-
ombAskProtocolPrompt(),
|
|
137
|
-
...(correction ? [correction] : []),
|
|
138
|
-
"",
|
|
139
|
-
turn.text,
|
|
140
|
-
]
|
|
141
|
-
.filter((s) => s !== undefined)
|
|
142
|
-
.join("\n");
|
|
143
|
-
const postPrompt = async (promptText) => {
|
|
144
|
-
await loadCatalog(account()?.api ?? boatProviderApi());
|
|
145
|
-
const started = await api(`/boxes/${boxId}/prompt`, {
|
|
146
|
-
method: "POST",
|
|
147
|
-
body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
|
|
148
|
-
});
|
|
149
|
-
appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
|
|
150
|
-
// real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
|
|
151
|
-
// id:<boat id>} — never fall back to the bare id, it's the boat's
|
|
152
|
-
return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
|
|
153
|
-
};
|
|
154
|
-
const promptId = await postPrompt(prompt);
|
|
155
|
-
let cancelled = false;
|
|
156
|
-
active.set(threadId, {
|
|
157
|
-
turnId,
|
|
158
|
-
boxId,
|
|
159
|
-
cancel: () => {
|
|
160
|
-
cancelled = true;
|
|
161
|
-
heldAsks.get(threadId)?.settle(null, "system");
|
|
162
|
-
void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
166
|
-
emit({ ...base(threadId, turnId), type: "session.started", sessionId: promptId, model });
|
|
167
|
-
// poll events + run status until the prompt settles — and, when the
|
|
168
|
-
// run ends on an omb-ask block, until the person answers it: the OMB
|
|
169
|
-
// turn is the unit the whole server already understands (busy thread,
|
|
170
|
-
// waiting-on-you, routines waiting), so it stays open across the ask
|
|
171
|
-
// and the continuation prompt continues it rather than starting a
|
|
172
|
-
// driver-initiated turn nobody accounts for.
|
|
173
|
-
(async () => {
|
|
174
|
-
// Event ids seen this TURN. The events stream is the whole
|
|
175
|
-
// conversation, so a continuation run must not re-ingest history;
|
|
176
|
-
// `taskId` normally filters it, but ids survive even shape drift.
|
|
177
|
-
const seen = new Set();
|
|
178
|
-
let lastText = "";
|
|
179
|
-
let pendingText = "";
|
|
180
|
-
/** Why the boat could not answer (login expired, model refused, …). */
|
|
181
|
-
let problem = null;
|
|
182
|
-
/** Emit unflushed deltas as assistant_text and reset pendingText.
|
|
183
|
-
* The omb-ask block is protocol, not prose: it streamed raw (the
|
|
184
|
-
* card is its readable form), and the settled message shows the
|
|
185
|
-
* words around it, never the JSON. */
|
|
186
|
-
const flushAssistantText = () => {
|
|
187
|
-
const text = stripOmbAskBlock(pendingText);
|
|
188
|
-
pendingText = "";
|
|
189
|
-
if (!text.trim())
|
|
190
|
-
return;
|
|
191
|
-
emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_text", text });
|
|
192
|
-
};
|
|
193
|
-
/** Stream a full-text snapshot as a delta and accumulate it for flush. */
|
|
194
|
-
const ingest = (text) => {
|
|
195
|
-
const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
|
|
196
|
-
lastText = text;
|
|
197
|
-
if (!delta)
|
|
198
|
-
return;
|
|
199
|
-
pendingText += delta;
|
|
200
|
-
emit({ ...base(threadId, turnId), type: "content.delta", streamKind: "assistant_text", delta });
|
|
201
|
-
};
|
|
202
|
-
/** The run has settled: flush its prose, then hold the OMB turn open
|
|
203
|
-
* on an omb-ask block (if any) until the person answers or the ask
|
|
204
|
-
* times out. An answer chains the continuation run under this same
|
|
205
|
-
* turn; a deny or timeout ends it. */
|
|
206
|
-
const finishRun = async (ok, stopReason) => {
|
|
207
|
-
flushAssistantText();
|
|
208
|
-
// An interrupt that lands between ask text streaming and settle
|
|
209
|
-
// finds no heldAsks entry to settle; without this check the run
|
|
210
|
-
// would register a fresh ask for a dead turn and hold stop pending
|
|
211
|
-
// for the ask timeout.
|
|
212
|
-
if (cancelled) {
|
|
213
|
-
return { ok: false, stopReason: "interrupted" };
|
|
214
|
-
}
|
|
215
|
-
// A remotely interrupted or failed run must stay stopped, even
|
|
216
|
-
// if its partial output already contains a complete question.
|
|
217
|
-
if (!ok)
|
|
218
|
-
return { ok, stopReason };
|
|
219
|
-
const questions = parseOmbAskQuestions(lastText);
|
|
220
|
-
if (!questions) {
|
|
221
|
-
// A fence that did not parse is a question the person never saw.
|
|
222
|
-
// Say so on the next prompt instead of losing it silently. (An
|
|
223
|
-
// over-cap block does not land here: the parser caps it at six
|
|
224
|
-
// questions and still shows the card.)
|
|
225
|
-
if (ASK_FENCE_ANY.test(lastText))
|
|
226
|
-
malformedAsks.add(threadId);
|
|
227
|
-
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
228
|
-
}
|
|
229
|
-
const requestId = newId();
|
|
230
|
-
let settleHeld;
|
|
231
|
-
const held = new Promise((resolve) => {
|
|
232
|
-
const timer = setTimeout(() => settleHeld(null, "timeout"), config.askTimeoutMs ?? 15 * 60_000);
|
|
233
|
-
timer.unref?.();
|
|
234
|
-
settleHeld = (reply, source) => {
|
|
235
|
-
heldAsks.delete(threadId);
|
|
236
|
-
clearTimeout(timer);
|
|
237
|
-
resolve([reply, source]);
|
|
238
|
-
};
|
|
239
|
-
});
|
|
240
|
-
// register before emitting — an answer can race the emit
|
|
241
|
-
heldAsks.set(threadId, { requestId, settle: settleHeld });
|
|
242
|
-
const choices = questionChoices(questions);
|
|
243
|
-
emit({
|
|
244
|
-
...base(threadId, turnId),
|
|
245
|
-
requestId,
|
|
246
|
-
type: "request.opened",
|
|
247
|
-
requestType: "question",
|
|
248
|
-
tool: OMB_ASK_TOOL,
|
|
249
|
-
summary: askQuestionSummary(questions),
|
|
250
|
-
questions,
|
|
251
|
-
...(choices?.length ? { choices } : {}),
|
|
252
|
-
origin: "output",
|
|
253
|
-
});
|
|
254
|
-
const [reply, source] = await held;
|
|
255
|
-
emit({ ...base(threadId, turnId), requestId, type: "request.resolved", behavior: reply !== null ? "answer" : "deny", source });
|
|
256
|
-
if (reply === null || cancelled) {
|
|
257
|
-
return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
|
|
258
|
-
}
|
|
259
|
-
// The turn is still open, so this prompt continues it: same turnId,
|
|
260
|
-
// same accounting, and the answer reaches the boat the way the ask
|
|
261
|
-
// contract promised — Q:/A: blocks, capped for echo.
|
|
262
|
-
const continuation = [
|
|
263
|
-
capAnswerEcho(answerWithoutPreamble(reply)),
|
|
264
|
-
"",
|
|
265
|
-
"The person answered the omb-ask questions above (Q:/A:). Continue the task with their answers; end with another omb-ask block only if you truly need more.",
|
|
266
|
-
].join("\n");
|
|
267
|
-
const nextPromptId = await postPrompt(continuation);
|
|
268
|
-
// Stop can land while the continuation POST is in flight: the
|
|
269
|
-
// interrupt inside cancel() then hits a boat with no active run,
|
|
270
|
-
// and the continuation would start after it. Interrupt the run
|
|
271
|
-
// that just started before ending the turn.
|
|
272
|
-
if (cancelled) {
|
|
273
|
-
void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
|
|
274
|
-
return { ok: false, stopReason: "interrupted" };
|
|
275
|
-
}
|
|
276
|
-
return await settleRun(nextPromptId);
|
|
277
|
-
};
|
|
278
|
-
/** Poll one boat run to its settle. */
|
|
279
|
-
const settleRun = async (runPromptId) => {
|
|
280
|
-
const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
|
|
281
|
-
lastText = "";
|
|
282
|
-
pendingText = "";
|
|
283
|
-
problem = null;
|
|
284
|
-
for (;;) {
|
|
285
|
-
if (cancelled)
|
|
286
|
-
break;
|
|
287
|
-
await new Promise((r) => setTimeout(r, config.pollMs));
|
|
288
|
-
const events = await api(`/boxes/${boxId}/events`).catch(() => null);
|
|
289
|
-
const list = events?.events ?? events?.items ?? [];
|
|
290
|
-
for (const ev of list) {
|
|
291
|
-
// The stream is the whole conversation from its start; `taskId`
|
|
292
|
-
// names the prompt run an event belongs to. Earlier turns are
|
|
293
|
-
// history, not this answer.
|
|
294
|
-
if (runPromptId && ev.taskId && String(ev.taskId) !== runPromptId)
|
|
295
|
-
continue;
|
|
296
|
-
const id = String(ev.id ?? ev.eventId ?? JSON.stringify(ev).slice(0, 120));
|
|
297
|
-
if (seen.has(id))
|
|
298
|
-
continue;
|
|
299
|
-
seen.add(id);
|
|
300
|
-
appendNative(threadId, { dir: "in", source: "box.events", msg: ev });
|
|
301
|
-
const kind = String(ev.type ?? ev.kind ?? "");
|
|
302
|
-
// "response" events carry the agent's text at data.content —
|
|
303
|
-
// the FULL text so far, not a chunk. Clients accumulate
|
|
304
|
-
// deltas, so forward only the growth; a drifted (non-prefix)
|
|
305
|
-
// event re-sends whole and the settled message replaces the
|
|
306
|
-
// stream anyway.
|
|
307
|
-
const text = ev.text ?? ev.message ?? ev.data?.text ?? ev.data?.content ?? null;
|
|
308
|
-
if (/assistant|message|output|response/i.test(kind) && typeof text === "string" && text.trim()) {
|
|
309
|
-
ingest(text);
|
|
310
|
-
}
|
|
311
|
-
else if (/usage_limit|error|fail/i.test(kind)) {
|
|
312
|
-
const why = ev.data?.summary ?? ev.data?.message ?? ev.data?.error ?? ev.message;
|
|
313
|
-
if (typeof why === "string" && why.trim())
|
|
314
|
-
problem = why.trim();
|
|
315
|
-
}
|
|
316
|
-
else if (/tool|command|exec|browse/i.test(kind)) {
|
|
317
|
-
flushAssistantText();
|
|
318
|
-
emit({
|
|
319
|
-
...base(threadId, turnId),
|
|
320
|
-
type: "item.started",
|
|
321
|
-
itemType: "tool",
|
|
322
|
-
itemId: id,
|
|
323
|
-
title: String(ev.title ?? ev.command ?? kind).slice(0, 80),
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
// shape-drift backstop: without a promptId the status poll
|
|
327
|
-
// below can never see a terminal state, so settle off the
|
|
328
|
-
// events themselves instead of hanging to the 30-min ceiling
|
|
329
|
-
if (!runPromptId && /complete|finish|done|success|fail|error/i.test(kind)) {
|
|
330
|
-
const failed = /fail|error/i.test(kind);
|
|
331
|
-
return await finishRun(!failed, failed ? kind : null);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (runPromptId) {
|
|
335
|
-
const status = await api(`/boxes/${boxId}/prompts/${runPromptId}`).catch(() => null);
|
|
336
|
-
appendNative(threadId, { dir: "in", source: "box.prompt.status", msg: status });
|
|
337
|
-
// real shape (2026-08): {promptRun:{status:"finished",…}} —
|
|
338
|
-
// flat fallbacks kept for drift
|
|
339
|
-
const run = status?.promptRun ?? status?.prompt ?? status ?? {};
|
|
340
|
-
const state = String(run?.status ?? "");
|
|
341
|
-
if (/completed|succeeded|done|finished/i.test(state)) {
|
|
342
|
-
const result = run?.result ?? run?.output ?? lastText;
|
|
343
|
-
if (typeof result === "string" && result.trim() && result !== lastText) {
|
|
344
|
-
ingest(result);
|
|
345
|
-
}
|
|
346
|
-
if (!pendingText.trim() && !lastText.trim()) {
|
|
347
|
-
// a run that ends with nothing said and a recorded problem
|
|
348
|
-
// (login expired, …) is a failure the person must see
|
|
349
|
-
if (problem)
|
|
350
|
-
throw new Error(problem);
|
|
351
|
-
pendingText = "(finished)";
|
|
352
|
-
}
|
|
353
|
-
return await finishRun(true, null);
|
|
354
|
-
}
|
|
355
|
-
if (/failed|error|cancelled|interrupted/i.test(state)) {
|
|
356
|
-
const runError = [run?.error, run?.failureReason, run?.message].find((v) => typeof v === "string" && v.trim());
|
|
357
|
-
if (problem || runError || /failed|error/i.test(state))
|
|
358
|
-
throw new Error(problem ?? runError ?? `the box run ${state}`);
|
|
359
|
-
return await finishRun(false, state);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (Date.now() - startedAt > 30 * 60_000) {
|
|
363
|
-
throw new Error("box run exceeded 30 minutes — interrupted");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// cancelled
|
|
367
|
-
return await finishRun(false, "interrupted");
|
|
368
|
-
};
|
|
369
|
-
try {
|
|
370
|
-
const outcome = await settleRun(promptId);
|
|
371
|
-
active.delete(threadId);
|
|
372
|
-
emit({ ...base(threadId, turnId), type: "turn.completed", ok: outcome.ok, stopReason: outcome.stopReason, cost: null });
|
|
373
|
-
}
|
|
374
|
-
catch (e) {
|
|
375
|
-
flushAssistantText();
|
|
376
|
-
heldAsks.delete(threadId);
|
|
377
|
-
active.delete(threadId);
|
|
378
|
-
emit({ ...base(threadId, turnId), type: "runtime.error", message: e.message });
|
|
379
|
-
emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: "error", cost: null });
|
|
380
|
-
}
|
|
381
|
-
})();
|
|
382
|
-
return { turnId };
|
|
383
|
-
};
|
|
384
|
-
const snapshot = async () => {
|
|
385
|
-
if (!account()) {
|
|
386
|
-
return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
|
|
387
|
-
}
|
|
388
|
-
if (account()?.included)
|
|
389
|
-
return { state: "unavailable", reason: COMPUTER_ENGINE_CLOUD_UNAVAILABLE };
|
|
390
|
-
try {
|
|
391
|
-
await api("/me");
|
|
392
|
-
return { state: "available", authenticated: true, version: null };
|
|
393
|
-
}
|
|
394
|
-
catch (e) {
|
|
395
|
-
return { state: "unavailable", reason: `box API unreachable: ${e.message}` };
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
return {
|
|
399
|
-
instanceId,
|
|
400
|
-
driverKind: DRIVER_KIND,
|
|
401
|
-
displayName: input.displayName,
|
|
402
|
-
enabled: input.enabled,
|
|
403
|
-
models: MODELS,
|
|
404
|
-
snapshot,
|
|
405
|
-
adapter: {
|
|
406
|
-
provider: DRIVER_KIND,
|
|
407
|
-
capabilities: { sessionModelSwitch: "in-session", remoteAgent: true },
|
|
408
|
-
sendTurn,
|
|
409
|
-
interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
|
|
410
|
-
respondToRequest: async (threadId, requestId, decision) => {
|
|
411
|
-
const held = heldAsks.get(threadId);
|
|
412
|
-
if (!held || held.requestId !== requestId)
|
|
413
|
-
return "unavailable"; // settled, timed out, or restarted away
|
|
414
|
-
const reply = decision.message?.trim();
|
|
415
|
-
if (decision.behavior === "answer" && reply) {
|
|
416
|
-
held.settle(decision.message, "user");
|
|
417
|
-
return "answered";
|
|
418
|
-
}
|
|
419
|
-
if (decision.behavior === "deny") {
|
|
420
|
-
held.settle(null, "user");
|
|
421
|
-
return "rejected";
|
|
422
|
-
}
|
|
423
|
-
// "allow" is not an answer to a question, and blank text is not either
|
|
424
|
-
return "unavailable";
|
|
425
|
-
},
|
|
426
|
-
hasSession: (threadId) => active.has(threadId),
|
|
427
|
-
stopAll: async () => {
|
|
428
|
-
for (const { cancel } of active.values())
|
|
429
|
-
cancel();
|
|
430
|
-
},
|
|
431
|
-
onEvent: (listener) => {
|
|
432
|
-
listeners.add(listener);
|
|
433
|
-
return () => listeners.delete(listener);
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
dispose: async () => {
|
|
437
|
-
for (const { cancel } of active.values())
|
|
438
|
-
cancel();
|
|
439
|
-
listeners.clear();
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
},
|
|
443
|
-
};
|