openmausbot 0.1.89 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14810 -0
- package/dist-server/container-mcp.js +136 -36
- package/dist-server/drivers/agents-proxy.js +31 -10
- package/dist-server/index.js +48251 -44124
- package/dist-server/openmausbot.js +628 -292
- package/dist-server/pair-cli.js +628 -292
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +381 -334
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +121 -0
- package/dist-server/server/cloud-home.js +241 -0
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +156 -9
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +25 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +178 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1462 -576
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +42 -3
- package/dist-server/server/surface.js +14 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +12 -7
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +136 -36
- package/dist-server/workspace-backup.worker.js +165 -35
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -7,6 +7,52 @@
|
|
|
7
7
|
// gather state, call admit(), and apply the returned decision with their own
|
|
8
8
|
// side effects. Anything that can settle a turn (an awaited steer, a slow
|
|
9
9
|
// disk) belongs to the caller, which re-reads state and asks again.
|
|
10
|
+
// ── L2: ordering and batching on drain ─────────────────────────────────
|
|
11
|
+
// M2's drain rule: a sender's CONTIGUOUS burst inside a short window is one
|
|
12
|
+
// item; senders never merge, and a pause past the window reads as separate
|
|
13
|
+
// messages. The window is measured between consecutive queued items, so a
|
|
14
|
+
// rolling burst stays whole while hours-apart texts split.
|
|
15
|
+
/** How late one queued text may follow the previous one and still join the
|
|
16
|
+
* same drained turn. Two minutes covers a person typing a burst in pieces;
|
|
17
|
+
* anything slower is already a separate thought, not a continuation. */
|
|
18
|
+
export const DRAIN_COALESCE_WINDOW_MS = 120_000;
|
|
19
|
+
/** The most items one drained head group may carry. Room turns read the
|
|
20
|
+
* transcript through the room-context window (its last N messages), so a
|
|
21
|
+
* group larger than that window would append lines the responder never
|
|
22
|
+
* sees; the excess stays queued and drains with the next turn instead.
|
|
23
|
+
* The 1:1 drain carries every group item in its prompt directly, so it
|
|
24
|
+
* passes no cap. */
|
|
25
|
+
export const DRAIN_COALESCE_MAX_ITEMS = 30;
|
|
26
|
+
/** The leading run of queued items that drain together: consecutive items
|
|
27
|
+
* with the same merge identity, each arriving within the coalescing window
|
|
28
|
+
* of the one before it. Pure and shape-agnostic — each queue supplies its
|
|
29
|
+
* own identity (sender + provenance kind) and timestamp accessors, and the
|
|
30
|
+
* first item always drains, so an empty identity never strands a queue.
|
|
31
|
+
* `maxItems`, when given, stops the group before it outgrows a bound the
|
|
32
|
+
* consuming turn can actually carry. */
|
|
33
|
+
export function drainCoalesceHead(items, identityOf, queuedAtOf, maxItems) {
|
|
34
|
+
const head = [];
|
|
35
|
+
let previousIdentity;
|
|
36
|
+
let previousAt;
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
if (maxItems !== undefined && head.length >= maxItems)
|
|
39
|
+
break;
|
|
40
|
+
const identity = identityOf(item);
|
|
41
|
+
const at = queuedAtOf(item);
|
|
42
|
+
// The gap must be non-negative AND inside the window: a regressed
|
|
43
|
+
// timestamp (clock skew, a rewritten row) is not "within the window".
|
|
44
|
+
const gap = at - (previousAt ?? at);
|
|
45
|
+
if (head.length === 0 || (identity === previousIdentity && gap >= 0 && gap <= DRAIN_COALESCE_WINDOW_MS)) {
|
|
46
|
+
head.push(item);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
previousIdentity = identity;
|
|
52
|
+
previousAt = at;
|
|
53
|
+
}
|
|
54
|
+
return head;
|
|
55
|
+
}
|
|
10
56
|
/** The shared admission decision. Pure: same surface, message, and state
|
|
11
57
|
* always return the same decision, with no side effects. */
|
|
12
58
|
export function admit(surface, message = {}, state = {}) {
|
|
@@ -3,6 +3,7 @@ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "no
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.js";
|
|
5
5
|
import { DATA_DIR } from "./config.js";
|
|
6
|
+
// Persisted journal filenames keep their historical box-* names.
|
|
6
7
|
const FILE = join(DATA_DIR, "box-create-requests.json");
|
|
7
8
|
const LOCK_FILE = join(DATA_DIR, "box-create-requests.lock");
|
|
8
9
|
const KEY_RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
@@ -12,7 +13,7 @@ const LOCK_RETRY_MS = 20;
|
|
|
12
13
|
const MAX_REAPER_GENERATIONS = 128;
|
|
13
14
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
14
15
|
const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
|
|
15
|
-
const
|
|
16
|
+
const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
|
|
16
17
|
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
17
18
|
function isRequest(value) {
|
|
18
19
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -28,12 +29,12 @@ function isRequest(value) {
|
|
|
28
29
|
&& typeof request.createdAt === "number"
|
|
29
30
|
&& Number.isFinite(request.createdAt)
|
|
30
31
|
&& request.createdAt > 0
|
|
31
|
-
&& (request.boxId === undefined || (typeof request.boxId === "string" &&
|
|
32
|
+
&& (request.boxId === undefined || (typeof request.boxId === "string" && BOAT_ID.test(request.boxId)))
|
|
32
33
|
&& (request.resolved === undefined || (request.resolved === true && typeof request.boxId === "string")));
|
|
33
34
|
}
|
|
34
35
|
function recoveryStateError(detail, cause) {
|
|
35
36
|
return Object.assign(new Error(`Cloud computer creation is paused because its recovery state is ${detail}. `
|
|
36
|
-
+ "Check boat.dev for an unnamed
|
|
37
|
+
+ "Check boat.dev for an unnamed Boat before repairing OpenMausBot's local state."), { status: 503, cause });
|
|
37
38
|
}
|
|
38
39
|
function loadFresh() {
|
|
39
40
|
let raw;
|
|
@@ -57,17 +58,17 @@ function loadFresh() {
|
|
|
57
58
|
}
|
|
58
59
|
const identities = new Set();
|
|
59
60
|
const keys = new Set();
|
|
60
|
-
const
|
|
61
|
+
const botsWithKnownBoats = new Set();
|
|
61
62
|
for (const request of file.requests) {
|
|
62
63
|
const identity = `${request.botId}\0${request.requestBody}`;
|
|
63
64
|
if (identities.has(identity) || keys.has(request.idempotencyKey))
|
|
64
65
|
throw recoveryStateError("invalid");
|
|
65
|
-
if (request.boxId &&
|
|
66
|
+
if (request.boxId && botsWithKnownBoats.has(request.botId))
|
|
66
67
|
throw recoveryStateError("invalid");
|
|
67
68
|
identities.add(identity);
|
|
68
69
|
keys.add(request.idempotencyKey);
|
|
69
70
|
if (request.boxId)
|
|
70
|
-
|
|
71
|
+
botsWithKnownBoats.add(request.botId);
|
|
71
72
|
}
|
|
72
73
|
return file.requests.map((request) => ({ ...request }));
|
|
73
74
|
}
|
|
@@ -316,9 +317,9 @@ function withJournalLock(operation) {
|
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
/** Record the exact provider request before it leaves the process. A known
|
|
319
|
-
*
|
|
320
|
+
* Boat wins over a changed body: once a provider resource exists, recovering
|
|
320
321
|
* and naming it is safer than issuing any second create request. */
|
|
321
|
-
export function
|
|
322
|
+
export function beginBoatCreate(botId, requestBody) {
|
|
322
323
|
if (!BOT_ID.test(botId))
|
|
323
324
|
throw new Error("invalid bot id for cloud computer creation");
|
|
324
325
|
if (!requestBody || requestBody.length > 1_024)
|
|
@@ -335,7 +336,7 @@ export function beginBoxCreate(botId, requestBody) {
|
|
|
335
336
|
}
|
|
336
337
|
if (now - pending.createdAt >= KEY_RETENTION_MS) {
|
|
337
338
|
// Once the provider forgets the idempotency key, retrying it (or using
|
|
338
|
-
// a new key) may create a second billable
|
|
339
|
+
// a new key) may create a second billable Boat. Absence cannot be
|
|
339
340
|
// inferred from a lost response, so stop for manual reconciliation.
|
|
340
341
|
throw recoveryStateError("older than boat.dev's 24-hour retry window");
|
|
341
342
|
}
|
|
@@ -354,8 +355,8 @@ export function beginBoxCreate(botId, requestBody) {
|
|
|
354
355
|
});
|
|
355
356
|
}
|
|
356
357
|
/** Persist the returned identity before the caller attempts to rename it. */
|
|
357
|
-
export function
|
|
358
|
-
if (!
|
|
358
|
+
export function rememberCreatedBoat(request, boxId) {
|
|
359
|
+
if (!BOAT_ID.test(boxId))
|
|
359
360
|
throw new Error("boat.dev returned an invalid cloud computer id");
|
|
360
361
|
return withJournalLock((requests) => {
|
|
361
362
|
const current = requests.find((candidate) => (candidate.botId === request.botId
|
|
@@ -365,16 +366,16 @@ export function rememberCreatedBox(request, boxId) {
|
|
|
365
366
|
throw recoveryStateError("out of date");
|
|
366
367
|
}
|
|
367
368
|
const completed = { ...current, boxId };
|
|
368
|
-
// There can be an older rejected-TTL request for this bot. Once a
|
|
369
|
+
// There can be an older rejected-TTL request for this bot. Once a Boat is
|
|
369
370
|
// known, it is the only recovery authority we need to retain.
|
|
370
371
|
save([...requests.filter((candidate) => candidate.botId !== request.botId), completed]);
|
|
371
372
|
return { ...completed };
|
|
372
373
|
});
|
|
373
374
|
}
|
|
374
375
|
/** Mark the recovery record safe only after the deterministic provider rename
|
|
375
|
-
* succeeds. Keeping the resolved
|
|
376
|
+
* succeeds. Keeping the resolved Boat ID still lets a later retry recover from
|
|
376
377
|
* an eventually-consistent account listing without blocking bot deletion. */
|
|
377
|
-
export function
|
|
378
|
+
export function resolveBoatCreate(request) {
|
|
378
379
|
return withJournalLock((requests) => {
|
|
379
380
|
const current = requests.find((candidate) => (candidate.botId === request.botId
|
|
380
381
|
&& candidate.requestBody === request.requestBody
|
|
@@ -388,8 +389,8 @@ export function resolveBoxCreate(request) {
|
|
|
388
389
|
});
|
|
389
390
|
}
|
|
390
391
|
/** Read-only deletion guard. Both a key-only request with an ambiguous
|
|
391
|
-
* provider outcome and a known-but-not-yet-named
|
|
392
|
-
export function
|
|
392
|
+
* provider outcome and a known-but-not-yet-named Boat must keep its bot owner. */
|
|
393
|
+
export function hasUnresolvedBoatCreate(botId) {
|
|
393
394
|
if (!BOT_ID.test(botId))
|
|
394
395
|
throw new Error("invalid bot id for cloud computer creation");
|
|
395
396
|
return withJournalLock((requests) => (requests.some((request) => request.botId === botId && request.resolved !== true)));
|
|
@@ -398,30 +399,30 @@ export function hasUnresolvedBoxCreate(botId) {
|
|
|
398
399
|
* lock-protected read can prove a remembered provider id even while the
|
|
399
400
|
* account-wide LIST endpoint is eventually consistent. Provider request
|
|
400
401
|
* bodies and idempotency keys never cross this boundary. */
|
|
401
|
-
export function
|
|
402
|
+
export function boatCreateRecoverySnapshot() {
|
|
402
403
|
return withJournalLock((requests) => requests.map((request) => ({
|
|
403
404
|
botId: request.botId,
|
|
404
405
|
...(request.boxId ? { boxId: request.boxId } : {}),
|
|
405
406
|
resolved: request.resolved === true,
|
|
406
407
|
})));
|
|
407
408
|
}
|
|
408
|
-
/** Persist ownership discovered from a pre-journal deterministic
|
|
409
|
+
/** Persist ownership discovered from a pre-journal deterministic Boat name.
|
|
409
410
|
* The provider listing proves which live bot owns the exact immutable id, but
|
|
410
411
|
* it must never steal an identity remembered for another bot. It supersedes
|
|
411
412
|
* stale attempts for the same bot, is idempotent, and fails closed on a true
|
|
412
413
|
* cross-bot conflict. */
|
|
413
|
-
export function
|
|
414
|
+
export function adoptResolvedBoat(botId, boxId) {
|
|
414
415
|
if (!BOT_ID.test(botId))
|
|
415
416
|
throw new Error("invalid bot id for cloud computer ownership");
|
|
416
|
-
if (!
|
|
417
|
+
if (!BOAT_ID.test(boxId))
|
|
417
418
|
throw new Error("invalid cloud computer id for ownership");
|
|
418
419
|
withJournalLock((requests) => {
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
420
|
+
const sameBoat = requests.find((request) => request.boxId === boxId);
|
|
421
|
+
if (sameBoat && sameBoat.botId !== botId) {
|
|
421
422
|
throw recoveryStateError("conflicted with another bot's remembered cloud computer");
|
|
422
423
|
}
|
|
423
|
-
const adopted =
|
|
424
|
-
? { ...
|
|
424
|
+
const adopted = sameBoat
|
|
425
|
+
? { ...sameBoat, resolved: true }
|
|
425
426
|
: {
|
|
426
427
|
botId,
|
|
427
428
|
requestBody: JSON.stringify({ adopted: "legacy-name", boxId }),
|
|
@@ -434,19 +435,19 @@ export function adoptResolvedBox(botId, boxId) {
|
|
|
434
435
|
if (retained.length >= MAX_REQUESTS)
|
|
435
436
|
throw recoveryStateError("full");
|
|
436
437
|
if (requests.filter((request) => request.botId === botId).length === 1
|
|
437
|
-
&&
|
|
438
|
-
&&
|
|
438
|
+
&& sameBoat?.resolved === true
|
|
439
|
+
&& sameBoat.botId === botId)
|
|
439
440
|
return;
|
|
440
441
|
// A successfully listed deterministic legacy name resolves any older
|
|
441
442
|
// key-only/remembered attempt for this bot. Keeping those stale rows would
|
|
442
|
-
// leave deletion permanently blocked after the adopted
|
|
443
|
+
// leave deletion permanently blocked after the adopted Boat is removed.
|
|
443
444
|
save([...retained, adopted]);
|
|
444
445
|
});
|
|
445
446
|
}
|
|
446
|
-
/** Retire only the durable identity for a
|
|
447
|
+
/** Retire only the durable identity for a Boat the provider has confirmed was
|
|
447
448
|
* deleted. Callers must never use this for a failed or ambiguous deletion. */
|
|
448
|
-
export function
|
|
449
|
-
if (!
|
|
449
|
+
export function retireDeletedBoatCreate(boxId) {
|
|
450
|
+
if (!BOAT_ID.test(boxId))
|
|
450
451
|
throw new Error("invalid deleted cloud computer id");
|
|
451
452
|
withJournalLock((requests) => {
|
|
452
453
|
const next = requests.filter((request) => request.boxId !== boxId);
|
|
@@ -454,7 +455,7 @@ export function retireDeletedBoxCreate(boxId) {
|
|
|
454
455
|
save(next);
|
|
455
456
|
});
|
|
456
457
|
}
|
|
457
|
-
export function
|
|
458
|
+
export function discardBoatCreate(request) {
|
|
458
459
|
withJournalLock((requests) => {
|
|
459
460
|
const next = requests.filter((candidate) => candidate.idempotencyKey !== request.idempotencyKey);
|
|
460
461
|
if (next.length !== requests.length)
|
|
@@ -3,6 +3,7 @@ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "no
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.js";
|
|
5
5
|
import { DATA_DIR } from "./config.js";
|
|
6
|
+
// Persisted journal filenames keep their historical box-* names.
|
|
6
7
|
const FILE = join(DATA_DIR, "box-delete-requests.json");
|
|
7
8
|
const LOCK_FILE = join(DATA_DIR, "box-delete-requests.lock");
|
|
8
9
|
const MAX_RECORDS = 4_096;
|
|
@@ -11,7 +12,7 @@ const LOCK_RETRY_MS = 20;
|
|
|
11
12
|
const MAX_REAPER_GENERATIONS = 128;
|
|
12
13
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
13
14
|
const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
|
|
14
|
-
const
|
|
15
|
+
const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
|
|
15
16
|
const OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
|
|
16
17
|
const PROVIDER_STATUSES = new Set(["pending", "processing", "blocked", "completed"]);
|
|
17
18
|
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -29,7 +30,7 @@ function isRecord(value) {
|
|
|
29
30
|
return false;
|
|
30
31
|
const record = value;
|
|
31
32
|
if (typeof record.boxId !== "string"
|
|
32
|
-
|| !
|
|
33
|
+
|| !BOAT_ID.test(record.boxId)
|
|
33
34
|
|| !validName(record.name)
|
|
34
35
|
|| !(record.ownerBotId === null || (typeof record.ownerBotId === "string" && BOT_ID.test(record.ownerBotId)))
|
|
35
36
|
|| !["prepared", "accepted", "blocked"].includes(record.phase)
|
|
@@ -49,7 +50,7 @@ function isRecord(value) {
|
|
|
49
50
|
}
|
|
50
51
|
function stateError(detail, cause) {
|
|
51
52
|
return Object.assign(new Error(`Cloud computer deletion is paused because its recovery state is ${detail}. `
|
|
52
|
-
+ "Check the
|
|
53
|
+
+ "Check the Boat provider before repairing OpenMausBot's local state."), { status: 503, cause });
|
|
53
54
|
}
|
|
54
55
|
function clone(record) {
|
|
55
56
|
return { ...record };
|
|
@@ -76,12 +77,12 @@ function loadFresh() {
|
|
|
76
77
|
|| journal.records.length > MAX_RECORDS
|
|
77
78
|
|| !journal.records.every(isRecord))
|
|
78
79
|
throw stateError("invalid");
|
|
79
|
-
const
|
|
80
|
+
const boatIds = new Set();
|
|
80
81
|
const operationIds = new Set();
|
|
81
82
|
for (const record of journal.records) {
|
|
82
|
-
if (
|
|
83
|
+
if (boatIds.has(record.boxId))
|
|
83
84
|
throw stateError("invalid");
|
|
84
|
-
|
|
85
|
+
boatIds.add(record.boxId);
|
|
85
86
|
if (record.operationId) {
|
|
86
87
|
if (operationIds.has(record.operationId))
|
|
87
88
|
throw stateError("invalid");
|
|
@@ -319,7 +320,7 @@ function withJournalLock(operation) {
|
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
322
|
function validateIdentity(identity) {
|
|
322
|
-
if (!
|
|
323
|
+
if (!BOAT_ID.test(identity.boxId))
|
|
323
324
|
throw new Error("invalid cloud computer id for deletion");
|
|
324
325
|
if (!validName(identity.name))
|
|
325
326
|
throw new Error("invalid cloud computer name for deletion");
|
|
@@ -344,7 +345,7 @@ function parseOperation(boxId, value, expectedStatus) {
|
|
|
344
345
|
}
|
|
345
346
|
/** Write the immutable target before sending DELETE to the provider. Existing
|
|
346
347
|
* in-flight work wins; a blocked operation may be explicitly retried. */
|
|
347
|
-
export function
|
|
348
|
+
export function prepareBoatDeletion(identity) {
|
|
348
349
|
validateIdentity(identity);
|
|
349
350
|
return withJournalLock((records) => {
|
|
350
351
|
const existing = records.find((record) => record.boxId === identity.boxId);
|
|
@@ -368,14 +369,14 @@ export function prepareBoxDeletion(identity) {
|
|
|
368
369
|
return clone(prepared);
|
|
369
370
|
});
|
|
370
371
|
}
|
|
371
|
-
/** Bind a provider receipt to the exact immutable
|
|
372
|
-
* accidentally attach another
|
|
373
|
-
export function
|
|
374
|
-
if (!
|
|
372
|
+
/** Bind a provider receipt to the exact immutable Boat target. A caller cannot
|
|
373
|
+
* accidentally attach another Boat's operation to this deletion fence. */
|
|
374
|
+
export function markBoatDeletionAccepted(boxId, value) {
|
|
375
|
+
if (!BOAT_ID.test(boxId))
|
|
375
376
|
throw new Error("invalid cloud computer id for deletion");
|
|
376
377
|
const operation = parseOperation(boxId, value);
|
|
377
378
|
if (operation.status === "blocked") {
|
|
378
|
-
throw new Error("blocked cloud computer deletion must use
|
|
379
|
+
throw new Error("blocked cloud computer deletion must use markBoatDeletionBlocked");
|
|
379
380
|
}
|
|
380
381
|
const acceptedStatus = operation.status;
|
|
381
382
|
return withJournalLock((records) => {
|
|
@@ -412,8 +413,8 @@ export function markBoxDeletionAccepted(boxId, value) {
|
|
|
412
413
|
/** Record an explicit provider block without discarding the target. The
|
|
413
414
|
* target remains fenced from normal use; only a deliberate deletion retry
|
|
414
415
|
* may prepare the same immutable target again. */
|
|
415
|
-
export function
|
|
416
|
-
if (!
|
|
416
|
+
export function markBoatDeletionBlocked(boxId, value) {
|
|
417
|
+
if (!BOAT_ID.test(boxId))
|
|
417
418
|
throw new Error("invalid cloud computer id for deletion");
|
|
418
419
|
const operation = value === undefined ? null : parseOperation(boxId, value, "blocked");
|
|
419
420
|
return withJournalLock((records) => {
|
|
@@ -438,30 +439,30 @@ export function markBoxDeletionBlocked(boxId, value) {
|
|
|
438
439
|
});
|
|
439
440
|
}
|
|
440
441
|
/** A fresh, lock-protected copy suitable for server reconciliation. */
|
|
441
|
-
export function
|
|
442
|
+
export function boatDeletionSnapshot() {
|
|
442
443
|
return withJournalLock((records) => records.map(clone));
|
|
443
444
|
}
|
|
444
|
-
export const
|
|
445
|
-
export function
|
|
446
|
-
if (!
|
|
445
|
+
export const listBoatDeletions = boatDeletionSnapshot;
|
|
446
|
+
export function getBoatDeletion(boxId) {
|
|
447
|
+
if (!BOAT_ID.test(boxId))
|
|
447
448
|
throw new Error("invalid cloud computer id for deletion");
|
|
448
449
|
return withJournalLock((records) => {
|
|
449
450
|
const record = records.find((candidate) => candidate.boxId === boxId);
|
|
450
451
|
return record ? clone(record) : null;
|
|
451
452
|
});
|
|
452
453
|
}
|
|
453
|
-
export function
|
|
454
|
-
return
|
|
454
|
+
export function isBoatDeletionPending(boxId) {
|
|
455
|
+
return getBoatDeletion(boxId) !== null;
|
|
455
456
|
}
|
|
456
|
-
export function
|
|
457
|
+
export function hasPendingBoatDeletionForBot(botId) {
|
|
457
458
|
if (!BOT_ID.test(botId))
|
|
458
459
|
throw new Error("invalid bot id for cloud computer deletion");
|
|
459
460
|
return withJournalLock((records) => records.some((record) => record.ownerBotId === botId));
|
|
460
461
|
}
|
|
461
462
|
/** Remove a record only after direct absence or a completed target-bound
|
|
462
|
-
* provider operation proves the immutable
|
|
463
|
-
export function
|
|
464
|
-
if (!
|
|
463
|
+
* provider operation proves the immutable Boat identity is gone. */
|
|
464
|
+
export function retireBoatDeletion(boxId) {
|
|
465
|
+
if (!BOAT_ID.test(boxId))
|
|
465
466
|
throw new Error("invalid deleted cloud computer id");
|
|
466
467
|
withJournalLock((records) => {
|
|
467
468
|
const next = records.filter((record) => record.boxId !== boxId);
|