openmausbot 0.1.90 → 0.1.92
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-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +128 -15
- package/dist-server/container-mcp.js +277 -45
- package/dist-server/drivers/agents-proxy.js +16 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +46703 -39955
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4099 -1217
- package/dist-server/pair-cli.js +4099 -1217
- package/dist-server/server/boat.js +67 -22
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +136 -26
- package/dist-server/server/cloud-home.js +64 -5
- package/dist-server/server/cloud-lending.js +167 -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-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/config.js +215 -17
- package/dist-server/server/container-computer.js +3 -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/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/agents-catalog.js +22 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +313 -34
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1753 -263
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- 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/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +25 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routes/desktop-viewer.js +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +79 -124
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +20 -11
- package/dist-server/server/vps-computer.js +79 -12
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +25 -3
- package/dist-server/server/workspace-backup.js +28 -8
- package/dist-server/server/workspace.js +65 -15
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +278 -45
- package/dist-server/workspace-backup.worker.js +184 -38
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -94,7 +94,9 @@ export function createTeamBackup(store, routines, name) {
|
|
|
94
94
|
key: group.id, name: group.name, section: group.section, dm: Boolean(group.dm) && memberIds.length === 2,
|
|
95
95
|
bulletin: group.bulletin, memberIds,
|
|
96
96
|
defaultResponder: group.defaultResponder.kind === "member" && !memberIds.includes(group.defaultResponder.botId)
|
|
97
|
-
? { kind: "mentions" }
|
|
97
|
+
? { kind: "mentions" }
|
|
98
|
+
: group.defaultResponder.kind === "auto" && group.defaultResponder.fallbackBotId && !memberIds.includes(group.defaultResponder.fallbackBotId)
|
|
99
|
+
? { kind: "auto" } : group.defaultResponder,
|
|
98
100
|
activeTask: group.threadId, tasks: history(group),
|
|
99
101
|
};
|
|
100
102
|
});
|
|
@@ -226,7 +228,9 @@ export function importTeamBackup(store, routines, input, selection, options = {}
|
|
|
226
228
|
groups.push(group);
|
|
227
229
|
groupIds.set(source.key, group.id);
|
|
228
230
|
const responder = source.defaultResponder;
|
|
229
|
-
store.patchGroup(group.id, { bulletin: source.bulletin, setupCompletedAt: Date.now(), defaultResponder: responder.kind === "member" ? { kind: "member", botId: botIds.get(responder.botId) }
|
|
231
|
+
store.patchGroup(group.id, { bulletin: source.bulletin, setupCompletedAt: Date.now(), defaultResponder: responder.kind === "member" ? { kind: "member", botId: botIds.get(responder.botId) }
|
|
232
|
+
: responder.kind === "auto" ? { kind: "auto", ...(responder.fallbackBotId ? { fallbackBotId: botIds.get(responder.fallbackBotId) } : {}) }
|
|
233
|
+
: responder });
|
|
230
234
|
// Use the existing task APIs for rooms; direct-message rooms have one.
|
|
231
235
|
const threads = source.tasks.map((task, i) => {
|
|
232
236
|
if (i === 0) {
|
|
@@ -25,11 +25,18 @@ const optionalText = (max) => z
|
|
|
25
25
|
.transform((value) => value?.trim() || undefined)
|
|
26
26
|
.refine((value) => value === undefined || value.length <= max, { message: "is too long" })
|
|
27
27
|
.optional();
|
|
28
|
-
const
|
|
28
|
+
const RESPONDER_KINDS = new Set(["member", "everyone", "mentions", "auto"]);
|
|
29
|
+
/** A responder kind from a newer release reads as "no responder": the room
|
|
30
|
+
* is created with the default lead, never refused. */
|
|
31
|
+
const responderSchema = z.preprocess((value) => {
|
|
32
|
+
const kind = value && typeof value === "object" ? value.kind : undefined;
|
|
33
|
+
return typeof kind === "string" && !RESPONDER_KINDS.has(kind) ? { kind: "mentions" } : value;
|
|
34
|
+
}, z.discriminatedUnion("kind", [
|
|
29
35
|
z.object({ kind: z.literal("member"), member: requiredText(64) }),
|
|
30
36
|
z.object({ kind: z.literal("everyone") }),
|
|
31
37
|
z.object({ kind: z.literal("mentions") }),
|
|
32
|
-
|
|
38
|
+
z.object({ kind: z.literal("auto"), member: requiredText(64).optional() }),
|
|
39
|
+
]));
|
|
33
40
|
const memberSchema = z.object({
|
|
34
41
|
key: requiredText(64).regex(/^[a-z0-9][a-z0-9_-]*$/, {
|
|
35
42
|
message: "may only contain lowercase letters, numbers, - and _",
|
|
@@ -117,7 +124,8 @@ export function parseTeamManifest(value) {
|
|
|
117
124
|
result.team.description = parsed.data.team.description;
|
|
118
125
|
if (parsed.data.version === LEGACY_TEAM_MANIFEST_VERSION) {
|
|
119
126
|
const defaultResponder = parsed.data.team.room.defaultResponder;
|
|
120
|
-
if (defaultResponder.kind === "member" &&
|
|
127
|
+
if ((defaultResponder.kind === "member" || defaultResponder.kind === "auto") && defaultResponder.member !== undefined &&
|
|
128
|
+
!seenKeys.has(defaultResponder.member)) {
|
|
121
129
|
throw new Error(`Unknown default responder: ${defaultResponder.member}`);
|
|
122
130
|
}
|
|
123
131
|
result.team.room = {
|
|
@@ -215,6 +215,8 @@ function isRuntimeEvent(value) {
|
|
|
215
215
|
value.source === "peer"));
|
|
216
216
|
case "thread.token-usage.updated":
|
|
217
217
|
return typeof value.input === "number" && typeof value.output === "number";
|
|
218
|
+
case "runtime.notice":
|
|
219
|
+
return typeof value.message === "string";
|
|
218
220
|
case "runtime.error":
|
|
219
221
|
return typeof value.message === "string" &&
|
|
220
222
|
(value.setup === undefined || typeof value.setup === "boolean") &&
|
|
@@ -35,6 +35,39 @@ export class ThreadStarters {
|
|
|
35
35
|
get(threadId) {
|
|
36
36
|
return this.starters.get(threadId);
|
|
37
37
|
}
|
|
38
|
+
/** The threads opened for one person, newest first, at most `limit`. */
|
|
39
|
+
threadsOf(person, limit) {
|
|
40
|
+
const threads = [];
|
|
41
|
+
for (const [threadId, starter] of [...this.starters].reverse()) {
|
|
42
|
+
if (starter !== person)
|
|
43
|
+
continue;
|
|
44
|
+
threads.push(threadId);
|
|
45
|
+
if (threads.length >= limit)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return threads;
|
|
49
|
+
}
|
|
50
|
+
/** Everyone a thread here was opened for. */
|
|
51
|
+
people() {
|
|
52
|
+
return new Set(this.starters.values());
|
|
53
|
+
}
|
|
54
|
+
/** Name `to` for every thread whose person `move` accepts: a one-time
|
|
55
|
+
* migration (server/cloud-owner.ts), not an ordinary record. The threads
|
|
56
|
+
* that changed. */
|
|
57
|
+
reassign(move, to) {
|
|
58
|
+
if (!KEY.test(to))
|
|
59
|
+
return [];
|
|
60
|
+
const moved = [];
|
|
61
|
+
for (const [threadId, person] of this.starters) {
|
|
62
|
+
if (person === to || !move(person))
|
|
63
|
+
continue;
|
|
64
|
+
this.starters.set(threadId, to);
|
|
65
|
+
moved.push(threadId);
|
|
66
|
+
}
|
|
67
|
+
if (moved.length)
|
|
68
|
+
writeFileAtomic(this.file, JSON.stringify(Object.fromEntries(this.starters)) + "\n", { mode: 0o600 });
|
|
69
|
+
return moved;
|
|
70
|
+
}
|
|
38
71
|
/** Record once; a thread keeps the person it was first opened for. */
|
|
39
72
|
set(threadId, person) {
|
|
40
73
|
if (!person || !KEY.test(person) || !THREAD.test(threadId) || this.starters.has(threadId))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// What a generated thread title is asked from.
|
|
2
|
+
//
|
|
3
|
+
// A fresh thread is named from its first message. "Regenerate title" (#1858)
|
|
4
|
+
// names it again from the conversation as it is now: the newest user and bot
|
|
5
|
+
// lines, never tool output, cards or screen frames, and bounded so the
|
|
6
|
+
// one-shot stays as cheap as the first-message one.
|
|
7
|
+
import { redactSecretsInText } from "./redact.js";
|
|
8
|
+
/** The most text either kind of title one-shot ever sees. */
|
|
9
|
+
export const TITLE_INPUT_MAX_CHARS = 1_500;
|
|
10
|
+
const EXCERPT_MAX_MESSAGES = 12;
|
|
11
|
+
const EXCERPT_LINE_MAX_CHARS = 300;
|
|
12
|
+
const ATTACHMENT_TAG = /<attached-(?:image|file)\b[^>]*\/>/g;
|
|
13
|
+
export function threadTitlePrompt(text, source = "first-message") {
|
|
14
|
+
return [
|
|
15
|
+
source === "conversation"
|
|
16
|
+
? "Name the conversation below by what it is about now."
|
|
17
|
+
: "Name the conversation that begins with the message below.",
|
|
18
|
+
"Reply with only a short title: 3 to 6 words, plain text, no quotes, no trailing period.",
|
|
19
|
+
source === "conversation" ? "Conversation:" : "Message:",
|
|
20
|
+
text.trim().slice(0, TITLE_INPUT_MAX_CHARS),
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
/** The newest user and bot text lines, oldest first, one per line and each
|
|
24
|
+
* clipped, until the next line would pass the input cap. Attachment markup
|
|
25
|
+
* and secrets are scrubbed before anything leaves this machine. Empty when
|
|
26
|
+
* the thread has nothing a title could be drawn from. */
|
|
27
|
+
export function titleConversationExcerpt(messages) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
let length = 0;
|
|
30
|
+
for (let index = messages.length - 1; index >= 0 && lines.length < EXCERPT_MAX_MESSAGES; index--) {
|
|
31
|
+
const message = messages[index];
|
|
32
|
+
// peer asides and still-queued lines are not the conversation yet
|
|
33
|
+
if (message.kind !== "text" || !message.text || message.aside || message.queued)
|
|
34
|
+
continue;
|
|
35
|
+
const text = redactSecretsInText(message.text.replace(ATTACHMENT_TAG, " ")).replace(/\s+/g, " ").trim();
|
|
36
|
+
if (!text)
|
|
37
|
+
continue;
|
|
38
|
+
const clipped = text.length > EXCERPT_LINE_MAX_CHARS ? `${text.slice(0, EXCERPT_LINE_MAX_CHARS - 1)}…` : text;
|
|
39
|
+
const line = `${message.role === "user" ? "User" : "Bot"}: ${clipped}`;
|
|
40
|
+
if (length + line.length + 1 > TITLE_INPUT_MAX_CHARS)
|
|
41
|
+
break;
|
|
42
|
+
lines.push(line);
|
|
43
|
+
length += line.length + 1;
|
|
44
|
+
}
|
|
45
|
+
return lines.reverse().join("\n");
|
|
46
|
+
}
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
// client already splits text into utterances and fetches the next while the
|
|
11
11
|
// current one plays, which gets the same perceived latency with far fewer
|
|
12
12
|
// moving parts — and no socket to leak when a turn is interrupted.
|
|
13
|
-
|
|
13
|
+
//
|
|
14
|
+
// The base URL is the caller's: ElevenLabs itself for the person's own key
|
|
15
|
+
// (OMB_ELEVENLABS_API overrides it for dev and tests), Cloud Pro's relay for
|
|
16
|
+
// its included token (included-services.ts). Only a key being saved is
|
|
17
|
+
// verified, and that is always an own key.
|
|
18
|
+
import { elevenLabsProviderApi } from "../included-services.js";
|
|
14
19
|
const MODEL = "eleven_flash_v2_5";
|
|
15
20
|
// 64kbps mono is indistinguishable for speech and a third of the bytes
|
|
16
21
|
const FORMAT = "mp3_44100_64";
|
|
@@ -51,7 +56,7 @@ function message(status, what, body) {
|
|
|
51
56
|
* picker needs next, so it tests exactly the capability that matters. */
|
|
52
57
|
export async function verifyKey(key) {
|
|
53
58
|
try {
|
|
54
|
-
const res = await fetch(`${
|
|
59
|
+
const res = await fetch(`${elevenLabsProviderApi()}/voices`, {
|
|
55
60
|
headers: { "xi-api-key": key },
|
|
56
61
|
signal: AbortSignal.timeout(20_000),
|
|
57
62
|
});
|
|
@@ -63,8 +68,8 @@ export async function verifyKey(key) {
|
|
|
63
68
|
return { ok: false, message: "Couldn't reach ElevenLabs to check that key — check your connection." };
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
|
-
export async function listVoices(key) {
|
|
67
|
-
const res = await fetch(`${
|
|
71
|
+
export async function listVoices(key, api) {
|
|
72
|
+
const res = await fetch(`${api}/voices`, {
|
|
68
73
|
headers: { "xi-api-key": key },
|
|
69
74
|
signal: AbortSignal.timeout(20_000),
|
|
70
75
|
});
|
|
@@ -79,8 +84,8 @@ export async function listVoices(key) {
|
|
|
79
84
|
}))
|
|
80
85
|
.filter((v) => v.id);
|
|
81
86
|
}
|
|
82
|
-
export async function synthesize(text, voiceId, key) {
|
|
83
|
-
const res = await fetch(`${
|
|
87
|
+
export async function synthesize(text, voiceId, key, api) {
|
|
88
|
+
const res = await fetch(`${api}/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${FORMAT}`, {
|
|
84
89
|
method: "POST",
|
|
85
90
|
headers: { "xi-api-key": key, "content-type": "application/json", accept: "audio/mpeg" },
|
|
86
91
|
body: JSON.stringify({ text, model_id: MODEL }),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Fish Audio text to speech. The API key stays on the harness: the renderer
|
|
2
|
-
// receives only voice metadata and synthesized audio bytes.
|
|
3
1
|
const API = (process.env.OMB_FISH_AUDIO_API || "https://api.fish.audio").replace(/\/+$/, "");
|
|
4
|
-
|
|
2
|
+
/** Used when no model is saved, so existing setups keep their behavior. */
|
|
3
|
+
export const DEFAULT_FISH_MODEL = "s2.1-pro";
|
|
5
4
|
const MAX_ERROR_LENGTH = 240;
|
|
6
5
|
const PAGE_SIZE = 100;
|
|
7
6
|
const MAX_OWNED_PAGES = 100;
|
|
@@ -124,7 +123,7 @@ export async function listVoices(key) {
|
|
|
124
123
|
});
|
|
125
124
|
return [...new Map(voices.map((voice) => [voice.id, voice])).values()];
|
|
126
125
|
}
|
|
127
|
-
export async function synthesize(text, voiceId, key) {
|
|
126
|
+
export async function synthesize(text, voiceId, key, model = DEFAULT_FISH_MODEL) {
|
|
128
127
|
let res;
|
|
129
128
|
try {
|
|
130
129
|
res = await fetch(`${API}/v1/tts`, {
|
|
@@ -133,7 +132,7 @@ export async function synthesize(text, voiceId, key) {
|
|
|
133
132
|
authorization: `Bearer ${key}`,
|
|
134
133
|
"content-type": "application/json",
|
|
135
134
|
accept: "audio/mpeg",
|
|
136
|
-
model
|
|
135
|
+
model,
|
|
137
136
|
},
|
|
138
137
|
body: JSON.stringify({
|
|
139
138
|
text,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { voiceCredential } from "../included-services.js";
|
|
1
2
|
import * as chatterbox from "./chatterbox.js";
|
|
2
3
|
import * as elevenlabs from "./elevenlabs.js";
|
|
3
4
|
import * as grok from "./grok.js";
|
|
@@ -16,6 +17,9 @@ export class NoVoiceConfigured extends Error {
|
|
|
16
17
|
this.reason = reason;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
/** The ElevenLabs credential in use: the person's own key, else the one
|
|
21
|
+
* included with Cloud Pro. Settings' own-key flows read cfg.tts.key instead. */
|
|
22
|
+
const elevenLabs = (cfg) => voiceCredential(cfg.tts?.key);
|
|
19
23
|
export function voiceProvider(cfg) {
|
|
20
24
|
const provider = cfg.tts?.provider;
|
|
21
25
|
return provider === "xai" || provider === "fish" || provider === "system" || provider === "chatterbox" ? provider : "elevenlabs";
|
|
@@ -33,7 +37,7 @@ export function providerConfigured(cfg) {
|
|
|
33
37
|
return Boolean(cfg.tts?.baseUrl?.trim());
|
|
34
38
|
if (provider === "fish")
|
|
35
39
|
return Boolean(cfg.tts?.fishKey);
|
|
36
|
-
return Boolean(cfg
|
|
40
|
+
return Boolean(elevenLabs(cfg));
|
|
37
41
|
}
|
|
38
42
|
export function voiceConfigured(cfg) {
|
|
39
43
|
const provider = voiceProvider(cfg);
|
|
@@ -46,7 +50,7 @@ export function voiceConfigured(cfg) {
|
|
|
46
50
|
return Boolean(cfg.tts?.baseUrl?.trim() && cfg.tts?.voice);
|
|
47
51
|
if (provider === "fish")
|
|
48
52
|
return Boolean(cfg.tts?.fishKey && cfg.tts?.voice);
|
|
49
|
-
return Boolean(cfg
|
|
53
|
+
return Boolean(elevenLabs(cfg) && cfg.tts?.voice);
|
|
50
54
|
}
|
|
51
55
|
/** A per-bot voice is a complete choice too; it should not be blocked just
|
|
52
56
|
* because the app-wide fallback has not been selected yet. */
|
|
@@ -61,13 +65,16 @@ export function voiceReady(cfg, voiceId) {
|
|
|
61
65
|
return Boolean(cfg.tts?.baseUrl?.trim() && (voiceId || cfg.tts?.voice));
|
|
62
66
|
if (provider === "fish")
|
|
63
67
|
return Boolean(cfg.tts?.fishKey && (voiceId || cfg.tts?.voice));
|
|
64
|
-
return Boolean(cfg
|
|
68
|
+
return Boolean(elevenLabs(cfg) && (voiceId || cfg.tts?.voice));
|
|
65
69
|
}
|
|
66
70
|
/** What the settings panel needs. Never includes the key — same write-only
|
|
67
71
|
* rule as every other credential. baseUrl and model are Chatterbox
|
|
68
|
-
* settings, not credentials, so they come back in full
|
|
72
|
+
* settings, not credentials, so they come back in full; `fishModel` is the
|
|
73
|
+
* Fish Audio speech model, resolved to its default when unset. `included`
|
|
74
|
+
* says the voice is Cloud Pro's, not a saved key. */
|
|
69
75
|
export function describeVoice(cfg) {
|
|
70
76
|
const provider = voiceProvider(cfg);
|
|
77
|
+
const included = provider === "elevenlabs" && elevenLabs(cfg)?.included === true;
|
|
71
78
|
return {
|
|
72
79
|
configured: providerConfigured(cfg),
|
|
73
80
|
ready: voiceConfigured(cfg),
|
|
@@ -75,6 +82,8 @@ export function describeVoice(cfg) {
|
|
|
75
82
|
provider,
|
|
76
83
|
baseUrl: provider === "chatterbox" ? (cfg.tts?.baseUrl ?? "") : "",
|
|
77
84
|
model: provider === "chatterbox" ? (cfg.tts?.model ?? "") : "",
|
|
85
|
+
...(provider === "fish" ? { fishModel: cfg.tts?.fishModel ?? fish.DEFAULT_FISH_MODEL } : {}),
|
|
86
|
+
...(included ? { included: true } : {}),
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
export function verifyKey(provider, key) {
|
|
@@ -94,10 +103,10 @@ export async function listVoices(cfg, run) {
|
|
|
94
103
|
const key = cfg.tts?.fishKey;
|
|
95
104
|
return key ? fish.listVoices(key) : [];
|
|
96
105
|
}
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
106
|
+
const credential = elevenLabs(cfg);
|
|
107
|
+
if (!credential)
|
|
99
108
|
return [];
|
|
100
|
-
return elevenlabs.listVoices(
|
|
109
|
+
return elevenlabs.listVoices(credential.token, credential.api);
|
|
101
110
|
}
|
|
102
111
|
/** Synthesize one utterance. Throws NoVoiceConfigured when there is nothing
|
|
103
112
|
* to speak with, which the route turns into a 409 the client can explain. */
|
|
@@ -140,13 +149,13 @@ export function speak(cfg, text, voiceId, run) {
|
|
|
140
149
|
const voice = voiceId || cfg.tts?.voice;
|
|
141
150
|
if (!voice)
|
|
142
151
|
throw new NoVoiceConfigured("voice");
|
|
143
|
-
return fish.synthesize(text, voice, key);
|
|
152
|
+
return fish.synthesize(text, voice, key, cfg.tts?.fishModel);
|
|
144
153
|
}
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
154
|
+
const credential = elevenLabs(cfg);
|
|
155
|
+
if (!credential)
|
|
147
156
|
throw new NoVoiceConfigured("key");
|
|
148
157
|
const voice = voiceId || cfg.tts?.voice;
|
|
149
158
|
if (!voice)
|
|
150
159
|
throw new NoVoiceConfigured("voice");
|
|
151
|
-
return elevenlabs.synthesize(text, voice,
|
|
160
|
+
return elevenlabs.synthesize(text, voice, credential.token, credential.api);
|
|
152
161
|
}
|
|
@@ -85,6 +85,8 @@ const SCREENSHOT_CLEANUP_BUDGET_MS = 10_000;
|
|
|
85
85
|
const PREPARATION_LOCK_TIMEOUT_MS = SCREENSHOT_BUDGET_MS + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
86
86
|
const pendingScreenshots = new Map();
|
|
87
87
|
const viewerConnections = new Map();
|
|
88
|
+
const REMOTE_VIEWER_GRACE_MS = 30_000;
|
|
89
|
+
const NATIVE_VIEWER_LIFETIME_MS = 8 * 60 * 60_000;
|
|
88
90
|
const desktopTunnels = new Map();
|
|
89
91
|
function invalidateVpsStatus(key) {
|
|
90
92
|
statusCache.delete(key);
|
|
@@ -198,12 +200,53 @@ function stopDesktopTunnel(botId) {
|
|
|
198
200
|
return true;
|
|
199
201
|
}
|
|
200
202
|
export function closeVpsDesktopTunnel(botId) {
|
|
203
|
+
const tunnel = desktopTunnels.get(botId);
|
|
204
|
+
// Remote tabs still hold the tunnel; drop only the native claim and let
|
|
205
|
+
// the last tab's release start the grace period.
|
|
206
|
+
if (tunnel?.viewers) {
|
|
207
|
+
tunnel.localViewer = false;
|
|
208
|
+
clearTimeout(tunnel.expiry);
|
|
209
|
+
return { closed: false };
|
|
210
|
+
}
|
|
201
211
|
return { closed: stopDesktopTunnel(botId) };
|
|
202
212
|
}
|
|
203
213
|
export function closeAllVpsDesktopTunnels() {
|
|
204
214
|
for (const botId of desktopTunnels.keys())
|
|
205
215
|
stopDesktopTunnel(botId);
|
|
206
216
|
}
|
|
217
|
+
/** Resolve only a tunnel opened by the authenticated join route. Multiple
|
|
218
|
+
* remote tabs share it; the final tab releases it unless a native viewer owns it. */
|
|
219
|
+
export function vpsDesktopConnection(botId) {
|
|
220
|
+
const tunnel = desktopTunnels.get(botId);
|
|
221
|
+
if (!tunnel || tunnel.child.exitCode !== null || tunnel.child.killed)
|
|
222
|
+
return;
|
|
223
|
+
const url = new URL(tunnel.joinUrl);
|
|
224
|
+
return {
|
|
225
|
+
port: Number(url.port),
|
|
226
|
+
password: new URLSearchParams(url.hash.slice(1)).get("password"),
|
|
227
|
+
live: () => desktopTunnels.get(botId) === tunnel && tunnel.child.exitCode === null && !tunnel.child.killed,
|
|
228
|
+
retain: () => {
|
|
229
|
+
tunnel.viewers++;
|
|
230
|
+
if (!tunnel.localViewer)
|
|
231
|
+
clearTimeout(tunnel.expiry);
|
|
232
|
+
let released = false;
|
|
233
|
+
return () => {
|
|
234
|
+
if (released)
|
|
235
|
+
return;
|
|
236
|
+
released = true;
|
|
237
|
+
tunnel.viewers--;
|
|
238
|
+
if (!tunnel.viewers && !tunnel.localViewer && desktopTunnels.get(botId) === tunnel) {
|
|
239
|
+
// A short grace period lets a reload/reconnect reuse the tunnel.
|
|
240
|
+
tunnel.expiry = setTimeout(() => {
|
|
241
|
+
if (desktopTunnels.get(botId) === tunnel)
|
|
242
|
+
stopDesktopTunnel(botId);
|
|
243
|
+
}, REMOTE_VIEWER_GRACE_MS);
|
|
244
|
+
tunnel.expiry.unref();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
207
250
|
const STREAM_CAP_CHARS = 16 * 1024 * 1024;
|
|
208
251
|
/** Keeps the LAST 16MB of a stream without rebuilding one giant string per
|
|
209
252
|
* chunk (a 10-minute `docker build` stream made that rebuild quadratic).
|
|
@@ -317,7 +360,7 @@ function emptyStatus(botId, alias) {
|
|
|
317
360
|
desktopReady: false,
|
|
318
361
|
desktop_error: null,
|
|
319
362
|
ready: false,
|
|
320
|
-
problem: alias ? "Docker over SSH is not reachable" : "Configure a VPS SSH alias in
|
|
363
|
+
problem: alias ? "Docker over SSH is not reachable" : "Configure a VPS SSH alias in Settings → API keys",
|
|
321
364
|
image_ref: VPS_IMAGE,
|
|
322
365
|
base_image_ref: BASE_IMAGE,
|
|
323
366
|
driver_version: CUA_DRIVER_VERSION,
|
|
@@ -335,6 +378,9 @@ function isMissingObjectMessage(message) {
|
|
|
335
378
|
return /no such (object|image|container)/i.test(message);
|
|
336
379
|
}
|
|
337
380
|
function transportFailure(message) {
|
|
381
|
+
if (/timed out/i.test(message)) {
|
|
382
|
+
return "Docker over SSH failed while checking the VPS: the connection timed out. Check that the VPS is online, SSH is reachable from this computer, and Docker is running on the VPS, then retry. No computer was reset.";
|
|
383
|
+
}
|
|
338
384
|
return `Docker over SSH failed while checking the VPS: ${message.trim().slice(0, 200) || "unknown transport error"}`;
|
|
339
385
|
}
|
|
340
386
|
function privateDockerIpv4(value) {
|
|
@@ -371,7 +417,7 @@ function hasNoPublishedPorts(config, networks) {
|
|
|
371
417
|
}
|
|
372
418
|
function statusProblem(status) {
|
|
373
419
|
if (!status.configured)
|
|
374
|
-
return "Configure a VPS SSH alias in
|
|
420
|
+
return "Configure a VPS SSH alias in Settings → API keys";
|
|
375
421
|
if (!status.daemonUp)
|
|
376
422
|
return "Docker over SSH could not reach the VPS; check the SSH alias and Docker on the VPS";
|
|
377
423
|
if (!status.image)
|
|
@@ -412,9 +458,23 @@ async function computeVpsComputerStatus(cfg, botId, runner) {
|
|
|
412
458
|
return status;
|
|
413
459
|
viewerConnections.delete(`${alias}:${status.container_name}`);
|
|
414
460
|
const run = (args, timeoutMs = 10_000, input) => runner(vpsDockerArgs(alias, args), { timeoutMs, input });
|
|
461
|
+
// Only repeat read-only Docker inspections, never an exec or lifecycle
|
|
462
|
+
// mutation whose outcome is unknown after a lost connection. Awaiting the
|
|
463
|
+
// runner also lets its owned SSH process finish cleanup before retrying.
|
|
464
|
+
const inspect = async (args) => {
|
|
465
|
+
try {
|
|
466
|
+
return await run(args);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
470
|
+
if (!/timed out|connection reset by peer|broken pipe/i.test(message))
|
|
471
|
+
throw error;
|
|
472
|
+
return run(args);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
415
475
|
let inspectedImageId = null;
|
|
416
476
|
try {
|
|
417
|
-
const inspected = JSON.parse((await
|
|
477
|
+
const inspected = JSON.parse((await inspect(["image", "inspect", VPS_IMAGE])).stdout);
|
|
418
478
|
status.daemonUp = true;
|
|
419
479
|
const image = inspected[0];
|
|
420
480
|
const labels = image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels;
|
|
@@ -434,7 +494,7 @@ async function computeVpsComputerStatus(cfg, botId, runner) {
|
|
|
434
494
|
status.image = false;
|
|
435
495
|
}
|
|
436
496
|
try {
|
|
437
|
-
const inspected = JSON.parse((await
|
|
497
|
+
const inspected = JSON.parse((await inspect(["inspect", status.container_name])).stdout);
|
|
438
498
|
const detail = inspected[0];
|
|
439
499
|
const labels = detail?.Config?.Labels;
|
|
440
500
|
const containerId = detail?.Id ?? detail?.id;
|
|
@@ -897,7 +957,7 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
897
957
|
cfg = snapshotVpsConfig(cfg);
|
|
898
958
|
const alias = vpsSshAlias(cfg);
|
|
899
959
|
if (!alias) {
|
|
900
|
-
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in
|
|
960
|
+
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in Settings → API keys"), { status: 409 });
|
|
901
961
|
}
|
|
902
962
|
if (!MANAGED_VPS_CONTAINER_NAME.test(containerName)) {
|
|
903
963
|
throw Object.assign(new Error("invalid managed VPS computer name"), { status: 400 });
|
|
@@ -948,7 +1008,7 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
948
1008
|
cfg = snapshotVpsConfig(cfg);
|
|
949
1009
|
const alias = vpsSshAlias(cfg);
|
|
950
1010
|
if (!alias)
|
|
951
|
-
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in
|
|
1011
|
+
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in Settings → API keys"), { status: 409 });
|
|
952
1012
|
const key = `${alias}:${vpsContainerName(botId)}`;
|
|
953
1013
|
const pending = action === "provision" ? pendingProvisions.get(key) : undefined;
|
|
954
1014
|
if (pending)
|
|
@@ -1054,13 +1114,20 @@ export async function inspectVpsForAuto(cfg, botId, runner = defaultRunner) {
|
|
|
1054
1114
|
* returned URL is loopback-only and contains the per-container password in
|
|
1055
1115
|
* its fragment (never sent in HTTP). Closing the app viewer calls the paired
|
|
1056
1116
|
* close endpoint, and process shutdown closes every remaining child. */
|
|
1057
|
-
export async function vpsComputerJoin(cfg, botId, runner = defaultRunner) {
|
|
1117
|
+
export async function vpsComputerJoin(cfg, botId, runner = defaultRunner, remoteViewer = false) {
|
|
1058
1118
|
cfg = snapshotVpsConfig(cfg);
|
|
1059
1119
|
const alias = vpsSshAlias(cfg);
|
|
1060
1120
|
if (!alias)
|
|
1061
1121
|
throw Object.assign(new Error("VPS is not configured"), { status: 409 });
|
|
1062
1122
|
const existing = desktopTunnels.get(botId);
|
|
1063
1123
|
if (existing && existing.child.exitCode === null && !existing.child.killed) {
|
|
1124
|
+
if (!remoteViewer)
|
|
1125
|
+
existing.localViewer = true;
|
|
1126
|
+
if (!existing.viewers || existing.localViewer) {
|
|
1127
|
+
clearTimeout(existing.expiry);
|
|
1128
|
+
existing.expiry = setTimeout(() => stopDesktopTunnel(botId), existing.localViewer ? NATIVE_VIEWER_LIFETIME_MS : REMOTE_VIEWER_GRACE_MS);
|
|
1129
|
+
existing.expiry.unref();
|
|
1130
|
+
}
|
|
1064
1131
|
return { joinUrl: existing.joinUrl, state: "running" };
|
|
1065
1132
|
}
|
|
1066
1133
|
stopDesktopTunnel(botId);
|
|
@@ -1114,11 +1181,11 @@ export async function vpsComputerJoin(cfg, botId, runner = defaultRunner) {
|
|
|
1114
1181
|
throw Object.assign(new Error(`Could not open the VPS live desktop: ${detail}`), { status: 502 });
|
|
1115
1182
|
}
|
|
1116
1183
|
const joinUrl = `http://127.0.0.1:${localPort}/vnc.html#autoconnect=true&resize=scale&password=${encodeURIComponent(connection.password)}`;
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
const expiry = setTimeout(() => stopDesktopTunnel(botId),
|
|
1184
|
+
// Remote tabs retain the tunnel when their WebSocket opens. Reclaim an
|
|
1185
|
+
// abandoned join promptly; native windows retain the existing close/ceiling.
|
|
1186
|
+
const expiry = setTimeout(() => stopDesktopTunnel(botId), remoteViewer ? REMOTE_VIEWER_GRACE_MS : NATIVE_VIEWER_LIFETIME_MS);
|
|
1120
1187
|
expiry.unref?.();
|
|
1121
|
-
desktopTunnels.set(botId, { child, joinUrl, expiry });
|
|
1188
|
+
desktopTunnels.set(botId, { child, joinUrl, expiry, viewers: 0, localViewer: !remoteViewer });
|
|
1122
1189
|
return { joinUrl, state: "running" };
|
|
1123
1190
|
}
|
|
1124
1191
|
export function vpsContainerMcpArgs(alias, containerName) {
|
|
@@ -1142,7 +1209,7 @@ export function vpsDriverError(driverKind, computerMcp) {
|
|
|
1142
1209
|
return "The Computer engine runs its agent on Boat and cannot use a self-hosted VPS — choose Claude or an ACP engine";
|
|
1143
1210
|
}
|
|
1144
1211
|
if (!computerMcp) {
|
|
1145
|
-
return "This model
|
|
1212
|
+
return "This model cannot mount a self-hosted VPS computer — choose Claude or an ACP model provider";
|
|
1146
1213
|
}
|
|
1147
1214
|
return null;
|
|
1148
1215
|
}
|
|
@@ -51,6 +51,10 @@ const storedWebhookSchema = z.object({
|
|
|
51
51
|
updatedAt: z.number().finite().nonnegative(),
|
|
52
52
|
lastReceivedAt: z.number().finite().nonnegative().optional(),
|
|
53
53
|
lastRunId: z.string().optional(),
|
|
54
|
+
/** delivery:"post" only: the stable destination thread, resolved once
|
|
55
|
+
* via WebhookManagerOptions.resolvePostThread and reused forever
|
|
56
|
+
* after -- see the `post`/`resolvePostThread` doc comments above. */
|
|
57
|
+
resultsThreadId: z.string().min(1).optional(),
|
|
54
58
|
deliveryCount: z.number().int().nonnegative(),
|
|
55
59
|
verificationPending: z.boolean().optional(),
|
|
56
60
|
verifiedAt: z.number().finite().nonnegative().optional(),
|
|
@@ -429,9 +433,22 @@ export class WebhookManager {
|
|
|
429
433
|
// without a post sink is a configuration error, not a run request.
|
|
430
434
|
if (!this.options.post)
|
|
431
435
|
fail(503, "This server cannot post webhook messages to chat");
|
|
436
|
+
// Resolved on every dispatch, exactly like routines.ts's newRun calls
|
|
437
|
+
// resolveResultsThread on every run -- the resolver owns the
|
|
438
|
+
// reuse-vs-allocate decision from trigger.resultsThreadId, so a host
|
|
439
|
+
// without resolvePostThread wired can only fail closed here, never
|
|
440
|
+
// silently fall back to whatever thread the bot happens to have
|
|
441
|
+
// selected right now (the bug this replaces -- issue #2071).
|
|
442
|
+
const threadId = this.options.resolvePostThread?.(trigger, false);
|
|
443
|
+
if (!threadId)
|
|
444
|
+
fail(503, "This server could not resolve a destination thread for this delivery");
|
|
445
|
+
if (threadId !== trigger.resultsThreadId) {
|
|
446
|
+
trigger.resultsThreadId = threadId;
|
|
447
|
+
trigger.updatedAt = now;
|
|
448
|
+
}
|
|
432
449
|
const raw = event.payload;
|
|
433
450
|
const text = raw && typeof raw === "object" && typeof raw.text === "string" && raw.text.trim() ? raw.text : serializePayload(event.payload);
|
|
434
|
-
this.options.post(trigger.botId, text.slice(0, 20000));
|
|
451
|
+
this.options.post(trigger.botId, threadId, text.slice(0, 20000));
|
|
435
452
|
this.deliveries.push({ key: `${trigger.endpointId}:${deliveryId}`, runId: "post", at: now });
|
|
436
453
|
if (this.deliveries.length > MAX_DELIVERIES)
|
|
437
454
|
this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
|
|
@@ -12,19 +12,38 @@ export function portableWorkspaceConfig(value) {
|
|
|
12
12
|
const config = parseStoredConfig(value);
|
|
13
13
|
return Object.fromEntries(PORTABLE_CONFIG_KEYS.filter((key) => config[key] !== undefined).map((key) => [key, config[key]]));
|
|
14
14
|
}
|
|
15
|
+
// Switches that belong to the machine even inside a portable section.
|
|
16
|
+
// Computer sharing (lending a computer to this server) opens routes and a
|
|
17
|
+
// desktop connector here, so a restored workspace never turns it on or off.
|
|
18
|
+
const DESTINATION_FEATURES = ["sharedComputers"];
|
|
15
19
|
export function restoredWorkspaceConfig(portable, destination) {
|
|
16
20
|
if (!destination || typeof destination !== "object" || Array.isArray(destination))
|
|
17
21
|
throw new Error("Invalid destination workspace configuration; connections were not changed.");
|
|
18
22
|
const merged = { ...destination };
|
|
19
23
|
for (const key of PORTABLE_CONFIG_KEYS)
|
|
20
24
|
delete merged[key];
|
|
21
|
-
|
|
25
|
+
const restored = { ...merged, ...portableWorkspaceConfig(portable) };
|
|
26
|
+
const own = destination.features ?? {};
|
|
27
|
+
const features = { ...restored.features };
|
|
28
|
+
for (const key of DESTINATION_FEATURES) {
|
|
29
|
+
delete features[key];
|
|
30
|
+
if (own[key] !== undefined)
|
|
31
|
+
features[key] = own[key];
|
|
32
|
+
}
|
|
33
|
+
if (Object.keys(features).length)
|
|
34
|
+
restored.features = features;
|
|
35
|
+
else
|
|
36
|
+
delete restored.features;
|
|
37
|
+
return restored;
|
|
22
38
|
}
|
|
23
39
|
/** Exact app-owned authentication paths, not a scan of user document text. */
|
|
24
40
|
export function excludedWorkspaceAuthPath(path) {
|
|
25
41
|
// Machine/provider-specific execution grants are not portable template data.
|
|
26
42
|
if (/^command-allowlist\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
|
|
27
43
|
return true;
|
|
44
|
+
// MCP sign-in tokens belong to this machine's browser sign-ins.
|
|
45
|
+
if (/^mcp-oauth\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
|
|
46
|
+
return true;
|
|
28
47
|
return /^(?:(?:providers|caddy|chrome-profile|\.agent-browser)(?:\/|$)|workspace-credentials\.json$|external-runtimes\.json$|browser-engine-key$)/.test(path) ||
|
|
29
48
|
/^(?:config\.json|webhooks\.json|workspace-credentials\.json|external-runtimes\.json|browser-engine-key|sessions\.json|tunnel-account\.json)\.\d+(?:\.[0-9a-f-]+)?\.tmp$/.test(path) ||
|
|
30
49
|
/^(?:vm-home|vm-homes\/[^/]+)\/\.browser-profiles(?:\/|$)/.test(path);
|
|
@@ -41,7 +60,10 @@ export function redownloadedOrgLibraryPath(path) {
|
|
|
41
60
|
/** Per-turn engine hook bearers (`hook-tokens/<digest>.token`, written by the
|
|
42
61
|
* Claude driver). They are dead once their turn settles, so they are never
|
|
43
62
|
* exported. Unlike the saved auth paths above they are not refused on import:
|
|
44
|
-
* v0.1.85 did export them, and such a backup must still restore, without them.
|
|
63
|
+
* v0.1.85 did export them, and such a backup must still restore, without them.
|
|
64
|
+
* The same holds for the session registry's open marker: it describes the
|
|
65
|
+
* process that wrote it, so it is never exported or installed, and a restore
|
|
66
|
+
* leaves the destination's own marker where it is (workspace-backup.ts). */
|
|
45
67
|
export function ephemeralWorkspaceTokenPath(path) {
|
|
46
|
-
return /^hook-tokens(?:\/|$)/.test(path);
|
|
68
|
+
return /^(?:hook-tokens(?:\/|$)|sessions\.json\.open$)/.test(path);
|
|
47
69
|
}
|