openmausbot 0.1.86 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +12 -1
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -7,6 +7,7 @@ import { redactSecretsInText } from "./redact.js";
|
|
|
7
7
|
const section = (value) => value?.trim() || "";
|
|
8
8
|
const teamName = z.string().trim().min(1).max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials");
|
|
9
9
|
const fieldsSchema = z.object({
|
|
10
|
+
chiefOfStaff: z.boolean().optional(),
|
|
10
11
|
name: z.string().optional(), title: z.string().optional(), description: z.string().optional(), soul: z.string().optional(),
|
|
11
12
|
section: z.string().trim().max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials").optional(),
|
|
12
13
|
modelSelection: z.object({ instanceId: z.string().trim().min(1), model: z.string().trim().min(1), effort: z.string().optional() }).strict().optional(),
|
|
@@ -44,7 +45,7 @@ export class TeamSetupRequestService {
|
|
|
44
45
|
return chief;
|
|
45
46
|
}
|
|
46
47
|
fields(input, current) {
|
|
47
|
-
const { section: targetSection, modelSelection, ...profile } = input;
|
|
48
|
+
const { section: targetSection, modelSelection, chiefOfStaff, ...profile } = input;
|
|
48
49
|
const safe = Object.fromEntries(Object.entries(profile).map(([key, value]) => [key, redactSecretsInText(value)]));
|
|
49
50
|
const parsed = parseBotProfilePatch(safe, true);
|
|
50
51
|
if (!parsed.ok)
|
|
@@ -52,6 +53,8 @@ export class TeamSetupRequestService {
|
|
|
52
53
|
const result = { ...parsed.patch };
|
|
53
54
|
if (targetSection !== undefined)
|
|
54
55
|
result.section = targetSection;
|
|
56
|
+
if (chiefOfStaff !== undefined)
|
|
57
|
+
result.chiefOfStaff = chiefOfStaff;
|
|
55
58
|
if (modelSelection) {
|
|
56
59
|
const selection = modelSelection;
|
|
57
60
|
const error = this.options.validateModel(selection, current);
|
|
@@ -108,7 +111,7 @@ export class TeamSetupRequestService {
|
|
|
108
111
|
throw new TeamSetupError("The destination team is outside this Chief's authorized scope", 403);
|
|
109
112
|
if (!request.newTeams.includes(section(destination)) && !existingTeams.has(section(destination)))
|
|
110
113
|
throw new TeamSetupError("The destination team no longer exists", 409);
|
|
111
|
-
const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: target?.chiefOfStaff, hidden: false };
|
|
114
|
+
const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: fields.chiefOfStaff ?? target?.chiefOfStaff, hidden: false };
|
|
112
115
|
const at = projected.findIndex((bot) => bot.id === operation.botId);
|
|
113
116
|
if (at < 0)
|
|
114
117
|
projected.push(next);
|
|
@@ -125,7 +128,7 @@ export class TeamSetupRequestService {
|
|
|
125
128
|
if (projected.some((bot) => bot.id !== candidate.id && !bot.hidden && bot.section === candidate.section && bot.name.trim().toLowerCase() === candidate.name.trim().toLowerCase()))
|
|
126
129
|
throw new TeamSetupError(`@${candidate.name} already exists in that team`, 409);
|
|
127
130
|
if (candidate.chiefOfStaff && projected.some((bot) => bot.id !== candidate.id && bot.chiefOfStaff && bot.section === candidate.section))
|
|
128
|
-
throw new TeamSetupError("
|
|
131
|
+
throw new TeamSetupError("Each team can have one Chief. Include the current Chief's demotion in this plan.", 409);
|
|
129
132
|
}
|
|
130
133
|
if (request.deletion) {
|
|
131
134
|
const target = this.options.store.bot(request.deletion.botId);
|
|
@@ -195,12 +198,20 @@ export class TeamSetupRequestService {
|
|
|
195
198
|
const lines = [`Why: ${request.reason}`];
|
|
196
199
|
if (request.deletion)
|
|
197
200
|
lines.push(`Delete @${request.deletion.name} (${request.deletion.botId}).`, "Permanently removes this bot, all its conversations, memory, instructions, skills, and any computer owned only by it. Generated project files and shared team computers remain. Active work or an unavailable provider can block deletion safely.");
|
|
198
|
-
if (request.newTeams.length)
|
|
199
|
-
lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}
|
|
201
|
+
if (request.newTeams.length) {
|
|
202
|
+
lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}.`);
|
|
203
|
+
if (!request.operations.some(op => op.botId === chief.id && op.fields.chiefOfStaff === false)) {
|
|
204
|
+
lines.push(`Authorize @${chief.name} to coordinate and propose setup changes in these new teams.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
200
207
|
for (const operation of request.operations) {
|
|
201
208
|
const current = this.options.store.bot(operation.botId);
|
|
202
209
|
lines.push(`\n${operation.action === "create" ? "Create" : "Update"} @${operation.fields.name ?? current?.name} (${operation.action === "create" ? "new bot" : operation.botId})`);
|
|
203
210
|
for (const [key, value] of Object.entries(operation.fields)) {
|
|
211
|
+
if (key === "chiefOfStaff") {
|
|
212
|
+
lines.push(`Chief of Staff: ${current?.chiefOfStaff ? "Yes" : "No"} → ${value ? "Yes" : "No"}.${value ? " May coordinate and configure bots in this team." : " Additional managed-team access is removed."}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
204
215
|
const before = current ? (key === "section" ? current.section || "General" : current[key]) : undefined;
|
|
205
216
|
lines.push(`${key === "modelSelection" ? "Default engine/model" : key}: ${current ? `${JSON.stringify(before ?? "")} → ` : ""}${key === "section" ? JSON.stringify(value || "General") : JSON.stringify(value)}`);
|
|
206
217
|
}
|
|
@@ -133,6 +133,10 @@ const stringOrMissing = (value) => value === undefined || typeof value === "stri
|
|
|
133
133
|
const stringOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "string";
|
|
134
134
|
const numberOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "number";
|
|
135
135
|
const stringsOrMissing = (value) => value === undefined || (Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
136
|
+
/** A computer holder as the wait events carry it: a name, and the holding
|
|
137
|
+
* thread's title when one was known. */
|
|
138
|
+
const computerHolderOrMissing = (value) => value === undefined ||
|
|
139
|
+
(isRecord(value) && typeof value.name === "string" && stringOrMissing(value.task));
|
|
136
140
|
/** A replayed structured ask. Only the shape the card actually reads is
|
|
137
141
|
* required; the rest is optional and simply absent on an older event. */
|
|
138
142
|
const askQuestionsOrMissing = (value) => value === undefined ||
|
|
@@ -175,6 +179,15 @@ function isRuntimeEvent(value) {
|
|
|
175
179
|
stringsOrMissing(value.denials) &&
|
|
176
180
|
(value.usage === undefined ||
|
|
177
181
|
(isRecord(value.usage) && typeof value.usage.input === "number" && typeof value.usage.output === "number")));
|
|
182
|
+
case "turn.wait_started":
|
|
183
|
+
return typeof value.resource === "string" && computerHolderOrMissing(value.holder);
|
|
184
|
+
case "turn.wait_ended":
|
|
185
|
+
return (typeof value.resource === "string" &&
|
|
186
|
+
computerHolderOrMissing(value.holder) &&
|
|
187
|
+
typeof value.waitedMs === "number" &&
|
|
188
|
+
Number.isFinite(value.waitedMs) &&
|
|
189
|
+
value.waitedMs >= 0 &&
|
|
190
|
+
(value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "stopped"));
|
|
178
191
|
case "item.started":
|
|
179
192
|
return (value.itemType === "tool" || value.itemType === "reasoning") && stringOrMissing(value.title);
|
|
180
193
|
case "item.updated":
|
|
@@ -188,7 +201,10 @@ function isRuntimeEvent(value) {
|
|
|
188
201
|
typeof value.tool === "string" &&
|
|
189
202
|
typeof value.summary === "string" &&
|
|
190
203
|
stringsOrMissing(value.choices) &&
|
|
191
|
-
askQuestionsOrMissing(value.questions)
|
|
204
|
+
askQuestionsOrMissing(value.questions) &&
|
|
205
|
+
// Required fields only: events recorded before origin exists replay
|
|
206
|
+
// unchanged, and a value other than the two documented marks is not.
|
|
207
|
+
(value.origin === undefined || value.origin === "tool" || value.origin === "output"));
|
|
192
208
|
case "request.resolved":
|
|
193
209
|
return ((value.behavior === "allow" || value.behavior === "deny" || value.behavior === "answer") &&
|
|
194
210
|
(value.source === "user" ||
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { peerName } from "./peer-roster.js";
|
|
2
|
+
// All three use createOpenAIChatRuntime and get history via SendTurnInput.transcript.
|
|
3
|
+
export const NATIVELY_REPLAYING_DRIVER_KINDS = ["grok", "openai-compat", "minimax"];
|
|
2
4
|
/** Does this engine need the thread replayed to it? True when a DIFFERENT
|
|
3
5
|
* instance ran the last turn here — a cursor of our own is not enough,
|
|
4
6
|
* because it only proves we once had a session covering some prefix of the
|
|
@@ -23,10 +23,10 @@ export class TurnWatchdog {
|
|
|
23
23
|
this.timer = null;
|
|
24
24
|
this.turns.clear();
|
|
25
25
|
}
|
|
26
|
-
/** A turn was
|
|
26
|
+
/** A turn was admitted on this thread. */
|
|
27
27
|
watch(threadId, botId) {
|
|
28
28
|
const at = this.now();
|
|
29
|
-
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false });
|
|
29
|
+
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false, waitingOnComputer: false });
|
|
30
30
|
}
|
|
31
31
|
/** Any provider event for the thread proves the turn is alive. */
|
|
32
32
|
touch(threadId) {
|
|
@@ -43,6 +43,15 @@ export class TurnWatchdog {
|
|
|
43
43
|
turn.waitingOnHuman = waiting;
|
|
44
44
|
turn.lastEventAt = this.now();
|
|
45
45
|
}
|
|
46
|
+
/** Shared-computer waits have their own bounded deadline. Waiting for
|
|
47
|
+
* another turn to release the computer is not a silent provider stall. */
|
|
48
|
+
setWaitingOnComputer(threadId, waiting) {
|
|
49
|
+
const turn = this.turns.get(threadId);
|
|
50
|
+
if (!turn)
|
|
51
|
+
return;
|
|
52
|
+
turn.waitingOnComputer = waiting;
|
|
53
|
+
turn.lastEventAt = this.now();
|
|
54
|
+
}
|
|
46
55
|
/** The turn settled normally — stop watching it. */
|
|
47
56
|
settle(threadId) {
|
|
48
57
|
this.turns.delete(threadId);
|
|
@@ -54,7 +63,7 @@ export class TurnWatchdog {
|
|
|
54
63
|
sweep() {
|
|
55
64
|
const at = this.now();
|
|
56
65
|
for (const turn of this.turns.values()) {
|
|
57
|
-
if (turn.waitingOnHuman)
|
|
66
|
+
if (turn.waitingOnHuman || turn.waitingOnComputer)
|
|
58
67
|
continue;
|
|
59
68
|
if (at - turn.lastEventAt < this.opts.stallMs)
|
|
60
69
|
continue;
|
|
@@ -18,6 +18,14 @@ const DAY_MS = 24 * 60 * 60_000;
|
|
|
18
18
|
const writeQueues = new Map();
|
|
19
19
|
const clean = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
20
20
|
const finiteOrNull = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
21
|
+
const promptBytesOf = (value) => {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return null;
|
|
24
|
+
const record = value;
|
|
25
|
+
return typeof record.stable === "number" && typeof record.volatile === "number"
|
|
26
|
+
? { stable: clean(record.stable), volatile: clean(record.volatile) }
|
|
27
|
+
: null;
|
|
28
|
+
};
|
|
21
29
|
function monthKey(at) {
|
|
22
30
|
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
23
31
|
}
|
|
@@ -34,12 +42,15 @@ export function usageRowKey(row) {
|
|
|
34
42
|
/** Append one settled turn. Fire-and-forget (see the module comment): the
|
|
35
43
|
* returned promise never rejects, and says whether the row reached disk. */
|
|
36
44
|
export function appendUsage(dataDir, row) {
|
|
45
|
+
const { promptBytes: rawPromptBytes, ...rest } = row;
|
|
46
|
+
const promptBytes = promptBytesOf(rawPromptBytes);
|
|
37
47
|
const record = {
|
|
38
|
-
...
|
|
48
|
+
...rest,
|
|
39
49
|
at: row.at ?? new Date().toISOString(),
|
|
40
50
|
input: clean(row.input),
|
|
41
51
|
output: clean(row.output),
|
|
42
52
|
...(typeof row.cachedInput === "number" ? { cachedInput: clean(row.cachedInput) } : {}),
|
|
53
|
+
...(promptBytes ? { promptBytes } : {}),
|
|
43
54
|
costUsd: finiteOrNull(row.costUsd),
|
|
44
55
|
};
|
|
45
56
|
// A cost is labelled with where it came from; an unpriced row carries no label.
|
|
@@ -6,7 +6,8 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { createConnection, createServer } from "node:net";
|
|
7
7
|
import { BASE_IMAGE, CUA_DRIVER_VERSION, CUA_SOCKET, IMAGE as CUA_IMAGE, cuaExecArgs, dockerSecurityIsHardened, imageLabelsMatch, managedImageDockerfile, wholeScreenshot, BASE_IMAGE_DIGEST, BASE_IMAGE_LABEL, DRIVER_LABEL, IMAGE_LAYER_LABEL, IMAGE_LAYER_VERSION, MANAGED_LABEL, } from "./container-computer.js";
|
|
8
8
|
import { DATA_DIR, isValidSshAlias, vpsSshAlias } from "./config.js";
|
|
9
|
-
import { augmentedPath
|
|
9
|
+
import { augmentedPath } from "./env-path.js";
|
|
10
|
+
import { killCliTree, spawnCli } from "./procs.js";
|
|
10
11
|
import { prepareVpsSsh } from "./vps-ssh.js";
|
|
11
12
|
import { loadEnvironmentId } from "./environment.js";
|
|
12
13
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
@@ -28,7 +29,9 @@ function vpsEnvironmentId() {
|
|
|
28
29
|
// SIGTERM must give ssh + docker time to tear down the remote exec before the
|
|
29
30
|
// SIGKILL escalation; 1s was routinely too short over a WAN round-trip, and an
|
|
30
31
|
// orphaned remote exec keeps the driver socket busy for the next command.
|
|
31
|
-
const
|
|
32
|
+
const COMMAND_TIMEOUT_TERM_GRACE_MS = 5_000;
|
|
33
|
+
// killCliTree also waits up to one second for the forced group exit.
|
|
34
|
+
const COMMAND_TIMEOUT_KILL_GRACE_MS = COMMAND_TIMEOUT_TERM_GRACE_MS + 1_000;
|
|
32
35
|
const CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/;
|
|
33
36
|
const CONTAINER_ID = /^[a-f0-9]{12,64}$/i;
|
|
34
37
|
const FULL_CONTAINER_ID = /^[a-f0-9]{64}$/i;
|
|
@@ -63,20 +66,32 @@ function snapshotVpsConfig(cfg) {
|
|
|
63
66
|
export function vpsLifecycleBusy() {
|
|
64
67
|
return lifecycleLocks.size > 0;
|
|
65
68
|
}
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
+
// Sleep, remove and preview requests must not queue behind a potentially
|
|
70
|
+
// 10-minute image build. Turn preparation may instead wait through a normal
|
|
71
|
+
// preview, whose bounded duration is accounted for below.
|
|
69
72
|
const LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
|
|
70
73
|
// The panel polls status every 4-6s and the screen poller re-checks it before
|
|
71
74
|
// every frame; each full status is several docker-over-SSH processes. Same
|
|
72
75
|
// pattern as container-computer's screenshotStatusCache, and the same TTL.
|
|
73
76
|
const STATUS_CACHE_TTL_MS = 10_000;
|
|
74
77
|
const statusCache = new Map();
|
|
78
|
+
const pendingStatuses = new Map();
|
|
79
|
+
const pendingProvisions = new Map();
|
|
75
80
|
const SCREENSHOT_BUDGET_MS = 45_000;
|
|
76
81
|
const SCREENSHOT_CLEANUP_BUDGET_MS = 10_000;
|
|
82
|
+
// Turn preparation may wait for a normal preview to finish; destructive
|
|
83
|
+
// actions retain the short acquisition limit. Overlapping provisions share
|
|
84
|
+
// one operation below, so they never queue behind their own image build.
|
|
85
|
+
const PREPARATION_LOCK_TIMEOUT_MS = SCREENSHOT_BUDGET_MS + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
77
86
|
const pendingScreenshots = new Map();
|
|
78
87
|
const viewerConnections = new Map();
|
|
79
88
|
const desktopTunnels = new Map();
|
|
89
|
+
function invalidateVpsStatus(key) {
|
|
90
|
+
statusCache.delete(key);
|
|
91
|
+
// Detach old polls as well: an inspection begun before a mutation must
|
|
92
|
+
// neither be joined nor republish its old result after that mutation.
|
|
93
|
+
pendingStatuses.delete(key);
|
|
94
|
+
}
|
|
80
95
|
/** Whether a turn may prepare or start the VPS container rather than only
|
|
81
96
|
* reuse a running one. Explicit Cloud always may; Auto only when the person
|
|
82
97
|
* switched on Start VPS automatically — except for unattended runs. A
|
|
@@ -216,11 +231,10 @@ function tailCollector() {
|
|
|
216
231
|
}
|
|
217
232
|
export function defaultRunner(args, options = {}) {
|
|
218
233
|
return new Promise((resolve, reject) => {
|
|
219
|
-
const command = resolveCliSpawn("docker", args);
|
|
220
234
|
// docker's SSH transport runs the first `ssh` on PATH: the app's shim,
|
|
221
235
|
// which shares one connection across every command of this VPS.
|
|
222
236
|
const ssh = prepareVpsSsh(DATA_DIR, augmentedPath());
|
|
223
|
-
const child =
|
|
237
|
+
const child = spawnCli("docker", args, {
|
|
224
238
|
shell: false,
|
|
225
239
|
env: { ...process.env, PATH: ssh.path },
|
|
226
240
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -228,31 +242,33 @@ export function defaultRunner(args, options = {}) {
|
|
|
228
242
|
const stdout = tailCollector();
|
|
229
243
|
const stderr = tailCollector();
|
|
230
244
|
let settled = false;
|
|
231
|
-
let
|
|
232
|
-
let killTimer;
|
|
245
|
+
let failure = null;
|
|
233
246
|
let timeout;
|
|
234
247
|
const settle = (finish) => {
|
|
235
248
|
if (settled)
|
|
236
249
|
return;
|
|
237
250
|
settled = true;
|
|
238
251
|
clearTimeout(timeout);
|
|
239
|
-
if (killTimer)
|
|
240
|
-
clearTimeout(killTimer);
|
|
241
252
|
finish();
|
|
242
253
|
};
|
|
243
|
-
|
|
244
|
-
if (settled)
|
|
254
|
+
const fail = (error) => {
|
|
255
|
+
if (settled || failure)
|
|
245
256
|
return;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
failure = error;
|
|
258
|
+
clearTimeout(timeout);
|
|
259
|
+
// Docker launches ssh, which can retain these pipes after Docker exits.
|
|
260
|
+
// Only this command's private process group is ours to stop; persistent
|
|
261
|
+
// SSH masters detach from it and may serve other commands. Hold callers'
|
|
262
|
+
// lifecycle ownership until the bounded tree cleanup has completed.
|
|
263
|
+
const cleaned = () => {
|
|
264
|
+
child.stdin.destroy();
|
|
265
|
+
child.stdout.destroy();
|
|
266
|
+
child.stderr.destroy();
|
|
267
|
+
settle(() => reject(error));
|
|
268
|
+
};
|
|
269
|
+
void killCliTree(child, COMMAND_TIMEOUT_TERM_GRACE_MS).then(cleaned, cleaned);
|
|
270
|
+
};
|
|
271
|
+
timeout = setTimeout(() => fail(new Error("Docker-over-SSH command timed out")), options.timeoutMs ?? 120_000);
|
|
256
272
|
timeout.unref?.();
|
|
257
273
|
child.stdout.setEncoding("utf8");
|
|
258
274
|
child.stderr.setEncoding("utf8");
|
|
@@ -263,18 +279,14 @@ export function defaultRunner(args, options = {}) {
|
|
|
263
279
|
stderr.push(chunk);
|
|
264
280
|
});
|
|
265
281
|
child.stdin.on("error", (error) => {
|
|
266
|
-
|
|
267
|
-
return;
|
|
268
|
-
settle(() => reject(new Error(`Docker-over-SSH stdin failed: ${error.message}`)));
|
|
282
|
+
fail(new Error(`Docker-over-SSH stdin failed: ${error.message}`));
|
|
269
283
|
});
|
|
270
284
|
child.on("error", (error) => {
|
|
271
|
-
|
|
285
|
+
fail(new Error(`Docker-over-SSH could not start: ${error.message}`));
|
|
272
286
|
});
|
|
273
287
|
child.on("close", (code, signal) => {
|
|
274
|
-
if (
|
|
275
|
-
settle(() => reject(new Error("Docker-over-SSH command timed out")));
|
|
288
|
+
if (failure)
|
|
276
289
|
return;
|
|
277
|
-
}
|
|
278
290
|
settle(() => {
|
|
279
291
|
if (code === 0)
|
|
280
292
|
return resolve({ stdout: stdout.text(), stderr: stderr.text() });
|
|
@@ -286,7 +298,7 @@ export function defaultRunner(args, options = {}) {
|
|
|
286
298
|
child.stdin.end(options.input);
|
|
287
299
|
}
|
|
288
300
|
catch (error) {
|
|
289
|
-
|
|
301
|
+
fail(new Error(`Docker-over-SSH stdin failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
290
302
|
}
|
|
291
303
|
});
|
|
292
304
|
}
|
|
@@ -531,15 +543,30 @@ export async function vpsComputerStatus(cfg, botId, runner = defaultRunner) {
|
|
|
531
543
|
if (cacheable) {
|
|
532
544
|
// A capture already checks this exact target. Let it finish instead of
|
|
533
545
|
// opening another six SSH connections while its readiness check is cold.
|
|
534
|
-
|
|
546
|
+
const capture = pendingScreenshots.get(key);
|
|
547
|
+
if (capture)
|
|
548
|
+
await capture.catch(() => { });
|
|
535
549
|
const cached = statusCache.get(key);
|
|
536
550
|
if (cached && cached.expiresAt > Date.now())
|
|
537
551
|
return cached.status;
|
|
552
|
+
const pending = pendingStatuses.get(key);
|
|
553
|
+
if (pending)
|
|
554
|
+
return pending;
|
|
538
555
|
}
|
|
539
|
-
const
|
|
556
|
+
const inspection = computeVpsComputerStatus(cfg, botId, runner);
|
|
540
557
|
if (cacheable)
|
|
541
|
-
|
|
542
|
-
|
|
558
|
+
pendingStatuses.set(key, inspection);
|
|
559
|
+
try {
|
|
560
|
+
const status = await inspection;
|
|
561
|
+
if (cacheable && pendingStatuses.get(key) === inspection && !lifecycleLocks.has(key)) {
|
|
562
|
+
statusCache.set(key, { status, expiresAt: Date.now() + STATUS_CACHE_TTL_MS });
|
|
563
|
+
}
|
|
564
|
+
return status;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
if (cacheable && pendingStatuses.get(key) === inspection)
|
|
568
|
+
pendingStatuses.delete(key);
|
|
569
|
+
}
|
|
543
570
|
}
|
|
544
571
|
const VPS_INVENTORY_LIMIT = 256;
|
|
545
572
|
const VPS_INVENTORY_STATES = new Set([
|
|
@@ -761,14 +788,32 @@ async function prepareVpsImage(alias, runner) {
|
|
|
761
788
|
* Backoff doubles 0.5s→4s because each poll is a real SSH connection, and
|
|
762
789
|
* between polls only ONE cheap exec (`cua-driver status`) asks whether the
|
|
763
790
|
* driver answers yet — the full multi-invocation status runs again only when
|
|
764
|
-
* that predicate flips
|
|
765
|
-
*
|
|
791
|
+
* that predicate flips. Every command, including the diagnostic tail, is
|
|
792
|
+
* clamped to the readiness phase's deadline and transport cleanup allowance. */
|
|
766
793
|
async function waitForVpsReady(cfg, botId, runner, budgetMs = 60_000) {
|
|
767
794
|
const alias = vpsSshAlias(cfg);
|
|
768
795
|
const deadline = Date.now() + budgetMs;
|
|
769
|
-
let
|
|
796
|
+
let budgetExpired = false;
|
|
797
|
+
const boundedRunner = async (args, options = {}) => {
|
|
798
|
+
// Include transport termination in the readiness phase's budget; a
|
|
799
|
+
// final failed probe or diagnostic tail must not keep its lock past it.
|
|
800
|
+
const remaining = deadline - Date.now() - COMMAND_TIMEOUT_KILL_GRACE_MS;
|
|
801
|
+
if (remaining <= 0) {
|
|
802
|
+
budgetExpired = true;
|
|
803
|
+
throw new Error("VPS desktop readiness timed out");
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
return await runner(args, { ...options, timeoutMs: Math.min(options.timeoutMs ?? 120_000, remaining) });
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (Date.now() >= deadline - COMMAND_TIMEOUT_KILL_GRACE_MS)
|
|
810
|
+
budgetExpired = true;
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
let status = await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
770
815
|
let delayMs = 500;
|
|
771
|
-
while (!status.ready && alias && Date.now() < deadline) {
|
|
816
|
+
while (!status.ready && alias && !budgetExpired && Date.now() < deadline) {
|
|
772
817
|
if (!status.daemonUp ||
|
|
773
818
|
!status.image ||
|
|
774
819
|
!status.imageMatches ||
|
|
@@ -785,14 +830,24 @@ async function waitForVpsReady(cfg, botId, runner, budgetMs = 60_000) {
|
|
|
785
830
|
});
|
|
786
831
|
delayMs = Math.min(delayMs * 2, 4_000);
|
|
787
832
|
const container = status.container_id ?? status.container_name;
|
|
788
|
-
const driverAnswers = await
|
|
833
|
+
const driverAnswers = await boundedRunner(vpsDockerArgs(alias, cuaExecArgs(["status", "--socket", CUA_SOCKET], { container })), { timeoutMs: 10_000 }).then(() => true, () => false);
|
|
834
|
+
if (budgetExpired)
|
|
835
|
+
break;
|
|
789
836
|
if (!driverAnswers && Date.now() < deadline)
|
|
790
837
|
continue;
|
|
791
|
-
status = await computeVpsComputerStatus(cfg, botId,
|
|
838
|
+
status = await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
792
839
|
}
|
|
840
|
+
if (budgetExpired)
|
|
841
|
+
return {
|
|
842
|
+
...status,
|
|
843
|
+
ready: false,
|
|
844
|
+
desktopReady: false,
|
|
845
|
+
desktop_error: "VPS desktop readiness timed out",
|
|
846
|
+
problem: "The VPS desktop did not become ready in time — retry when the connection recovers",
|
|
847
|
+
};
|
|
793
848
|
return status;
|
|
794
849
|
}
|
|
795
|
-
async function withVpsLifecycleLock(key, operation) {
|
|
850
|
+
async function withVpsLifecycleLock(key, operation, acquireTimeoutMs = LOCK_ACQUIRE_TIMEOUT_MS) {
|
|
796
851
|
const previous = lifecycleLocks.get(key);
|
|
797
852
|
let release;
|
|
798
853
|
const current = new Promise((resolve) => {
|
|
@@ -804,7 +859,7 @@ async function withVpsLifecycleLock(key, operation) {
|
|
|
804
859
|
const acquired = await Promise.race([
|
|
805
860
|
previous.then(() => true),
|
|
806
861
|
new Promise((resolve) => {
|
|
807
|
-
acquireTimer = setTimeout(() => resolve(false),
|
|
862
|
+
acquireTimer = setTimeout(() => resolve(false), acquireTimeoutMs);
|
|
808
863
|
acquireTimer.unref?.();
|
|
809
864
|
}),
|
|
810
865
|
]);
|
|
@@ -849,6 +904,7 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
849
904
|
}
|
|
850
905
|
const key = `${alias}:${containerName}`;
|
|
851
906
|
return withVpsLifecycleLock(key, async () => {
|
|
907
|
+
invalidateVpsStatus(key);
|
|
852
908
|
const scan = await scanManagedVpsComputers(cfg, owners, runner);
|
|
853
909
|
const inventory = scan.inventory;
|
|
854
910
|
if (!inventory.available) {
|
|
@@ -874,13 +930,14 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
874
930
|
await runner(vpsDockerArgs(alias, ["rm", "-f", containerId]), { timeoutMs: 2 * 60_000 });
|
|
875
931
|
}
|
|
876
932
|
catch (error) {
|
|
933
|
+
invalidateVpsStatus(key);
|
|
877
934
|
const detail = (error instanceof Error ? error.message : String(error))
|
|
878
935
|
.replace(/[\r\n\t]+/g, " ")
|
|
879
936
|
.trim()
|
|
880
937
|
.slice(0, 200);
|
|
881
938
|
throw Object.assign(new Error(`The VPS refused to remove this computer${detail ? `: ${detail}` : ""}`), { status: 502 });
|
|
882
939
|
}
|
|
883
|
-
|
|
940
|
+
invalidateVpsStatus(key);
|
|
884
941
|
viewerConnections.delete(key);
|
|
885
942
|
if (instance.ownerBotId)
|
|
886
943
|
stopDesktopTunnel(instance.ownerBotId);
|
|
@@ -893,14 +950,19 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
893
950
|
if (!alias)
|
|
894
951
|
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in App Settings → Connections"), { status: 409 });
|
|
895
952
|
const key = `${alias}:${vpsContainerName(botId)}`;
|
|
953
|
+
const pending = action === "provision" ? pendingProvisions.get(key) : undefined;
|
|
954
|
+
if (pending)
|
|
955
|
+
return pending;
|
|
896
956
|
const operation = async () => {
|
|
897
957
|
// A mutation invalidates every cached poll answer, before and after: the
|
|
898
958
|
// panel must never keep showing the pre-action world for a TTL.
|
|
899
|
-
|
|
959
|
+
invalidateVpsStatus(key);
|
|
900
960
|
try {
|
|
901
961
|
const before = await computeVpsComputerStatus(cfg, botId, runner);
|
|
902
962
|
if (!before.daemonUp)
|
|
903
963
|
throw Object.assign(new Error(before.problem ?? "Docker over SSH is not reachable"), { status: 409 });
|
|
964
|
+
if (action === "provision" && before.ready)
|
|
965
|
+
return before;
|
|
904
966
|
// A real lifecycle change invalidates the remote endpoint. Provision
|
|
905
967
|
// is also the turn-start idempotency path, so leave an already-running
|
|
906
968
|
// viewer alone when no start/replacement will occur.
|
|
@@ -946,7 +1008,7 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
946
1008
|
throw Object.assign(new Error("The VPS container name is occupied by a container OpenMausBot did not create — remove it on the VPS yourself"), { status: 409 });
|
|
947
1009
|
}
|
|
948
1010
|
await run(["rm", "-f", containerRef]);
|
|
949
|
-
return computeVpsComputerStatus(cfg, botId, runner);
|
|
1011
|
+
return await computeVpsComputerStatus(cfg, botId, runner);
|
|
950
1012
|
}
|
|
951
1013
|
else {
|
|
952
1014
|
if (before.container !== "running")
|
|
@@ -954,13 +1016,22 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
954
1016
|
assertUsableContainer(before);
|
|
955
1017
|
await run(["stop", containerRef]);
|
|
956
1018
|
}
|
|
957
|
-
return action === "stop" ? computeVpsComputerStatus(cfg, botId, runner) : waitForVpsReady(cfg, botId, runner);
|
|
1019
|
+
return await (action === "stop" ? computeVpsComputerStatus(cfg, botId, runner) : waitForVpsReady(cfg, botId, runner));
|
|
958
1020
|
}
|
|
959
1021
|
finally {
|
|
960
|
-
|
|
1022
|
+
invalidateVpsStatus(key);
|
|
961
1023
|
}
|
|
962
1024
|
};
|
|
963
|
-
|
|
1025
|
+
const preparation = withVpsLifecycleLock(key, operation, action === "provision" ? PREPARATION_LOCK_TIMEOUT_MS : LOCK_ACQUIRE_TIMEOUT_MS);
|
|
1026
|
+
if (action === "provision")
|
|
1027
|
+
pendingProvisions.set(key, preparation);
|
|
1028
|
+
try {
|
|
1029
|
+
return await preparation;
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
if (pendingProvisions.get(key) === preparation)
|
|
1033
|
+
pendingProvisions.delete(key);
|
|
1034
|
+
}
|
|
964
1035
|
}
|
|
965
1036
|
/** Auto is intentionally read-only: it can attach only to an existing ready
|
|
966
1037
|
* container. It recomputes rather than reading the poll cache — routing a
|
|
@@ -976,7 +1047,7 @@ export async function inspectVpsForAuto(cfg, botId, runner = defaultRunner) {
|
|
|
976
1047
|
cfg = snapshotVpsConfig(cfg);
|
|
977
1048
|
const key = vpsLockKey(cfg, botId);
|
|
978
1049
|
return key
|
|
979
|
-
? withVpsLifecycleLock(key, () => computeVpsComputerStatus(cfg, botId, runner))
|
|
1050
|
+
? withVpsLifecycleLock(key, () => computeVpsComputerStatus(cfg, botId, runner), PREPARATION_LOCK_TIMEOUT_MS)
|
|
980
1051
|
: computeVpsComputerStatus(cfg, botId, runner);
|
|
981
1052
|
}
|
|
982
1053
|
/** Open a temporary noVNC connection through the configured SSH alias. The
|
|
@@ -1116,9 +1187,26 @@ export async function vpsComputerScreenshot(cfg, botId, runner = defaultRunner)
|
|
|
1116
1187
|
// poller runs every few seconds, and re-verifying the whole container
|
|
1117
1188
|
// between frames multiplied every frame's SSH cost.
|
|
1118
1189
|
const cached = cacheable ? statusCache.get(key) : undefined;
|
|
1190
|
+
const pendingStatus = cacheable ? pendingStatuses.get(key) : undefined;
|
|
1191
|
+
// A poll which started first already owns the inspection. Sharing its
|
|
1192
|
+
// result must not extend this preview's own deadline; the poll remains
|
|
1193
|
+
// independently owned if it is still checking when this preview expires.
|
|
1194
|
+
let statusTimer;
|
|
1119
1195
|
const status = cached && cached.expiresAt > Date.now()
|
|
1120
1196
|
? cached.status
|
|
1121
|
-
:
|
|
1197
|
+
: pendingStatus
|
|
1198
|
+
? await Promise.race([
|
|
1199
|
+
pendingStatus,
|
|
1200
|
+
new Promise((_resolve, reject) => {
|
|
1201
|
+
statusTimer = setTimeout(() => {
|
|
1202
|
+
invalidateVpsStatus(key);
|
|
1203
|
+
reject(timeoutError());
|
|
1204
|
+
}, Math.max(0, workDeadline - Date.now()));
|
|
1205
|
+
statusTimer.unref?.();
|
|
1206
|
+
}),
|
|
1207
|
+
]).finally(() => { if (statusTimer)
|
|
1208
|
+
clearTimeout(statusTimer); })
|
|
1209
|
+
: await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
1122
1210
|
// Status converts transport errors into displayable state. A deadline is
|
|
1123
1211
|
// still a timeout, not evidence that this container became incompatible.
|
|
1124
1212
|
if (budgetExpired) {
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// mcp-bridge.ts, shared with the Local VM entry point.
|
|
6
6
|
import { runMcpBridge } from "./mcp-bridge.js";
|
|
7
7
|
import { vpsContainerMcpArgs, vpsDockerArgs } from "./vps-computer.js";
|
|
8
|
+
import { DATA_DIR } from "./config.js";
|
|
9
|
+
import { augmentedPath } from "./env-path.js";
|
|
10
|
+
import { prepareVpsSsh } from "./vps-ssh.js";
|
|
8
11
|
const [alias, containerName] = process.argv.slice(2);
|
|
9
12
|
const sshAlias = alias ?? "";
|
|
10
13
|
let args;
|
|
@@ -19,9 +22,13 @@ catch {
|
|
|
19
22
|
// through `ps`, and the token guards a loopback endpoint.
|
|
20
23
|
const controlUrl = process.env.OMB_CONTROL_URL ?? "";
|
|
21
24
|
const controlToken = process.env.OMB_CONTROL_TOKEN ?? "";
|
|
25
|
+
const ssh = prepareVpsSsh(DATA_DIR, augmentedPath());
|
|
22
26
|
runMcpBridge({
|
|
23
27
|
command: "docker",
|
|
24
28
|
args,
|
|
29
|
+
// Keep tool calls and the watchdog on the same bounded, shared transport
|
|
30
|
+
// as startup and previews; otherwise only the panel gets the SSH defaults.
|
|
31
|
+
env: { ...process.env, PATH: ssh.path },
|
|
25
32
|
label: "VPS Cua Driver",
|
|
26
33
|
// The probe checks the TRANSPORT (SSH + daemon), deliberately not the
|
|
27
34
|
// driver: a busy desktop mid-tool-call must never look dead, while an
|