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.
Files changed (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. 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
- // Restore is redo-friendly on purpose: it commits a safety point, then moves
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 and restore shape follow Gemini CLI's gitService, the sanitized
24
- // GIT_* env list and the exclude categories follow Roo-Code's checkpoint
25
- // service, and the snapshot-before-every-turn cadence follows Cline.
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. The API hands out full hashes; accepting anything
34
- * looser would let arbitrary revspecs ("HEAD~3", "main@{u}") reach git. */
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. Restores must never delete
37
- // these either: `git clean -fd` (without -x) leaves ignored files alone, so
38
- // everything listed here survives a rollback untouched. Categories follow
39
- // Roo-Code's checkpoint excludes: VCS internals, dependency trees, build
40
- // output, caches, logs, secrets, media, archives, databases, model weights.
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/restore, after
124
- // their source objects are no longer needed.
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, as turn-resources.ts does for workspace claims.
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/restore resolves relative to it. A hung git (index
207
- * lock, dead network filesystem) would otherwise jam the per-repo queue for
208
- * the whole session, so every call carries a hard timeout. */
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 restores against the
238
- // same folder queue behind each other (git's index lock would fail the loser
239
- // anyway — this turns a crash into a wait). The stored tail never rejects, so
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 — it is filtered from listings and refused as a restore
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. A partial add must never replace
296
- * the last usable checkpoint or authorize removal of its objects. */
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 { hash: previous, complete: false };
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 { hash: previous, complete: true };
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 { hash, complete: true };
337
+ return hash;
320
338
  }
321
339
  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.
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 restore point.
330
- console.warn("checkpoint cleanup deferred; the latest restore point is preserved");
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, or the folder is refused.
335
- * Never throws — this is called fire-and-forget on the turn path. */
336
- export async function snapshot(botId, cwd, label, signal) {
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 result = await commitAll(worktree, env, label, signal);
352
- if (!result.complete)
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 result.hash;
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
- /** Diff the settled workspace without replacing its pre-turn restore point.
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
- export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpenMausStatus = "", boundedCoordination = false) {
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 — and keeps this
21
- // prompt byte-identical to what Chiefs have always been given. The
22
- // ordinary-bot roster drops it (peer-roster.ts); widening the Chief's
23
- // existing exposure was never in scope, and narrowing it here would
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. Outside a room, each distinct assignment starts a fresh thread for that teammate, using their own model and permissions. Include the context they need in each brief. Busy teammates queue. Give self-contained 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
+ ? "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
- // A server too old to mint a credential simply has no invite: the web link
498
- // still works, so an upgrade is never required to pair a browser.
499
- // The address the phone will dial. `--public-url` wins, exactly as it does
500
- // for the web link above: a server behind someone else's proxy often does
501
- // not know its own public name, which is what that flag is for. Gate on the
502
- // credential, never on the server's own invite — a server started without
503
- // OMB_PUBLIC_URL returns a credential and no invite, and gating on the
504
- // invite would throw away a secret the CLI has every part it needs to use.
505
- const address = options.publicUrl ?? (typeof body.url === "string" ? originOf(body.url) : null);
506
- // A server too old to mint a credential simply has no invite: the web link
507
- // still works, so an upgrade is never required to pair a browser.
508
- const invite = typeof body.credential === "string" && address
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 = runServe, flow = {}) {
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 runServe(options);
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. */