openmausbot 0.1.89 → 0.1.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-q73qM-At.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
- package/dist/assets/channel-BCuyHjFV.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
- package/dist/assets/index-CRvo-DS2.css +1 -0
- package/dist/assets/index-CWYGlA63.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-G1PHKGyh.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-Dwc3Hrru.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-Dc4OePBL.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14795 -0
- package/dist-server/container-mcp.js +52 -4
- package/dist-server/drivers/agents-proxy.js +20 -10
- package/dist-server/index.js +8985 -6461
- package/dist-server/openmausbot.js +327 -145
- package/dist-server/pair-cli.js +327 -145
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +330 -328
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +103 -0
- package/dist-server/server/cloud-home.js +218 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +115 -6
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +9 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +13 -12
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +177 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/index.js +1029 -479
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +25 -1
- package/dist-server/server/surface.js +1 -1
- package/dist-server/server/system-prompt.js +5 -5
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +1 -1
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +52 -4
- package/dist-server/workspace-backup.worker.js +52 -4
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { BOT_AVATAR_CROPS, botAvatarCropSchema, botAvatarUrlSchema } from "../shared/bot-avatar.js";
|
|
2
|
+
import { AVATAR_ZOOM_MAX, AVATAR_ZOOM_MIN, BOT_AVATAR_CROPS, botAvatarCropSchema, botAvatarUrlSchema, } from "../shared/bot-avatar.js";
|
|
3
3
|
import { BOT_PROFILE_LIMITS, fitsOnOneLine } from "../shared/bot-profile.js";
|
|
4
4
|
import { MASCOT_BODY_IDS, mascotBodySchema } from "../shared/mascot-bodies.js";
|
|
5
5
|
// Shared with the package format, which checks the same rule on names it
|
|
@@ -13,6 +13,9 @@ export const BOT_PROFILE_PATCH_FIELDS = [
|
|
|
13
13
|
"notifications",
|
|
14
14
|
"avatarUrl",
|
|
15
15
|
"avatarCrop",
|
|
16
|
+
"avatarZoom",
|
|
17
|
+
"avatarFocusX",
|
|
18
|
+
"avatarFocusY",
|
|
16
19
|
"mascotBody",
|
|
17
20
|
"voice",
|
|
18
21
|
"speakReplies",
|
|
@@ -46,6 +49,9 @@ export const profilePatchSchema = z.object({
|
|
|
46
49
|
})
|
|
47
50
|
.optional(),
|
|
48
51
|
avatarCrop: botAvatarCropSchema.optional(),
|
|
52
|
+
avatarZoom: z.number({ error: "avatarZoom must be a number from 1 to 3" }).finite().min(AVATAR_ZOOM_MIN).max(AVATAR_ZOOM_MAX).optional(),
|
|
53
|
+
avatarFocusX: z.number({ error: "avatarFocusX must be a number from 0 to 1" }).finite().min(0).max(1).optional(),
|
|
54
|
+
avatarFocusY: z.number({ error: "avatarFocusY must be a number from 0 to 1" }).finite().min(0).max(1).optional(),
|
|
49
55
|
mascotBody: mascotBodySchema.optional(),
|
|
50
56
|
voice: z
|
|
51
57
|
.string({ error: "voice must be a string" })
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// never saw.
|
|
8
8
|
import { newId } from "./contracts.js";
|
|
9
9
|
import { chatFollowups, saveChatFollowup, settleChatFollowups } from "./message-db.js";
|
|
10
|
+
import { drainCoalesceHead, DRAIN_COALESCE_MAX_ITEMS } from "./admission.js";
|
|
10
11
|
const queues = new Map(); // threadId -> waiting sends
|
|
11
12
|
export function restoreChannelMessages() {
|
|
12
13
|
queues.clear();
|
|
@@ -16,7 +17,14 @@ export function restoreChannelMessages() {
|
|
|
16
17
|
const entry = queues.get(row.threadId) ?? { groupId: row.ownerId, items: [] };
|
|
17
18
|
if (entry.groupId !== row.ownerId)
|
|
18
19
|
throw new Error("queued task belongs to another channel");
|
|
19
|
-
entry.items.push({
|
|
20
|
+
entry.items.push({
|
|
21
|
+
...row.payload,
|
|
22
|
+
id: row.id,
|
|
23
|
+
mode: row.payload.mode ?? "chat",
|
|
24
|
+
// rows queued before timestamps were kept read as queued at restore
|
|
25
|
+
// time: the burst was still live when the restart interrupted it
|
|
26
|
+
queuedAt: row.payload.queuedAt ?? Date.now(),
|
|
27
|
+
});
|
|
20
28
|
queues.set(row.threadId, entry);
|
|
21
29
|
}
|
|
22
30
|
}
|
|
@@ -33,6 +41,7 @@ export function queueChannelMessage(groupId, threadId, text, options = {}) {
|
|
|
33
41
|
via: options.via,
|
|
34
42
|
sender: options.sender,
|
|
35
43
|
trigger: options.trigger,
|
|
44
|
+
queuedAt: Date.now(),
|
|
36
45
|
};
|
|
37
46
|
saveChatFollowup({ id: item.id, kind: "channel", ownerId: groupId, threadId, payload: item });
|
|
38
47
|
entry.items.push(item);
|
|
@@ -66,10 +75,11 @@ export function cancelChannelMessage(groupId, queueId) {
|
|
|
66
75
|
/** Atomically lift a channel thread's whole queue out for a live steer. The
|
|
67
76
|
* entry leaves first so a room that settles while the adapter is still
|
|
68
77
|
* thinking can never also drain the same words as a follow-up turn. Only
|
|
69
|
-
* the HEAD can be lifted
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
78
|
+
* the HEAD can be lifted (the chip the UI offers Steer on): the success
|
|
79
|
+
* path steers and settles the head GROUP — the leading coalesced run — as
|
|
80
|
+
* one, so a request naming a later item must not lift the queue at all (it
|
|
81
|
+
* would steer and delete words the requester did not name). The caller must
|
|
82
|
+
* either restore the held queue or settle its head group. */
|
|
73
83
|
export function holdChannelQueue(groupId, threadId, queueId) {
|
|
74
84
|
const entry = queues.get(threadId);
|
|
75
85
|
if (!entry || entry.groupId !== groupId || entry.items[0]?.id !== queueId)
|
|
@@ -106,11 +116,13 @@ export function resolveHeldReplyTarget(held, resolve) {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
/** Mark a held queue's head delivered — its words were folded into the
|
|
109
|
-
* running turn —
|
|
110
|
-
*
|
|
119
|
+
* running turn — the whole head group steers as one — and re-queue the
|
|
120
|
+
* rest for the room's one-coalesced-item-per-turn drain. A restart must
|
|
121
|
+
* not replay the steered words as a fresh follow-up. */
|
|
111
122
|
export function settleHeldChannelQueueHead(held) {
|
|
112
|
-
const
|
|
113
|
-
|
|
123
|
+
const head = headChannelGroup(held.items);
|
|
124
|
+
const rest = held.items.slice(head.length);
|
|
125
|
+
settleChatFollowups(head.map((item) => item.id), null);
|
|
114
126
|
if (rest.length === 0)
|
|
115
127
|
return;
|
|
116
128
|
const existing = queues.get(held.threadId);
|
|
@@ -122,27 +134,55 @@ export function settleHeldChannelQueueHead(held) {
|
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
/**
|
|
125
|
-
* Start at most one follow-up per idle channel
|
|
126
|
-
*
|
|
127
|
-
*
|
|
137
|
+
* Start at most one follow-up per idle channel: the leading coalesced
|
|
138
|
+
* group — one sender's contiguous in-window burst — runs as ONE item.
|
|
139
|
+
* Starting it synchronously marks the channel working again; its completion
|
|
140
|
+
* calls this drain for the next group. Removing first makes repeated settle
|
|
141
|
+
* notifications harmless.
|
|
128
142
|
*/
|
|
129
143
|
export function drainChannelMessages(isWorking, run) {
|
|
130
144
|
for (const [threadId, entry] of queues) {
|
|
131
145
|
if (isWorking(entry.groupId))
|
|
132
146
|
continue;
|
|
133
|
-
|
|
134
|
-
if (!item) {
|
|
147
|
+
if (entry.items.length === 0) {
|
|
135
148
|
queues.delete(threadId);
|
|
136
149
|
continue;
|
|
137
150
|
}
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
const group = headChannelGroup(entry.items);
|
|
152
|
+
const ids = group.map((item) => item.id);
|
|
153
|
+
settleChatFollowups(ids, "dispatching");
|
|
154
|
+
entry.items.splice(0, group.length);
|
|
140
155
|
if (entry.items.length === 0)
|
|
141
156
|
queues.delete(threadId);
|
|
142
|
-
const running = run({
|
|
143
|
-
void Promise.resolve(running).then(() => settleChatFollowups(
|
|
157
|
+
const running = run({ groupId: entry.groupId, threadId, items: group });
|
|
158
|
+
void Promise.resolve(running).then(() => settleChatFollowups(ids, null), () => settleChatFollowups(ids, "interrupted")).catch((error) => console.warn("channel-queue: could not settle durable follow-up", error));
|
|
144
159
|
}
|
|
145
160
|
}
|
|
161
|
+
/** The leading run of queued room messages that drain as one item: same
|
|
162
|
+
* sender, same provenance shape, each within the coalescing window of the
|
|
163
|
+
* one before it, bounded at the room-context window — a room turn reads
|
|
164
|
+
* the burst through that window, so a longer run would append lines the
|
|
165
|
+
* responder never sees; the excess stays queued for the next turn. Manual
|
|
166
|
+
* head-steer folds and settles exactly this group into the live turn. */
|
|
167
|
+
export function headChannelGroup(items) {
|
|
168
|
+
return drainCoalesceHead(items, coalesceIdentity, (item) => item.queuedAt, DRAIN_COALESCE_MAX_ITEMS);
|
|
169
|
+
}
|
|
170
|
+
/** A queued room message's coalescing identity: WHO sent it, with the
|
|
171
|
+
* provenance shape as part of the identity. A person's texts merge only
|
|
172
|
+
* with that same person's; an API send has no person behind it, so it never
|
|
173
|
+
* merges with anything, and neither does a goal (its coordinator routing
|
|
174
|
+
* is per-send). Unattributed local sends (the loopback owner typing in the
|
|
175
|
+
* room composer) are one identity — the transcript already names them all
|
|
176
|
+
* the same. */
|
|
177
|
+
function coalesceIdentity(item) {
|
|
178
|
+
if (item.via === "api")
|
|
179
|
+
return `api:${item.id}`;
|
|
180
|
+
if (item.mode !== "chat")
|
|
181
|
+
return `mode:${item.mode}:${item.id}`;
|
|
182
|
+
if (item.sender)
|
|
183
|
+
return `person:${item.sender.id ?? item.sender.name}`;
|
|
184
|
+
return "person:local";
|
|
185
|
+
}
|
|
146
186
|
/** Test helper. */
|
|
147
187
|
export function _queuedChannelCount(threadId) {
|
|
148
188
|
return queues.get(threadId)?.items.length ?? 0;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Restore is redo-friendly on purpose: it commits a safety point, then moves
|
|
12
12
|
// the index and work tree back with `git restore --source` + `git clean -fd`
|
|
13
|
-
// while HEAD
|
|
14
|
-
// checkpoint, and "undo the undo" is one more restore. Ignored/excluded files
|
|
13
|
+
// while HEAD keeps the safety point — so the state just replaced is the one
|
|
14
|
+
// retained checkpoint, and "undo the undo" is one more restore. Ignored/excluded files
|
|
15
15
|
// (node_modules, .env, media) are never snapshotted and never removed by one.
|
|
16
16
|
//
|
|
17
17
|
// Failure policy: checkpointing is best-effort convenience, never load-bearing.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// service, and the snapshot-before-every-turn cadence follows Cline.
|
|
26
26
|
import { execFile } from "node:child_process";
|
|
27
27
|
import { createHash } from "node:crypto";
|
|
28
|
-
import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { isAbsolute, join, parse, resolve } from "node:path";
|
|
31
31
|
import { DATA_DIR } from "./config.js";
|
|
@@ -119,8 +119,9 @@ Thumbs.db
|
|
|
119
119
|
*.gguf
|
|
120
120
|
`;
|
|
121
121
|
// gpgsign off: the user's global config may demand signing, and a shadow
|
|
122
|
-
// commit must never block on a passphrase prompt.
|
|
123
|
-
//
|
|
122
|
+
// commit must never block on a passphrase prompt. Automatic GC stays off:
|
|
123
|
+
// explicit GC runs inside the same serialization as snapshot/restore, after
|
|
124
|
+
// their source objects are no longer needed.
|
|
124
125
|
const GITCONFIG = "[commit]\n\tgpgsign = false\n[core]\n\tautocrlf = false\n[gc]\n\tauto = 0\n";
|
|
125
126
|
/** One failed git call disables checkpoints for that bot until restart —
|
|
126
127
|
* a broken shadow repo must cost the user one log line, not a failed turn. */
|
|
@@ -261,6 +262,28 @@ async function ensureShadow(cwd, env, shadow, signal) {
|
|
|
261
262
|
}
|
|
262
263
|
mkdirSync(join(shadow, ".git", "info"), { recursive: true });
|
|
263
264
|
writeFileSync(join(shadow, ".git", "info", "exclude"), EXCLUDES);
|
|
265
|
+
// Cargo and other tools can put caches outside conventional target/ or
|
|
266
|
+
// build/ paths. Honor the standard tag even for previously indexed caches.
|
|
267
|
+
const tags = (await runGit(["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "**/CACHEDIR.TAG"], cwd, env, signal)).split("\0");
|
|
268
|
+
const cachePatterns = new Set();
|
|
269
|
+
for (const tag of tags) {
|
|
270
|
+
if (!tag.includes("/") || /[\r\n]/.test(tag))
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const path = join(cwd, tag);
|
|
274
|
+
const stat = lstatSync(path);
|
|
275
|
+
if (!stat.isFile() || stat.size > 4096)
|
|
276
|
+
continue;
|
|
277
|
+
if (!readFileSync(path, "utf8").startsWith("Signature: 8a477f597d28d172789f06886806bc55"))
|
|
278
|
+
continue;
|
|
279
|
+
const directory = tag.slice(0, -"CACHEDIR.TAG".length);
|
|
280
|
+
cachePatterns.add("/" + directory.replace(/[\\*?[\]#! ]/g, "\\$&"));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// A disappearing/unreadable tag is not evidence that a path is a cache.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
writeFileSync(join(shadow, ".git", "info", "exclude"), EXCLUDES + [...cachePatterns].join("\n") + "\n");
|
|
264
287
|
try {
|
|
265
288
|
await runGit(["rev-parse", "--verify", "HEAD"], cwd, env, signal);
|
|
266
289
|
}
|
|
@@ -269,23 +292,43 @@ async function ensureShadow(cwd, env, shadow, signal) {
|
|
|
269
292
|
await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env, signal);
|
|
270
293
|
}
|
|
271
294
|
}
|
|
272
|
-
/**
|
|
273
|
-
*
|
|
274
|
-
* useful subset, but restore must not delete files its safety point missed. */
|
|
295
|
+
/** Capture fully before advancing HEAD. A partial add must never replace
|
|
296
|
+
* the last usable checkpoint or authorize removal of its objects. */
|
|
275
297
|
async function commitAll(cwd, env, label, signal) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
const previous = (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim();
|
|
299
|
+
try {
|
|
300
|
+
// Rebuild only the shadow index so newly ignored/cache-tagged files stop
|
|
301
|
+
// pinning old blobs; no user index or work-tree file is changed.
|
|
302
|
+
await runGit(["read-tree", "--empty"], cwd, env, signal);
|
|
303
|
+
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return { hash: previous, complete: false };
|
|
307
|
+
}
|
|
308
|
+
const changed = await hasStagedChanges(cwd, env, signal);
|
|
309
|
+
const base = (await runGit(["rev-list", "--max-parents=0", "HEAD"], cwd, env, signal)).trim();
|
|
310
|
+
const count = (await runGit(["rev-list", "--count", "HEAD"], cwd, env, signal)).trim();
|
|
311
|
+
if (!changed && Number(count) <= 2)
|
|
312
|
+
return { hash: previous, complete: true };
|
|
313
|
+
const tree = (await runGit(["write-tree"], cwd, env, signal)).trim();
|
|
314
|
+
const message = changed ? label : (await runGit(["show", "-s", "--format=%B", "HEAD"], cwd, env, signal)).trim();
|
|
315
|
+
// Parent only the empty marker, never the previous snapshot: keeping the
|
|
316
|
+
// previous commit as an ancestor would retain every old tree indefinitely.
|
|
317
|
+
const hash = (await runGit(["commit-tree", tree, "-p", base, "-m", message], cwd, env, signal)).trim();
|
|
318
|
+
await runGit(["update-ref", "HEAD", hash, previous], cwd, env, signal);
|
|
319
|
+
return { hash, complete: true };
|
|
320
|
+
}
|
|
321
|
+
async function collectObsolete(cwd, env) {
|
|
322
|
+
// No caller may still need the previous tree. In particular, restore runs
|
|
323
|
+
// this only AFTER using its source; the safety checkpoint stays reachable.
|
|
324
|
+
try {
|
|
325
|
+
await runGit(["reflog", "expire", "--expire=now", "--all"], cwd, env);
|
|
326
|
+
await runGit(["gc", "--prune=now", "--quiet"], cwd, env);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// A busy/full disk must not hide an otherwise usable restore point.
|
|
330
|
+
console.warn("checkpoint cleanup deferred; the latest restore point is preserved");
|
|
287
331
|
}
|
|
288
|
-
return { hash: (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim(), complete };
|
|
289
332
|
}
|
|
290
333
|
/** Snapshot the folder. Returns the checkpoint hash, or null when the
|
|
291
334
|
* feature is off for this bot, git is missing, or the folder is refused.
|
|
@@ -305,7 +348,11 @@ export async function snapshot(botId, cwd, label, signal) {
|
|
|
305
348
|
return await serialize(shadow, async () => {
|
|
306
349
|
const env = gitEnv(shadow, worktree);
|
|
307
350
|
await ensureShadow(worktree, env, shadow, signal);
|
|
308
|
-
|
|
351
|
+
const result = await commitAll(worktree, env, label, signal);
|
|
352
|
+
if (!result.complete)
|
|
353
|
+
return null;
|
|
354
|
+
await collectObsolete(worktree, env);
|
|
355
|
+
return result.hash;
|
|
309
356
|
});
|
|
310
357
|
}
|
|
311
358
|
catch (e) {
|
|
@@ -314,45 +361,42 @@ export async function snapshot(botId, cwd, label, signal) {
|
|
|
314
361
|
return null;
|
|
315
362
|
}
|
|
316
363
|
}
|
|
317
|
-
/**
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
export async function diffStat(botId, cwd, fromHash, toHash, signal) {
|
|
322
|
-
if (signal?.aborted)
|
|
364
|
+
/** Diff the settled workspace without replacing its pre-turn restore point.
|
|
365
|
+
* Staging is confined to the shadow index; the user's files and Git are untouched. */
|
|
366
|
+
export async function diffWorkingTree(botId, cwd, fromHash, signal) {
|
|
367
|
+
if (signal?.aborted || !COMMIT_HASH.test(fromHash))
|
|
323
368
|
return null;
|
|
324
|
-
if (
|
|
325
|
-
return null;
|
|
326
|
-
if (disabledBots.has(botId))
|
|
327
|
-
return null;
|
|
328
|
-
if (!(await gitAvailable()))
|
|
329
|
-
return null;
|
|
330
|
-
if (refusalReason(cwd) !== null)
|
|
369
|
+
if (disabledBots.has(botId) || !(await gitAvailable()) || refusalReason(cwd) !== null)
|
|
331
370
|
return null;
|
|
332
371
|
try {
|
|
333
372
|
const worktree = realpathSync(resolve(cwd));
|
|
334
373
|
const shadow = shadowDir(botId, worktree);
|
|
335
374
|
const env = gitEnv(shadow, worktree);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
375
|
+
return await serialize(shadow, async () => {
|
|
376
|
+
await ensureShadow(worktree, env, shadow, signal);
|
|
377
|
+
await runGit(["read-tree", "--empty"], worktree, env, signal);
|
|
378
|
+
await runGit(["add", "-A", "--ignore-errors", "."], worktree, env, signal);
|
|
379
|
+
const out = await runGit(["diff", "--cached", "--name-status", "--no-renames", "-z", fromHash], worktree, env, signal);
|
|
380
|
+
const diff = { changed: [], added: [], deleted: [] };
|
|
381
|
+
const fields = out.split("\0").filter((field) => field.length > 0);
|
|
382
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
383
|
+
const status = fields[i];
|
|
384
|
+
const path = fields[i + 1];
|
|
385
|
+
if (status.startsWith("A"))
|
|
386
|
+
diff.added.push(path);
|
|
387
|
+
else if (status.startsWith("D"))
|
|
388
|
+
diff.deleted.push(path);
|
|
389
|
+
else
|
|
390
|
+
diff.changed.push(path);
|
|
391
|
+
}
|
|
392
|
+
return diff;
|
|
393
|
+
});
|
|
350
394
|
}
|
|
351
395
|
catch {
|
|
352
396
|
return null;
|
|
353
397
|
}
|
|
354
398
|
}
|
|
355
|
-
/**
|
|
399
|
+
/** The latest usable checkpoint (plus legacy history until the next snapshot). Empty when nothing
|
|
356
400
|
* was ever snapshotted (listing never creates the shadow repo). The empty
|
|
357
401
|
* base marker is omitted — it is not a state anyone should return to. */
|
|
358
402
|
export async function listCheckpoints(botId, cwd) {
|
|
@@ -422,6 +466,7 @@ export async function restore(botId, cwd, hash) {
|
|
|
422
466
|
if (base === hash)
|
|
423
467
|
return { ok: false, error: "that is the empty base marker, not a checkpoint" };
|
|
424
468
|
// safety point: whatever is about to be overwritten becomes restorable
|
|
469
|
+
await ensureShadow(worktree, env, shadow);
|
|
425
470
|
const safety = await commitAll(worktree, env, "before restore");
|
|
426
471
|
if (!safety.complete) {
|
|
427
472
|
return {
|
|
@@ -439,9 +484,10 @@ export async function restore(botId, cwd, hash) {
|
|
|
439
484
|
// (unreadable files, add races) — never ignored/excluded files (no -x).
|
|
440
485
|
await runGit(["restore", "--source", hash, "--staged", "--worktree", "--", "."], worktree, env);
|
|
441
486
|
await runGit(["clean", "-fd"], worktree, env);
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
await
|
|
487
|
+
// Retain the pre-restore safety point, not another copy of the state
|
|
488
|
+
// now on disk. Reset only the shadow index so it cannot pin old blobs.
|
|
489
|
+
await runGit(["read-tree", "HEAD"], worktree, env);
|
|
490
|
+
await collectObsolete(worktree, env);
|
|
445
491
|
return { ok: true };
|
|
446
492
|
});
|
|
447
493
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Quiet-window policy for releasing a held seat while its turn lives.
|
|
2
|
+
*
|
|
3
|
+
* The policy is deliberately synchronous and clock-injected: like the
|
|
4
|
+
* claim map it serves, expiry is evaluated when a claim is touched
|
|
5
|
+
* (claim, blocker, owns) rather than by a timer, so the server's single
|
|
6
|
+
* thread decides every transition and tests pin time exactly. Slice
|
|
7
|
+
* #1655 reuses this window math for its own idle release; keep this
|
|
8
|
+
* module free of server imports. */
|
|
9
|
+
/** Default quiet window: no screen activity for this long releases the seat. */
|
|
10
|
+
export const COMPUTER_CLAIM_QUIET_MS = 90_000;
|
|
11
|
+
/** Default reclaim window: the previous holder re-claims directly for this long. */
|
|
12
|
+
export const COMPUTER_CLAIM_RECLAIM_MS = 10 * 60_000;
|
|
13
|
+
export class IdleReleasePolicy {
|
|
14
|
+
quietMs;
|
|
15
|
+
reclaimMs;
|
|
16
|
+
constructor(quietMs = COMPUTER_CLAIM_QUIET_MS, reclaimMs = COMPUTER_CLAIM_RECLAIM_MS) {
|
|
17
|
+
if (!Number.isFinite(quietMs) || quietMs <= 0)
|
|
18
|
+
throw new Error("Idle release quiet window must be positive");
|
|
19
|
+
if (!Number.isFinite(reclaimMs) || reclaimMs <= 0)
|
|
20
|
+
throw new Error("Idle release reclaim window must be positive");
|
|
21
|
+
this.quietMs = quietMs;
|
|
22
|
+
this.reclaimMs = reclaimMs;
|
|
23
|
+
}
|
|
24
|
+
/** True once the seat has been quiet for the full window. Screen-poller
|
|
25
|
+
* frames never refresh the activity clock, so they cannot keep a seat. */
|
|
26
|
+
quietElapsed(lastActivityAt, now) {
|
|
27
|
+
return now - lastActivityAt >= this.quietMs;
|
|
28
|
+
}
|
|
29
|
+
/** True while a released seat's previous holder still re-claims directly. */
|
|
30
|
+
reclaimLive(releasedAt, now) {
|
|
31
|
+
return now < releasedAt + this.reclaimMs;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CLOUD_BACKEND_CHANGE_ERROR = "stop the active turn before changing the cloud backend";
|
|
2
2
|
export const VPS_ALIAS_CHANGE_ERROR = "stop the active VPS turn before changing the SSH config alias";
|
|
3
|
-
export const
|
|
3
|
+
export const BOAT_ACCOUNT_RESOURCES_ERROR = "remove this installation's cloud computers before changing or clearing the Boat account";
|
|
4
4
|
export const VPS_ALIAS_RESOURCES_ERROR = "remove this installation's VPS computers before changing or clearing the SSH config alias";
|
|
5
5
|
export function cloudBackendChangeError(botBusy, activeVpsThread) {
|
|
6
6
|
return botBusy || activeVpsThread ? CLOUD_BACKEND_CHANGE_ERROR : null;
|
|
@@ -8,14 +8,14 @@ export function cloudBackendChangeError(botBusy, activeVpsThread) {
|
|
|
8
8
|
export function vpsAliasChangeError(currentAlias, nextAlias, activeVpsThread) {
|
|
9
9
|
return activeVpsThread && currentAlias !== nextAlias ? VPS_ALIAS_CHANGE_ERROR : null;
|
|
10
10
|
}
|
|
11
|
-
/** An old
|
|
11
|
+
/** An old Boat account may be detached only when it owns no local resources.
|
|
12
12
|
* Token rotation is safe when the replacement credential proves access to
|
|
13
13
|
* the exact same provider identities. */
|
|
14
|
-
export function
|
|
14
|
+
export function boatAccountResourceChangeError(current, replacement) {
|
|
15
15
|
if (current.length === 0)
|
|
16
16
|
return null;
|
|
17
17
|
if (!replacement || replacement.length !== current.length)
|
|
18
|
-
return
|
|
18
|
+
return BOAT_ACCOUNT_RESOURCES_ERROR;
|
|
19
19
|
const identities = (rows) => rows
|
|
20
20
|
.map(({ boxId, name }) => `${boxId}\u0000${name}`)
|
|
21
21
|
.sort();
|
|
@@ -23,7 +23,7 @@ export function boxAccountResourceChangeError(current, replacement) {
|
|
|
23
23
|
const replacementIdentities = identities(replacement);
|
|
24
24
|
return currentIdentities.every((identity, index) => identity === replacementIdentities[index])
|
|
25
25
|
? null
|
|
26
|
-
:
|
|
26
|
+
: BOAT_ACCOUNT_RESOURCES_ERROR;
|
|
27
27
|
}
|
|
28
28
|
export function vpsAliasResourceChangeError(instanceCount) {
|
|
29
29
|
return instanceCount > 0 ? VPS_ALIAS_RESOURCES_ERROR : null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Entry point of the OMB Cloud Pro home image (deploy/fly/Dockerfile).
|
|
2
|
+
//
|
|
3
|
+
// Starts as root only to hand a fresh Fly volume (mounted root-owned at
|
|
4
|
+
// /data) to the unprivileged `maus` user, then drops privileges for good
|
|
5
|
+
// and runs two children: the OpenMausBot server on 127.0.0.1:8799 (and its
|
|
6
|
+
// webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080. The edge is
|
|
7
|
+
// the only listener the network can reach, and it always forwards with
|
|
8
|
+
// X-Forwarded-*, so request-auth.ts never grants a remote request loopback
|
|
9
|
+
// trust. If either child exits, both stop and the machine restarts.
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
import { cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
|
|
15
|
+
const SERVICE_USER = "maus";
|
|
16
|
+
/** uid/gid from /etc/passwd; Node has no getpwnam. */
|
|
17
|
+
export function passwdIds(passwd, name) {
|
|
18
|
+
for (const line of passwd.split("\n")) {
|
|
19
|
+
const [user, , uid, gid] = line.split(":");
|
|
20
|
+
if (user === name && /^\d+$/.test(uid ?? "") && /^\d+$/.test(gid ?? ""))
|
|
21
|
+
return { uid: Number(uid), gid: Number(gid) };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** The server child's environment: the operator's contract plus fixed
|
|
26
|
+
* ports and paths (Linux paths inside the image, so POSIX joins on every
|
|
27
|
+
* host that builds them, tests included), never a platform gateway's settings. The edge child gets
|
|
28
|
+
* only what it needs to route. */
|
|
29
|
+
export function cloudHomeChildEnvironments(config, env, home) {
|
|
30
|
+
const server = {
|
|
31
|
+
...withoutIgnoredCloudKeys(env), HOME: home, OMB_DATA_DIR: env.OMB_DATA_DIR || posix.join(home, ".openmausbot"),
|
|
32
|
+
OMB_PORT: "8799", OMB_WEBHOOK_PORT: "8800", OMB_PUBLIC_URL: config.publicOrigin,
|
|
33
|
+
OMB_WEBHOOK_PUBLIC_URL: env.OMB_WEBHOOK_PUBLIC_URL || config.publicOrigin,
|
|
34
|
+
};
|
|
35
|
+
const edge = {
|
|
36
|
+
PATH: env.PATH ?? "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp/omb-edge",
|
|
37
|
+
XDG_DATA_HOME: "/tmp/omb-edge/data", XDG_CONFIG_HOME: "/tmp/omb-edge/config",
|
|
38
|
+
OMB_CLOUD_PUBLIC_HOST: cloudHomeHost(config),
|
|
39
|
+
};
|
|
40
|
+
return { server, edge };
|
|
41
|
+
}
|
|
42
|
+
export function startCloudHome(env = process.env) {
|
|
43
|
+
process.umask(0o077);
|
|
44
|
+
const config = cloudHomeConfiguration(env);
|
|
45
|
+
if (!config)
|
|
46
|
+
throw new Error("This image runs an OMB Cloud home machine; set its boot contract (docs/cloud-pro.md).");
|
|
47
|
+
// Logged here once: the server child never sees what they are about.
|
|
48
|
+
for (const warning of config.warnings)
|
|
49
|
+
console.warn(`cloud home: ${warning}`);
|
|
50
|
+
const home = env.HOME || "/data";
|
|
51
|
+
if (process.getuid?.() === 0) {
|
|
52
|
+
const ids = passwdIds(readFileSync("/etc/passwd", "utf8"), SERVICE_USER);
|
|
53
|
+
if (!ids)
|
|
54
|
+
throw new Error(`The ${SERVICE_USER} user is missing from this image.`);
|
|
55
|
+
// A new volume is a root-owned mount point. Only the mount point itself
|
|
56
|
+
// changes owner; anything inside keeps the owner it already has.
|
|
57
|
+
const stat = statSync(home);
|
|
58
|
+
if (stat.uid !== ids.uid || stat.gid !== ids.gid)
|
|
59
|
+
chownSync(home, ids.uid, ids.gid);
|
|
60
|
+
process.setgroups?.([]);
|
|
61
|
+
process.setgid(ids.gid);
|
|
62
|
+
process.setuid(ids.uid);
|
|
63
|
+
}
|
|
64
|
+
if (process.getuid?.() === 0)
|
|
65
|
+
throw new Error("The Cloud home must run as an unprivileged user.");
|
|
66
|
+
prepareCloudHomeVolume(home, config.machineId);
|
|
67
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const { server, edge } = cloudHomeChildEnvironments(config, env, home);
|
|
69
|
+
const children = [];
|
|
70
|
+
let stopping = false;
|
|
71
|
+
const stop = (failed) => {
|
|
72
|
+
if (stopping)
|
|
73
|
+
return;
|
|
74
|
+
stopping = true;
|
|
75
|
+
process.exitCode = failed ? 1 : 0;
|
|
76
|
+
for (const child of children)
|
|
77
|
+
if (child.exitCode === null)
|
|
78
|
+
child.kill("SIGTERM");
|
|
79
|
+
const force = setTimeout(() => { for (const child of children)
|
|
80
|
+
if (child.exitCode === null)
|
|
81
|
+
child.kill("SIGKILL"); }, 20_000);
|
|
82
|
+
force.unref();
|
|
83
|
+
};
|
|
84
|
+
const run = (command, args, childEnv) => {
|
|
85
|
+
const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
|
|
86
|
+
children.push(child);
|
|
87
|
+
child.once("error", () => stop(true));
|
|
88
|
+
child.once("exit", () => stop(true));
|
|
89
|
+
};
|
|
90
|
+
run(process.execPath, [join(here, "index.js")], server);
|
|
91
|
+
run(env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy", ["run", "--config", env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile", "--adapter", "caddyfile"], edge);
|
|
92
|
+
process.once("SIGTERM", () => stop(false));
|
|
93
|
+
process.once("SIGINT", () => stop(false));
|
|
94
|
+
}
|
|
95
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
96
|
+
try {
|
|
97
|
+
startCloudHome();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(`Cloud home startup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|