openmausbot 0.1.94 → 0.1.96
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-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
- package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
- package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
- package/dist/assets/index-Du7o6Dsf.js +610 -0
- package/dist/assets/index-KxG4bWSr.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
- package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
- package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +27 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/companion/src/routes.js +39 -0
- package/dist-server/container-mcp.js +16 -29
- package/dist-server/drivers/agents-proxy.js +161 -82
- package/dist-server/enterprise/server/index.js +2 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
- package/dist-server/index.js +4991 -4908
- package/dist-server/local-computer.js +2 -1
- package/dist-server/openmausbot.js +906 -1785
- package/dist-server/pair-cli.js +906 -1785
- package/dist-server/proxy-paths.js +2 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +25 -4
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/boat.js +4 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-runtime.js +1 -2
- package/dist-server/server/checkpoints.js +90 -148
- package/dist-server/server/chief-of-staff.js +9 -9
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home-start.js +12 -15
- package/dist-server/server/cloud-home.js +0 -5
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/composio.js +41 -5
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/config.js +121 -65
- package/dist-server/server/container-computer.js +23 -24
- package/dist-server/server/control-client.js +4 -10
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/delegations.js +3 -2
- package/dist-server/server/drivers/acp/core.js +11 -8
- package/dist-server/server/drivers/acp/opencode-go.js +17 -16
- package/dist-server/server/drivers/agents-call.js +88 -92
- package/dist-server/server/drivers/agents-catalog.js +90 -11
- package/dist-server/server/drivers/agents-proxy.js +10 -31
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +23 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +16 -14
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +5 -4
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +18 -11
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +1190 -971
- package/dist-server/server/lending-memory.js +12 -8
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +9 -13
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +5 -19
- package/dist-server/server/peer-roster.js +30 -4
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +17 -13
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +10 -5
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +0 -15
- package/dist-server/server/store.js +10 -2
- package/dist-server/server/surface.js +69 -28
- package/dist-server/server/system-prompt.js +17 -7
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +2 -2
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/vps-computer.js +4 -14
- package/dist-server/server/workspace.js +87 -50
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/ask-question.js +1 -1
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/cloud-computer.js +12 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/failed-turn.js +25 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/vps-container-mcp.js +16 -29
- package/dist-server/workspace-backup.worker.js +14 -27
- package/enterprise/server/index.js +2 -1
- package/package.json +1 -1
- package/dist/assets/channel-CdJX6i8k.js +0 -1
- package/dist/assets/index-CPYrGsv1.js +0 -610
- package/dist/assets/index-CzT42GR8.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Before a turn's engine can touch files, the working folder is snapshotted
|
|
4
4
|
// into a shadow repo at DATA_DIR/checkpoints/<botId>/<sha256(cwd)[..16]>/.git.
|
|
5
|
+
// When the turn settles, its digest diffs the folder against that snapshot to
|
|
6
|
+
// list the files the turn changed, added and deleted.
|
|
5
7
|
// The user's own .git (if the folder is a repository) is never read, written,
|
|
6
8
|
// or locked: every git call runs with GIT_DIR pointing at the shadow repo and
|
|
7
9
|
// GIT_WORK_TREE pointing at the folder, so the shadow index is the only index
|
|
8
10
|
// involved — and git always skips directories named .git when walking a work
|
|
9
11
|
// tree, so the user's repository internals are invisible to the snapshot.
|
|
10
12
|
//
|
|
11
|
-
//
|
|
12
|
-
// the index and work tree back with `git restore --source` + `git clean -fd`
|
|
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
|
-
// (node_modules, .env, media) are never snapshotted and never removed by one.
|
|
13
|
+
// Ignored/excluded files (node_modules, .env, media) are never snapshotted.
|
|
16
14
|
//
|
|
17
15
|
// Failure policy: checkpointing is best-effort convenience, never load-bearing.
|
|
18
16
|
// Any git failure disables the feature for that bot for the rest of the
|
|
@@ -20,9 +18,9 @@
|
|
|
20
18
|
//
|
|
21
19
|
// Adapted from the checkpoint designs of Cline, Roo-Code, and Gemini CLI
|
|
22
20
|
// (all Apache-2.0): the per-call GIT_DIR/GIT_WORK_TREE/GIT_CONFIG_* env
|
|
23
|
-
// override
|
|
24
|
-
//
|
|
25
|
-
//
|
|
21
|
+
// override follows Gemini CLI's gitService, the sanitized GIT_* env list and
|
|
22
|
+
// the exclude categories follow Roo-Code's checkpoint service, and the
|
|
23
|
+
// snapshot-before-every-turn cadence follows Cline.
|
|
26
24
|
import { execFile } from "node:child_process";
|
|
27
25
|
import { createHash } from "node:crypto";
|
|
28
26
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -30,15 +28,14 @@ import { homedir } from "node:os";
|
|
|
30
28
|
import { isAbsolute, join, parse, resolve } from "node:path";
|
|
31
29
|
import { DATA_DIR } from "./config.js";
|
|
32
30
|
export const CHECKPOINTS_DIR = join(DATA_DIR, "checkpoints");
|
|
33
|
-
/** Full sha1 hex only
|
|
34
|
-
*
|
|
31
|
+
/** Full sha1 hex only: accepting anything looser would let arbitrary
|
|
32
|
+
* revspecs ("HEAD~3", "main@{u}") reach git. */
|
|
35
33
|
const COMMIT_HASH = /^[0-9a-f]{40}$/;
|
|
36
|
-
// What a checkpoint deliberately does not carry
|
|
37
|
-
// these either
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const EXCLUDES = `# OpenMausBot checkpoint excludes — never snapshotted, never removed by restore
|
|
34
|
+
// What a checkpoint deliberately does not carry, so a digest never lists
|
|
35
|
+
// these either. Categories follow Roo-Code's checkpoint excludes: VCS
|
|
36
|
+
// internals, dependency trees, build output, caches, logs, secrets, media,
|
|
37
|
+
// archives, databases, model weights.
|
|
38
|
+
const EXCLUDES = `# OpenMausBot checkpoint excludes — never snapshotted
|
|
42
39
|
.git/
|
|
43
40
|
.svn/
|
|
44
41
|
.hg/
|
|
@@ -120,8 +117,8 @@ Thumbs.db
|
|
|
120
117
|
`;
|
|
121
118
|
// gpgsign off: the user's global config may demand signing, and a shadow
|
|
122
119
|
// commit must never block on a passphrase prompt. Automatic GC stays off:
|
|
123
|
-
// explicit GC runs inside the same serialization as snapshot
|
|
124
|
-
//
|
|
120
|
+
// explicit GC runs inside the same serialization as each snapshot, after the
|
|
121
|
+
// previous snapshot's objects are no longer needed.
|
|
125
122
|
const GITCONFIG = "[commit]\n\tgpgsign = false\n[core]\n\tautocrlf = false\n[gc]\n\tauto = 0\n";
|
|
126
123
|
/** One failed git call disables checkpoints for that bot until restart —
|
|
127
124
|
* a broken shadow repo must cost the user one log line, not a failed turn. */
|
|
@@ -140,8 +137,7 @@ function gitAvailable() {
|
|
|
140
137
|
}
|
|
141
138
|
/** Folders a checkpoint must never be taken in: missing paths, the sprawling
|
|
142
139
|
* personal folders (home, Desktop, Documents, Downloads), and the filesystem
|
|
143
|
-
* root — snapshotting those would trawl unbounded personal data into a repo
|
|
144
|
-
* and a restore's `git clean -fd` there would be an act of vandalism. */
|
|
140
|
+
* root — snapshotting those would trawl unbounded personal data into a repo. */
|
|
145
141
|
export function refusalReason(cwd) {
|
|
146
142
|
if (!isAbsolute(cwd))
|
|
147
143
|
return "the working folder must be an absolute path";
|
|
@@ -161,7 +157,7 @@ export function refusalReason(cwd) {
|
|
|
161
157
|
// the refusal while git still follows the symlink into the protected tree.
|
|
162
158
|
// The native realpath also settles Windows' spellings of one folder —
|
|
163
159
|
// c:\users\me, C:\Users\me\DOCUME~1 — and names compare case-insensitively
|
|
164
|
-
// there
|
|
160
|
+
// there.
|
|
165
161
|
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
166
162
|
if (dir === parse(dir).root)
|
|
167
163
|
return "checkpoints are not taken at the filesystem root";
|
|
@@ -203,9 +199,9 @@ function gitEnv(shadow, cwd) {
|
|
|
203
199
|
return env;
|
|
204
200
|
}
|
|
205
201
|
/** Run one git command against the shadow repo. cwd is the WORK TREE — the
|
|
206
|
-
* "." pathspec in add
|
|
207
|
-
*
|
|
208
|
-
*
|
|
202
|
+
* "." pathspec in add resolves relative to it. A hung git (index lock, dead
|
|
203
|
+
* network filesystem) would otherwise jam the per-repo queue for the whole
|
|
204
|
+
* session, so every call carries a hard timeout. */
|
|
209
205
|
function runGit(args, cwd, env, signal) {
|
|
210
206
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
211
207
|
signal?.throwIfAborted();
|
|
@@ -234,10 +230,10 @@ function hasStagedChanges(cwd, env, signal) {
|
|
|
234
230
|
});
|
|
235
231
|
});
|
|
236
232
|
}
|
|
237
|
-
// One operation at a time per shadow repo: snapshots and
|
|
238
|
-
// same folder queue behind each other (git's index lock would
|
|
239
|
-
// anyway — this turns a crash into a wait). The stored tail
|
|
240
|
-
// one failed operation can't poison the queue.
|
|
233
|
+
// One operation at a time per shadow repo: snapshots, diffs and pin releases
|
|
234
|
+
// against the same folder queue behind each other (git's index lock would
|
|
235
|
+
// fail the loser anyway — this turns a crash into a wait). The stored tail
|
|
236
|
+
// never rejects, so one failed operation can't poison the queue.
|
|
241
237
|
const chains = new Map();
|
|
242
238
|
function serialize(key, fn) {
|
|
243
239
|
const tail = chains.get(key) ?? Promise.resolve();
|
|
@@ -245,12 +241,32 @@ function serialize(key, fn) {
|
|
|
245
241
|
chains.set(key, run.then(() => undefined, () => undefined));
|
|
246
242
|
return run;
|
|
247
243
|
}
|
|
244
|
+
// A ref per running turn. Every snapshot parents only the empty base and the
|
|
245
|
+
// cleanup after it prunes whatever HEAD no longer reaches — so when a bot's
|
|
246
|
+
// threads work side by side in one folder, a sibling's snapshot would discard
|
|
247
|
+
// the pre-turn commit a still-running turn diffs against at its end. A pin
|
|
248
|
+
// keeps that commit reachable until release(). The pin's name is hashed so
|
|
249
|
+
// any token is a legal ref name.
|
|
250
|
+
const LIVE_REFS = "refs/omb-live/";
|
|
251
|
+
function liveRef(pin) {
|
|
252
|
+
return LIVE_REFS + createHash("sha256").update(pin).digest("hex").slice(0, 16);
|
|
253
|
+
}
|
|
254
|
+
// Pins outlive the process that wrote them (a crash mid-turn), so the first
|
|
255
|
+
// use of each shadow in this process drops every pin left behind: no turn of
|
|
256
|
+
// ours can be live in a shadow this process has not touched yet.
|
|
257
|
+
const sweptShadows = new Set();
|
|
258
|
+
async function sweepLiveRefs(cwd, env, shadow, signal) {
|
|
259
|
+
if (sweptShadows.has(shadow))
|
|
260
|
+
return;
|
|
261
|
+
const stale = (await runGit(["for-each-ref", "--format=%(refname)", LIVE_REFS], cwd, env, signal)).split("\n").filter(Boolean);
|
|
262
|
+
for (const ref of stale)
|
|
263
|
+
await runGit(["update-ref", "-d", ref], cwd, env, signal);
|
|
264
|
+
sweptShadows.add(shadow);
|
|
265
|
+
}
|
|
248
266
|
/** Create the shadow repo on first use; self-heal its config files on every
|
|
249
267
|
* use (they are tiny, and rewriting them lets exclude-list updates reach
|
|
250
268
|
* shadows that already exist). The base commit is an EMPTY marker so HEAD
|
|
251
|
-
* always resolves
|
|
252
|
-
* target, because "restore to empty" on a user's own project folder would
|
|
253
|
-
* delete their files. */
|
|
269
|
+
* always resolves; every snapshot parents only it. */
|
|
254
270
|
async function ensureShadow(cwd, env, shadow, signal) {
|
|
255
271
|
signal?.throwIfAborted();
|
|
256
272
|
mkdirSync(shadow, { recursive: true, mode: 0o700 });
|
|
@@ -291,9 +307,11 @@ async function ensureShadow(cwd, env, shadow, signal) {
|
|
|
291
307
|
// brand-new repo (or a crash between init and first commit)
|
|
292
308
|
await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env, signal);
|
|
293
309
|
}
|
|
310
|
+
await sweepLiveRefs(cwd, env, shadow, signal);
|
|
294
311
|
}
|
|
295
|
-
/** Capture fully before advancing HEAD
|
|
296
|
-
* the last usable checkpoint or
|
|
312
|
+
/** Capture fully before advancing HEAD: null when the add was partial, so a
|
|
313
|
+
* partial capture never replaces the last usable checkpoint or authorizes
|
|
314
|
+
* removal of its objects. */
|
|
297
315
|
async function commitAll(cwd, env, label, signal) {
|
|
298
316
|
const previous = (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim();
|
|
299
317
|
try {
|
|
@@ -303,37 +321,40 @@ async function commitAll(cwd, env, label, signal) {
|
|
|
303
321
|
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal);
|
|
304
322
|
}
|
|
305
323
|
catch {
|
|
306
|
-
return
|
|
324
|
+
return null;
|
|
307
325
|
}
|
|
308
326
|
const changed = await hasStagedChanges(cwd, env, signal);
|
|
309
327
|
const base = (await runGit(["rev-list", "--max-parents=0", "HEAD"], cwd, env, signal)).trim();
|
|
310
328
|
const count = (await runGit(["rev-list", "--count", "HEAD"], cwd, env, signal)).trim();
|
|
311
329
|
if (!changed && Number(count) <= 2)
|
|
312
|
-
return
|
|
330
|
+
return previous;
|
|
313
331
|
const tree = (await runGit(["write-tree"], cwd, env, signal)).trim();
|
|
314
332
|
const message = changed ? label : (await runGit(["show", "-s", "--format=%B", "HEAD"], cwd, env, signal)).trim();
|
|
315
333
|
// Parent only the empty marker, never the previous snapshot: keeping the
|
|
316
334
|
// previous commit as an ancestor would retain every old tree indefinitely.
|
|
317
335
|
const hash = (await runGit(["commit-tree", tree, "-p", base, "-m", message], cwd, env, signal)).trim();
|
|
318
336
|
await runGit(["update-ref", "HEAD", hash, previous], cwd, env, signal);
|
|
319
|
-
return
|
|
337
|
+
return hash;
|
|
320
338
|
}
|
|
321
339
|
async function collectObsolete(cwd, env) {
|
|
322
|
-
// No caller may still need
|
|
323
|
-
//
|
|
340
|
+
// No caller may still need an unpinned previous tree: HEAD stays reachable,
|
|
341
|
+
// and so does every commit a live turn still pins.
|
|
324
342
|
try {
|
|
325
343
|
await runGit(["reflog", "expire", "--expire=now", "--all"], cwd, env);
|
|
326
344
|
await runGit(["gc", "--prune=now", "--quiet"], cwd, env);
|
|
327
345
|
}
|
|
328
346
|
catch {
|
|
329
|
-
// A busy/full disk must not hide an otherwise usable
|
|
330
|
-
console.warn("checkpoint cleanup deferred; the latest
|
|
347
|
+
// A busy/full disk must not hide an otherwise usable snapshot.
|
|
348
|
+
console.warn("checkpoint cleanup deferred; the latest snapshot is preserved");
|
|
331
349
|
}
|
|
332
350
|
}
|
|
333
351
|
/** Snapshot the folder. Returns the checkpoint hash, or null when the
|
|
334
|
-
* feature is off for this bot, git is missing,
|
|
335
|
-
*
|
|
336
|
-
|
|
352
|
+
* feature is off for this bot, git is missing, the folder is refused, or a
|
|
353
|
+
* file could not be read.
|
|
354
|
+
* With `pin`, the returned commit stays reachable through later snapshots of
|
|
355
|
+
* the same folder until release(pin) — for a turn that will diff against it
|
|
356
|
+
* when it ends. Never throws — this is called fire-and-forget on the turn path. */
|
|
357
|
+
export async function snapshot(botId, cwd, label, signal, opts) {
|
|
337
358
|
if (signal?.aborted)
|
|
338
359
|
return null;
|
|
339
360
|
if (disabledBots.has(botId))
|
|
@@ -348,11 +369,13 @@ export async function snapshot(botId, cwd, label, signal) {
|
|
|
348
369
|
return await serialize(shadow, async () => {
|
|
349
370
|
const env = gitEnv(shadow, worktree);
|
|
350
371
|
await ensureShadow(worktree, env, shadow, signal);
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
372
|
+
const hash = await commitAll(worktree, env, label, signal);
|
|
373
|
+
if (!hash)
|
|
353
374
|
return null;
|
|
375
|
+
if (opts?.pin)
|
|
376
|
+
await runGit(["update-ref", liveRef(opts.pin), hash], worktree, env, signal);
|
|
354
377
|
await collectObsolete(worktree, env);
|
|
355
|
-
return
|
|
378
|
+
return hash;
|
|
356
379
|
});
|
|
357
380
|
}
|
|
358
381
|
catch (e) {
|
|
@@ -361,7 +384,27 @@ export async function snapshot(botId, cwd, label, signal) {
|
|
|
361
384
|
return null;
|
|
362
385
|
}
|
|
363
386
|
}
|
|
364
|
-
/**
|
|
387
|
+
/** Let go of a pinned pre-turn commit: the next snapshot's cleanup may
|
|
388
|
+
* reclaim it. Queued behind the shadow's pending operations, so a digest's
|
|
389
|
+
* diff already in that queue still finds its commit. Best-effort like the
|
|
390
|
+
* rest of this module — a pin that cannot be dropped costs one log line and
|
|
391
|
+
* is swept on the next start. */
|
|
392
|
+
export async function release(botId, cwd, pin) {
|
|
393
|
+
if (!(await gitAvailable()))
|
|
394
|
+
return;
|
|
395
|
+
try {
|
|
396
|
+
const worktree = realpathSync(resolve(cwd));
|
|
397
|
+
const shadow = shadowDir(botId, worktree);
|
|
398
|
+
if (!existsSync(join(shadow, ".git", "HEAD")))
|
|
399
|
+
return;
|
|
400
|
+
const env = gitEnv(shadow, worktree);
|
|
401
|
+
await serialize(shadow, () => runGit(["update-ref", "-d", liveRef(pin)], worktree, env));
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
console.warn(`checkpoint pin not released for bot ${botId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** Diff the settled workspace without replacing its pre-turn snapshot.
|
|
365
408
|
* Staging is confined to the shadow index; the user's files and Git are untouched. */
|
|
366
409
|
export async function diffWorkingTree(botId, cwd, fromHash, signal) {
|
|
367
410
|
if (signal?.aborted || !COMMIT_HASH.test(fromHash))
|
|
@@ -396,104 +439,3 @@ export async function diffWorkingTree(botId, cwd, fromHash, signal) {
|
|
|
396
439
|
return null;
|
|
397
440
|
}
|
|
398
441
|
}
|
|
399
|
-
/** The latest usable checkpoint (plus legacy history until the next snapshot). Empty when nothing
|
|
400
|
-
* was ever snapshotted (listing never creates the shadow repo). The empty
|
|
401
|
-
* base marker is omitted — it is not a state anyone should return to. */
|
|
402
|
-
export async function listCheckpoints(botId, cwd) {
|
|
403
|
-
if (disabledBots.has(botId))
|
|
404
|
-
return [];
|
|
405
|
-
if (!(await gitAvailable()))
|
|
406
|
-
return [];
|
|
407
|
-
if (refusalReason(cwd) !== null)
|
|
408
|
-
return [];
|
|
409
|
-
try {
|
|
410
|
-
const worktree = realpathSync(resolve(cwd));
|
|
411
|
-
const shadow = shadowDir(botId, worktree);
|
|
412
|
-
if (!existsSync(join(shadow, ".git", "HEAD")))
|
|
413
|
-
return [];
|
|
414
|
-
return await serialize(shadow, async () => {
|
|
415
|
-
const env = gitEnv(shadow, worktree);
|
|
416
|
-
const out = await runGit(["log", "--format=%H%x09%ct%x09%s"], worktree, env);
|
|
417
|
-
const lines = out.split("\n").filter((line) => line.trim() !== "");
|
|
418
|
-
lines.pop(); // the root of the log is always the empty base marker
|
|
419
|
-
return lines.flatMap((line) => {
|
|
420
|
-
const [hash, seconds, ...subject] = line.split("\t");
|
|
421
|
-
if (!hash || !COMMIT_HASH.test(hash))
|
|
422
|
-
return [];
|
|
423
|
-
return [{ hash, at: Number(seconds) * 1000, label: subject.join("\t") }];
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
catch (e) {
|
|
428
|
-
disable(botId, e instanceof Error ? e.message : String(e));
|
|
429
|
-
return [];
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
/** Can this bot take/restore checkpoints in this folder right now? */
|
|
433
|
-
export async function checkpointsEnabled(botId, cwd) {
|
|
434
|
-
return !disabledBots.has(botId) && (await gitAvailable()) && refusalReason(cwd) === null;
|
|
435
|
-
}
|
|
436
|
-
/** Move the folder's files back to a checkpoint. The current state is
|
|
437
|
-
* committed first ("before restore"), so the restore itself shows up as a
|
|
438
|
-
* checkpoint and can be undone; HEAD never moves backwards, only forward
|
|
439
|
-
* over the "restored" commit. Excluded and gitignored files are untouched. */
|
|
440
|
-
export async function restore(botId, cwd, hash) {
|
|
441
|
-
if (disabledBots.has(botId)) {
|
|
442
|
-
return { ok: false, error: "checkpoints are disabled for this bot until the app restarts (an earlier snapshot failed — see the server log)" };
|
|
443
|
-
}
|
|
444
|
-
if (!(await gitAvailable()))
|
|
445
|
-
return { ok: false, error: "git is not installed on this machine" };
|
|
446
|
-
const reason = refusalReason(cwd);
|
|
447
|
-
if (reason !== null)
|
|
448
|
-
return { ok: false, error: reason };
|
|
449
|
-
if (!COMMIT_HASH.test(hash))
|
|
450
|
-
return { ok: false, error: "hash must be a full 40-character checkpoint hash" };
|
|
451
|
-
try {
|
|
452
|
-
const worktree = realpathSync(resolve(cwd));
|
|
453
|
-
const shadow = shadowDir(botId, worktree);
|
|
454
|
-
if (!existsSync(join(shadow, ".git", "HEAD"))) {
|
|
455
|
-
return { ok: false, error: "no checkpoints exist for this folder" };
|
|
456
|
-
}
|
|
457
|
-
return await serialize(shadow, async () => {
|
|
458
|
-
const env = gitEnv(shadow, worktree);
|
|
459
|
-
try {
|
|
460
|
-
await runGit(["cat-file", "-e", `${hash}^{commit}`], worktree, env);
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
return { ok: false, error: "no such checkpoint" };
|
|
464
|
-
}
|
|
465
|
-
const base = (await runGit(["rev-list", "--max-parents=0", "HEAD"], worktree, env)).trim();
|
|
466
|
-
if (base === hash)
|
|
467
|
-
return { ok: false, error: "that is the empty base marker, not a checkpoint" };
|
|
468
|
-
// safety point: whatever is about to be overwritten becomes restorable
|
|
469
|
-
await ensureShadow(worktree, env, shadow);
|
|
470
|
-
const safety = await commitAll(worktree, env, "before restore");
|
|
471
|
-
if (!safety.complete) {
|
|
472
|
-
return {
|
|
473
|
-
ok: false,
|
|
474
|
-
error: "restore stopped because some current files could not be added to the safety checkpoint",
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
// Index AND work tree move to the source; HEAD stays put. --staged
|
|
478
|
-
// matters: with a work-tree-only restore, a file that exists in the
|
|
479
|
-
// source but not in the index (deleted in a later checkpoint, now
|
|
480
|
-
// resurrected) would be untracked the moment restore recreates it —
|
|
481
|
-
// and the clean below would delete it right back. Restoring the index
|
|
482
|
-
// too makes clean blind to everything the checkpoint owns; what clean
|
|
483
|
-
// then sweeps is exactly the strays the safety commit could not stage
|
|
484
|
-
// (unreadable files, add races) — never ignored/excluded files (no -x).
|
|
485
|
-
await runGit(["restore", "--source", hash, "--staged", "--worktree", "--", "."], worktree, env);
|
|
486
|
-
await runGit(["clean", "-fd"], worktree, env);
|
|
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);
|
|
491
|
-
return { ok: true };
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
catch (e) {
|
|
495
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
496
|
-
disable(botId, message);
|
|
497
|
-
return { ok: false, error: `restore failed: ${message}` };
|
|
498
|
-
}
|
|
499
|
-
}
|
|
@@ -8,8 +8,10 @@ const ROSTER_MAX_BOTS = 40;
|
|
|
8
8
|
const sectionKey = (section) => section?.trim() || "";
|
|
9
9
|
/** Dynamic system context for a section's Chief of Staff.
|
|
10
10
|
* It names the current team on every turn, while list_bots remains the
|
|
11
|
-
* authoritative tool for IDs and live availability at delegation time.
|
|
12
|
-
|
|
11
|
+
* authoritative tool for IDs and live availability at delegation time.
|
|
12
|
+
* Who is busy right now is not part of it: that changes turn to turn, so it
|
|
13
|
+
* rides the volatile half (teammateAvailabilityPrompt in peer-roster.ts). */
|
|
14
|
+
export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoordination = false) {
|
|
13
15
|
const chief = bots.find((bot) => bot.id === chiefId);
|
|
14
16
|
const chiefSection = sectionKey(chief?.section);
|
|
15
17
|
const sectionName = peerName(chiefSection) || "General";
|
|
@@ -17,11 +19,10 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
17
19
|
// the endpoints must agree, or the prompt names teammates the tools will
|
|
18
20
|
// then refuse to reach.
|
|
19
21
|
const team = reachablePeers(bots, chief ?? { id: chiefId, name: "" });
|
|
20
|
-
// `about: true` keeps the blurb the Chief staffs from
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// silently change how a Chief picks a specialist.
|
|
22
|
+
// `about: true` keeps the blurb the Chief staffs from. The ordinary-bot
|
|
23
|
+
// roster drops it (peer-roster.ts); widening the Chief's existing exposure
|
|
24
|
+
// was never in scope, and narrowing it here would silently change how a
|
|
25
|
+
// Chief picks a specialist.
|
|
25
26
|
const roster = renderRoster(team, {
|
|
26
27
|
max: ROSTER_MAX_BOTS,
|
|
27
28
|
empty: "- No other visible bots are available yet.",
|
|
@@ -29,7 +30,7 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
29
30
|
});
|
|
30
31
|
const delegation = canDelegate
|
|
31
32
|
? boundedCoordination
|
|
32
|
-
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work.
|
|
33
|
+
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Give clear briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
|
|
33
34
|
: [
|
|
34
35
|
"Use list_bots to confirm the live roster and IDs. When assigning work to a teammate, use delegate_bot: it returns immediately, keeps you available to the user, and delivers the teammate's outcome back into this conversation automatically — success or failure. When the result arrives you are woken with it: report it to the user and act. If the teammate fails or stalls, tell the user plainly and decide the next step yourself.",
|
|
35
36
|
"After delegate_bot accepts the task, acknowledge the handoff and continue with any independent work or end your turn. Do not call wait_delegation or repeatedly poll check_delegation in the same turn.",
|
|
@@ -52,6 +53,5 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
52
53
|
canDelegate ? "When the user asks you to assemble or configure a team, use list_team_setup for the exact authorized teams, bot IDs and model catalog, then propose_team_setup once with all named specialists and their profile/model changes. Include new teams explicitly; the plan covers their creation and your access. Existing thread models and other bots' execution permissions stay unchanged. Follow the tool result: granted Full Access may apply the plan immediately; after an applied result, continue already-requested work without another confirmation. Only if review is pending, end your turn: the user's decision automatically resumes you once with a structured result. Report failed or cancelled results honestly. Do not ask for another yes, poll, or repeat the proposal. After successful setup, use the available coordination tools for already requested work. Use create_bot only for a single specialist when no combined setup was requested. For explicitly requested bot deletion, use propose_bot_deletion separately and follow its applied or pending result too. Do not create duplicate or unnecessary bots." : "",
|
|
53
54
|
chief?.managedSections?.length ? "Reachable teammates in your allowed teams:" : `Current ${sectionName} section team:`,
|
|
54
55
|
roster,
|
|
55
|
-
trustedOpenMausStatus,
|
|
56
56
|
].filter(Boolean).join("\n");
|
|
57
57
|
}
|
|
@@ -30,11 +30,13 @@ import { dirname, join, resolve } from "node:path";
|
|
|
30
30
|
import { createInterface } from "node:readline/promises";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import qrcode from "qrcode-terminal";
|
|
33
|
+
import { phonePairingLink } from "../shared/pairing-link.js";
|
|
33
34
|
import { parseAllowList } from "./account-signin.js";
|
|
34
35
|
import { appendAdminAction, flushAdminActivity, sharedSignIn } from "./admin-activity.js";
|
|
35
36
|
import { bindDecisionRetention, decisionRetentionDays } from "./decision-log.js";
|
|
36
37
|
import { hostedWorkspaceConfigured } from "./enterprise.js";
|
|
37
38
|
import { resolveLoopbackTrust } from "./request-auth.js";
|
|
39
|
+
import { restartPolicy } from "./restart.js";
|
|
38
40
|
import { writeFileAtomic } from "./atomic.js";
|
|
39
41
|
import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
|
|
40
42
|
import { runServiceCommand } from "./service-cli.js";
|
|
@@ -466,15 +468,6 @@ export function pairingBlock(input) {
|
|
|
466
468
|
}
|
|
467
469
|
return lines.join("\n");
|
|
468
470
|
}
|
|
469
|
-
/** The scheme and host of a link, or null if it is not one we can dial. */
|
|
470
|
-
function originOf(link) {
|
|
471
|
-
try {
|
|
472
|
-
return new URL(link).origin;
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
471
|
export function qrToString(text) {
|
|
479
472
|
let out = "";
|
|
480
473
|
qrcode.generate(text, { small: true }, (rendered) => {
|
|
@@ -494,20 +487,18 @@ async function mintPairing(port, options) {
|
|
|
494
487
|
if (status !== 200)
|
|
495
488
|
throw new Error(`server refused to mint a pairing code: ${typeof body?.error === "string" ? body.error : status}`);
|
|
496
489
|
const url = options.publicUrl ? `${options.publicUrl}/pair#code=${body.code}` : typeof body.url === "string" ? body.url : null;
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
// credential, never on the server's
|
|
503
|
-
// OMB_PUBLIC_URL returns a credential and no invite
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
? `openmausbot://pair?address=${encodeURIComponent(address)}&token=${encodeURIComponent(body.credential)}${typeof body.serverName === "string" ? `&name=${encodeURIComponent(body.serverName)}` : ""}`
|
|
510
|
-
: typeof body.inviteUrl === "string" ? body.inviteUrl : null;
|
|
490
|
+
// The phone-app invite. This command asks over loopback, so the server
|
|
491
|
+
// already built it from its own public address with the same builder; it is
|
|
492
|
+
// printed as it came. `--public-url` wins, exactly as it does for the web
|
|
493
|
+
// link above: a server behind someone else's proxy often does not know its
|
|
494
|
+
// own public name, which is what that flag is for. That case gates on the
|
|
495
|
+
// credential, never on the server's invite — a server started without
|
|
496
|
+
// OMB_PUBLIC_URL returns a credential and no invite. A server too old to
|
|
497
|
+
// mint a credential simply has no invite: the web link still works, so an
|
|
498
|
+
// upgrade is never required to pair a browser.
|
|
499
|
+
const invite = (options.publicUrl && typeof body.credential === "string"
|
|
500
|
+
? phonePairingLink({ address: options.publicUrl, token: body.credential, name: typeof body.serverName === "string" ? body.serverName : undefined })
|
|
501
|
+
: null) ?? (typeof body.inviteUrl === "string" ? body.inviteUrl : null);
|
|
511
502
|
return pairingBlock({ code: body.code, url, inviteUrl: invite, expiresAt: body.expiresAt, hint: typeof body.hint === "string" ? body.hint : null, phone: options.phone });
|
|
512
503
|
}
|
|
513
504
|
// ── commands ───────────────────────────────────────────────────────────
|
|
@@ -853,7 +844,7 @@ async function planTunnel(options, log, recovery) {
|
|
|
853
844
|
}
|
|
854
845
|
}
|
|
855
846
|
else {
|
|
856
|
-
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery });
|
|
847
|
+
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery, log: (line) => log(`tunnel: ${line}`) });
|
|
857
848
|
if (account.credentials.status === "unavailable")
|
|
858
849
|
return fail(`${account.credentials.file} exists but could not be read; fix or remove it`);
|
|
859
850
|
if (!describeTunnelAccount(account.credentials.read()).email) {
|
|
@@ -1156,9 +1147,25 @@ export async function runServe(options, log = console.log) {
|
|
|
1156
1147
|
process.removeListener("SIGTERM", onSignal);
|
|
1157
1148
|
}
|
|
1158
1149
|
}
|
|
1150
|
+
/** `serve` until the server stops for good. A server that exits with
|
|
1151
|
+
* RESTART_EXIT_CODE (a copied workspace committed; docs/copy-workspace.md)
|
|
1152
|
+
* is started again in this process, with its tunnel, Tailscale or domain
|
|
1153
|
+
* address unchanged, and without a second pairing code or browser tab; at
|
|
1154
|
+
* most MAX_RESTARTS times in a row (server/restart.ts restartPolicy). */
|
|
1155
|
+
export async function serveUntilStopped(options, run = runServe, now = Date.now) {
|
|
1156
|
+
const policy = restartPolicy(now);
|
|
1157
|
+
let launch = options;
|
|
1158
|
+
for (;;) {
|
|
1159
|
+
const code = await run(launch);
|
|
1160
|
+
if (!policy.again(code))
|
|
1161
|
+
return code;
|
|
1162
|
+
console.log("\nOpenMausBot is starting again to finish installing a copy from the desktop app…");
|
|
1163
|
+
launch = { ...options, pair: false, open: false };
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1159
1166
|
/** Keep setup imports behind the data-dir override: config binds its paths
|
|
1160
1167
|
* when first imported. `serve` remains usable with stdin closed. */
|
|
1161
|
-
export async function runOnboardingCommand(options, io = defaultIo(), startServer =
|
|
1168
|
+
export async function runOnboardingCommand(options, io = defaultIo(), startServer = serveUntilStopped, flow = {}) {
|
|
1162
1169
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1163
1170
|
if (options.command === "setup" && !interactive) {
|
|
1164
1171
|
io.error("Setup needs an interactive terminal. Run `npx openmausbot setup` in a terminal, then use `npx openmausbot serve` for unattended starts.");
|
|
@@ -1235,7 +1242,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1235
1242
|
case "start":
|
|
1236
1243
|
return runOnboardingCommand(options);
|
|
1237
1244
|
case "serve":
|
|
1238
|
-
return
|
|
1245
|
+
return serveUntilStopped(options);
|
|
1239
1246
|
case "pair":
|
|
1240
1247
|
return runPair(options);
|
|
1241
1248
|
case "sessions":
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
export const CLOUD_BACKEND_CHANGE_ERROR = "stop the active turn before changing the cloud backend";
|
|
2
|
-
export const VPS_ALIAS_CHANGE_ERROR = "stop the active VPS turn before changing the SSH config alias";
|
|
3
2
|
export const BOAT_ACCOUNT_RESOURCES_ERROR = "remove this installation's cloud computers before changing or clearing the Boat account";
|
|
4
3
|
export const VPS_ALIAS_RESOURCES_ERROR = "remove this installation's VPS computers before changing or clearing the SSH config alias";
|
|
5
4
|
export function cloudBackendChangeError(botBusy, activeVpsThread) {
|
|
6
5
|
return botBusy || activeVpsThread ? CLOUD_BACKEND_CHANGE_ERROR : null;
|
|
7
6
|
}
|
|
8
|
-
export function vpsAliasChangeError(currentAlias, nextAlias, activeVpsThread) {
|
|
9
|
-
return activeVpsThread && currentAlias !== nextAlias ? VPS_ALIAS_CHANGE_ERROR : null;
|
|
10
|
-
}
|
|
11
7
|
/** An old Boat account may be detached only when it owns no local resources.
|
|
12
8
|
* Token rotation is safe when the replacement credential proves access to
|
|
13
9
|
* the exact same provider identities. */
|