openmausbot 0.1.95 → 0.1.97

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 (169) hide show
  1. package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +26 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/container-mcp.js +14 -27
  64. package/dist-server/drivers/agents-proxy.js +108 -49
  65. package/dist-server/harness-mcp-proxy.js +1 -2
  66. package/dist-server/index.js +4664 -4644
  67. package/dist-server/openmausbot.js +1425 -1437
  68. package/dist-server/pair-cli.js +1425 -1437
  69. package/dist-server/permission-proxy.js +0 -4
  70. package/dist-server/server/admin-activity.js +1 -4
  71. package/dist-server/server/auto-approve.js +20 -5
  72. package/dist-server/server/auto-vm-claims.js +13 -1
  73. package/dist-server/server/automatic-recovery.js +1 -2
  74. package/dist-server/server/boat.js +71 -87
  75. package/dist-server/server/bot-overview.js +1 -1
  76. package/dist-server/server/browser-engine.js +38 -1
  77. package/dist-server/server/browser-live.js +112 -25
  78. package/dist-server/server/browser-runtime.js +31 -1
  79. package/dist-server/server/checkpoints.js +340 -167
  80. package/dist-server/server/chief-of-staff.js +7 -6
  81. package/dist-server/server/cli-setup.js +3 -3
  82. package/dist-server/server/cli.js +33 -26
  83. package/dist-server/server/cloud-computer-tools.js +11 -4
  84. package/dist-server/server/cloud-home-start.js +13 -16
  85. package/dist-server/server/cloud-home.js +3 -13
  86. package/dist-server/server/cloud-move-http.js +44 -30
  87. package/dist-server/server/cloud-move.js +20 -18
  88. package/dist-server/server/cloud-owner.js +1 -1
  89. package/dist-server/server/composio.js +10 -3
  90. package/dist-server/server/computer-engine-removal.js +70 -0
  91. package/dist-server/server/computer-selection.js +18 -0
  92. package/dist-server/server/config.js +99 -75
  93. package/dist-server/server/connector-scope-hint.js +83 -0
  94. package/dist-server/server/container-computer.js +16 -5
  95. package/dist-server/server/contracts.js +0 -9
  96. package/dist-server/server/decider/jev.js +1 -4
  97. package/dist-server/server/decider/log.js +1 -3
  98. package/dist-server/server/decision-log.js +1 -4
  99. package/dist-server/server/default-model-selection.js +20 -14
  100. package/dist-server/server/delegations.js +14 -7
  101. package/dist-server/server/delta-context.js +6 -1
  102. package/dist-server/server/drivers/acp/core.js +5 -4
  103. package/dist-server/server/drivers/acp/opencode-go.js +19 -23
  104. package/dist-server/server/drivers/agents-call.js +47 -64
  105. package/dist-server/server/drivers/agents-catalog.js +69 -3
  106. package/dist-server/server/drivers/agents-proxy.js +2 -1
  107. package/dist-server/server/drivers/builtIn.js +0 -2
  108. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  109. package/dist-server/server/drivers/claude.js +49 -30
  110. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  111. package/dist-server/server/drivers/codex.js +7 -6
  112. package/dist-server/server/drivers/grok.js +1 -1
  113. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  114. package/dist-server/server/drivers/openai-chat.js +113 -11
  115. package/dist-server/server/drivers/pi.js +184 -49
  116. package/dist-server/server/drivers/prompt-split.js +4 -4
  117. package/dist-server/server/harness/bus.js +49 -0
  118. package/dist-server/server/harness-mcp-proxy.js +1 -1
  119. package/dist-server/server/index.js +942 -980
  120. package/dist-server/server/live-call.js +11 -6
  121. package/dist-server/server/managed-policy.js +1 -1
  122. package/dist-server/server/mcp-registry.js +35 -0
  123. package/dist-server/server/message-file.js +4 -4
  124. package/dist-server/server/model-catalog/presets.js +8 -0
  125. package/dist-server/server/peer-delivery.js +4 -19
  126. package/dist-server/server/peer-roster.js +40 -6
  127. package/dist-server/server/profile-versions.js +0 -31
  128. package/dist-server/server/provider-key-check.js +2 -1
  129. package/dist-server/server/restart.js +40 -0
  130. package/dist-server/server/room-handoffs.js +12 -0
  131. package/dist-server/server/room-turn-end.js +15 -0
  132. package/dist-server/server/routes/bot-memory.js +0 -24
  133. package/dist-server/server/routes/live.js +3 -3
  134. package/dist-server/server/routine-requests.js +2 -5
  135. package/dist-server/server/routines.js +6 -5
  136. package/dist-server/server/screen-frame-gate.js +1 -3
  137. package/dist-server/server/server-launcher.js +53 -0
  138. package/dist-server/server/store.js +126 -2
  139. package/dist-server/server/surface.js +40 -64
  140. package/dist-server/server/system-prompt.js +23 -13
  141. package/dist-server/server/team-manifest.js +1 -6
  142. package/dist-server/server/tunnel.js +4 -1
  143. package/dist-server/server/turn-continuation.js +162 -0
  144. package/dist-server/server/turn-images.js +1 -21
  145. package/dist-server/server/turn-resources.js +19 -150
  146. package/dist-server/server/usage-ledger.js +3 -1
  147. package/dist-server/server/workspace.js +3 -4
  148. package/dist-server/server-launcher.js +72 -0
  149. package/dist-server/shared/ask-question.js +1 -71
  150. package/dist-server/shared/cloud-computer.js +10 -0
  151. package/dist-server/shared/credential-request.js +1 -1
  152. package/dist-server/shared/failed-turn.js +22 -0
  153. package/dist-server/shared/live-approval.js +2 -2
  154. package/dist-server/shared/package-format.js +3 -3
  155. package/dist-server/shared/pairing-link.js +144 -0
  156. package/dist-server/shared/place-view.js +409 -0
  157. package/dist-server/shared/tool-scope-support.js +1 -2
  158. package/dist-server/vps-container-mcp.js +14 -27
  159. package/dist-server/workspace-backup.worker.js +14 -27
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DS2NbT5x.js +0 -1
  162. package/dist/assets/index-NF7XCHAy.css +0 -1
  163. package/dist/assets/index-X_5ydXIE.js +0 -610
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
  165. package/dist-server/server/claim-idle.js +0 -33
  166. package/dist-server/server/cloud-overflow.js +0 -167
  167. package/dist-server/server/drivers/boatagent.js +0 -443
  168. package/dist-server/server/tightening-requests.js +0 -435
  169. package/dist-server/shared/tightening-request.js +0 -87
@@ -9,6 +9,7 @@ import { lstatSync, realpathSync, statSync } from "node:fs";
9
9
  import { dirname, extname, resolve } from "node:path";
10
10
  import { fromMarkdown } from "mdast-util-from-markdown";
11
11
  import { ATTACHMENTS_DIR, IMAGE_MAX_BYTES } from "./attachments.js";
12
+ import { decodeAttachmentAttribute } from "./message-file.js";
12
13
  /** Matches the companion composer policy. Four maximum-sized images are
13
14
  * bounded to 40 MiB before a provider is asked to ingest them. */
14
15
  export const TURN_IMAGE_MAX_COUNT = 4;
@@ -24,27 +25,6 @@ const MIME_BY_EXTENSION = {
24
25
  function statusError(status, message) {
25
26
  return Object.assign(new Error(message), { status });
26
27
  }
27
- /** Decode exactly the entities emitted by the composer. Repeated or unknown
28
- * encodings stay encoded and therefore cannot acquire a privileged path. */
29
- function decodeAttachmentAttribute(value) {
30
- return value.replace(/&(quot|lt|gt|amp);|&#(9|10|13);/g, (entity, named, numeric) => {
31
- if (numeric === "9")
32
- return "\t";
33
- if (numeric === "10")
34
- return "\n";
35
- if (numeric === "13")
36
- return "\r";
37
- if (named === "quot")
38
- return '"';
39
- if (named === "lt")
40
- return "<";
41
- if (named === "gt")
42
- return ">";
43
- if (named === "amp")
44
- return "&";
45
- return entity;
46
- });
47
- }
48
28
  function walk(node, visit) {
49
29
  const pending = [node];
50
30
  while (pending.length > 0) {
@@ -1,12 +1,12 @@
1
- import { realpathSync } from "node:fs";
2
- import { relative, resolve, sep } from "node:path";
3
1
  /** One harness owns the data directory. Claims are synchronous and last for
4
2
  * the whole turn, not just a click: a screenshot and its following click
5
- * must see the same desktop. These coordinate app-managed resources; they
6
- * are not a sandbox for arbitrary shell commands. */
3
+ * must see the same desktop. These coordinate app-managed resources — a
4
+ * desktop, a browser session, a phone — that only one turn can drive at a
5
+ * time. A project folder is not one: a bot's threads work in one folder
6
+ * side by side, as several agent sessions do in one repo. They are not a
7
+ * sandbox for arbitrary shell commands. */
7
8
  export class TurnResources {
8
9
  owners = new Map();
9
- reclaims = new Map();
10
10
  /** Arrival-ordered waiters per requested resource (#1652). Entries exist
11
11
  * only while a turn is genuinely waiting (the exclusive bind's poll loop):
12
12
  * a lazy claim that failed once and moved on must not sit at the front and
@@ -72,25 +72,16 @@ export class TurnResources {
72
72
  waitEstimateMs(resource) {
73
73
  return this.waitStats.get(resource)?.ewmaMs;
74
74
  }
75
- blocker(resource, owner, now = Date.now()) {
76
- this.expireIdle(resource, now);
77
- for (const [key, current] of this.owners) {
78
- if (overlaps(key, resource) && !sameOwner(current.owner, owner))
79
- return current.owner;
80
- }
81
- return undefined;
75
+ blocker(resource, owner) {
76
+ const current = this.owners.get(resource);
77
+ return current && !sameOwner(current, owner) ? current : undefined;
82
78
  }
83
- claim(resource, owner, options = {}) {
84
- const now = options.now ?? Date.now();
85
- if (this.blocker(resource, owner, now))
79
+ claim(resource, owner) {
80
+ if (this.blocker(resource, owner))
86
81
  return false;
87
82
  const existing = this.owners.get(resource);
88
- if (existing && sameOwner(existing.owner, owner)) {
89
- // A repeated claim by the sitting owner changes nothing: replacing
90
- // the record would restart activityAt, and re-claims alone would
91
- // hold the seat past every quiet window (#1653).
83
+ if (existing && sameOwner(existing, owner))
92
84
  return true;
93
- }
94
85
  // The free seat belongs to the front waiter; a current holder's
95
86
  // revalidation above never loses its own claim to this queue.
96
87
  const queue = this.waiters.get(resource);
@@ -101,76 +92,12 @@ export class TurnResources {
101
92
  if (!queue.length)
102
93
  this.waiters.delete(resource);
103
94
  }
104
- const record = { owner };
105
- if (options.idle) {
106
- record.idle = options.idle;
107
- // Taking (or retaking) a seat is itself screen activity: the quiet
108
- // window of #1653 starts at the claim, not at the first later call.
109
- record.activityAt = now;
110
- }
111
- this.owners.set(resource, record);
112
- const reclaimed = this.reclaims.get(resource);
113
- // Seating an owner closes any idle-release record for this resource:
114
- // the previous holder re-claimed (its turn lives on), or another turn
115
- // took the free seat — an occupied seat is exactly where #1653 says
116
- // the old holder yields.
117
- if (reclaimed)
118
- this.reclaims.delete(resource);
95
+ this.owners.set(resource, owner);
119
96
  return true;
120
97
  }
121
- owns(resource, owner, now = Date.now()) {
122
- this.expireIdle(resource, now);
123
- const current = this.owners.get(resource);
124
- return Boolean(current && sameOwner(current.owner, owner));
125
- }
126
- /** A real screen call touched this claim (#1653): restart its quiet
127
- * window. Claims without an idle policy — and anything the screen
128
- * poller drives — are no-ops, so preview frames can never hold a seat. */
129
- activity(resource, owner, now = Date.now()) {
130
- // Expiry first: a straggler screen completion arriving at or past the
131
- // quiet boundary must release the seat and open the reclaim window,
132
- // not restart the clock on a claim that already lapsed.
133
- this.expireIdle(resource, now);
98
+ owns(resource, owner) {
134
99
  const current = this.owners.get(resource);
135
- if (current?.idle && sameOwner(current.owner, owner))
136
- current.activityAt = now;
137
- }
138
- /** A screen-touching computer tool call started on this claim (#1653):
139
- * the desktop is executing real I/O for the sitting owner, so the seat
140
- * cannot be idle-released mid-call. Only the sitting owner's start
141
- * counts; a start whose completion never arrives holds the seat until
142
- * settle — the pre-#1653 behavior, never an overlapping handoff. */
143
- beginComputerCall(resource, owner) {
144
- const current = this.owners.get(resource);
145
- if (current?.idle && sameOwner(current.owner, owner)) {
146
- current.computerCallsInFlight = (current.computerCallsInFlight ?? 0) + 1;
147
- }
148
- }
149
- /** The in-flight computer call finished: drop its fence and let the
150
- * completion's own activity tick govern the quiet window. A no-op when
151
- * the claim lapsed or another turn seated itself in between. */
152
- endComputerCall(resource, owner) {
153
- const current = this.owners.get(resource);
154
- if (!current?.idle || !sameOwner(current.owner, owner))
155
- return;
156
- const inFlight = current.computerCallsInFlight ?? 0;
157
- if (inFlight > 0)
158
- current.computerCallsInFlight = inFlight - 1;
159
- }
160
- /** Who may still re-claim `resource` directly after an idle release
161
- * (#1653): the mid-task turn that went quiet, until its reclaim window
162
- * ends or another turn seats itself. The direct re-claim itself is
163
- * just `claim`: a free seat answers the returning holder's next screen
164
- * call without the wait a new arrival would enter. */
165
- reclaimHolder(resource, now = Date.now()) {
166
- const reclaimed = this.reclaims.get(resource);
167
- if (!reclaimed)
168
- return undefined;
169
- if (reclaimed.until <= now) {
170
- this.reclaims.delete(resource);
171
- return undefined;
172
- }
173
- return reclaimed.owner;
100
+ return Boolean(current && sameOwner(current, owner));
174
101
  }
175
102
  release(owner) {
176
103
  for (const [key, queue] of this.waiters) {
@@ -181,81 +108,23 @@ export class TurnResources {
181
108
  this.waiters.delete(key);
182
109
  }
183
110
  for (const [key, current] of this.owners) {
184
- if (sameOwner(current.owner, owner))
111
+ if (sameOwner(current, owner))
185
112
  this.owners.delete(key);
186
113
  }
187
- // A settling turn keeps no reclaim priority (#1653): only a live
188
- // mid-task turn may pick its seat back up.
189
- for (const [key, reclaimed] of this.reclaims) {
190
- if (sameOwner(reclaimed.owner, owner))
191
- this.reclaims.delete(key);
192
- }
193
114
  }
194
115
  /** Drop one of an owner's claims early, when the sequence that took it
195
116
  * could not finish. The owner's other claims stand until settle. */
196
- releaseOne(resource, owner, now = Date.now()) {
197
- if (this.owns(resource, owner, now))
117
+ releaseOne(resource, owner) {
118
+ if (this.owns(resource, owner))
198
119
  this.owners.delete(resource);
199
120
  this.stopWaiting(resource, owner);
200
- // owns() may have just expired a quiet claim into a reclaim record
201
- // for this same owner: an early release keeps no reclaim priority.
202
- const reclaimed = this.reclaims.get(resource);
203
- if (reclaimed && sameOwner(reclaimed.owner, owner))
204
- this.reclaims.delete(resource);
205
- }
206
- /** Quiet-window expiry (#1653), evaluated when the claim is touched: a
207
- * screen-quiet seat is released while its turn lives and remembered for
208
- * the reclaim window. Synchronous on purpose — the claim map decides
209
- * every transition on the server's single thread. */
210
- expireIdle(resource, now) {
211
- const current = this.owners.get(resource);
212
- if (!current?.idle || current.activityAt === undefined)
213
- return;
214
- // A call the desktop is still executing fences the seat (#1653):
215
- // releasing mid-call would hand one desktop to two turns at once.
216
- if ((current.computerCallsInFlight ?? 0) > 0)
217
- return;
218
- if (!current.idle.quietElapsed(current.activityAt, now))
219
- return;
220
- this.owners.delete(resource);
221
- this.reclaims.set(resource, { owner: current.owner, until: now + current.idle.reclaimMs });
222
121
  }
223
122
  /** Whether any live owner holds this resource: the parked-resume drain's
224
123
  * gate (#1651) — a resume fires only when the seat it queued on is free. */
225
- free(resource, now = Date.now()) {
226
- for (const key of this.owners.keys()) {
227
- if (!overlaps(key, resource))
228
- continue;
229
- // A quiet-window claim expires the moment anything needs to know
230
- // whether the seat is free: the parked-resume gate must not wait on
231
- // a claim whose window already elapsed (#1653).
232
- this.expireIdle(key, now);
233
- if (this.owners.has(key))
234
- return false;
235
- }
236
- return true;
124
+ free(resource) {
125
+ return !this.owners.has(resource);
237
126
  }
238
127
  }
239
128
  function sameOwner(a, b) {
240
129
  return a.threadId === b.threadId && a.generation === b.generation;
241
130
  }
242
- export function workspaceResource(cwd) {
243
- // Selected folders must exist before the engine starts. Resolve symlinks
244
- // and native filename casing so aliases cannot grant two writers to the
245
- // same project on case-insensitive volumes.
246
- const canonical = realpathSync.native(resolve(cwd));
247
- return `workspace:${process.platform === "win32" ? canonical.toLowerCase() : canonical}`;
248
- }
249
- function overlaps(a, b) {
250
- if (a === b)
251
- return true;
252
- if (!a.startsWith("workspace:") || !b.startsWith("workspace:"))
253
- return false;
254
- const left = a.slice("workspace:".length);
255
- const right = b.slice("workspace:".length);
256
- const contains = (parent, child) => {
257
- const path = relative(parent, child);
258
- return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep) && !/^[A-Za-z]:/.test(path));
259
- };
260
- return contains(left, right) || contains(right, left);
261
- }
@@ -26,7 +26,9 @@ const promptBytesOf = (value) => {
26
26
  ? { stable: clean(record.stable), volatile: clean(record.volatile) }
27
27
  : null;
28
28
  };
29
- function monthKey(at) {
29
+ /** The UTC month a row belongs to, `YYYY-MM`: the name of its month file.
30
+ * The decision, admin-activity and decider logs use the same layout. */
31
+ export function monthKey(at) {
30
32
  return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
31
33
  }
32
34
  export function usageFileFor(dataDir, at) {
@@ -20,7 +20,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
20
20
  export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
21
21
  /** MCP support does not imply native filesystem tools or a local working directory. */
22
22
  export function supportsWorkspaceFiles(driverKind) {
23
- return !["grok", "openai-compat", "minimax", "mistral", "cerebras", "boxAgent"].includes(driverKind);
23
+ return !["grok", "openai-compat", "minimax", "mistral", "cerebras"].includes(driverKind);
24
24
  }
25
25
  /** Default task files are private to the thread, outside the bot's shared
26
26
  * memory folder. This is directory organization, not a shell sandbox. */
@@ -588,9 +588,8 @@ export function writeMemoryTopic(botId, name, text) {
588
588
  writeFileAtomic(join(workspaceDir(botId), "memory", name), redactSecretsInText(text), { mode: 0o600 });
589
589
  indexWrittenMemoryFile(botId, `memory/${name}`);
590
590
  }
591
- /** Read one topic file. The name gate runs here too, not only in the HTTP
592
- * route — a future caller must not be able to turn this into a read of an
593
- * arbitrary path. Null for anything invalid or unreadable. */
591
+ /** Read one topic file. The name gate runs here, so no caller can turn this
592
+ * into a read of an arbitrary path. Null for anything invalid or unreadable. */
594
593
  export function readMemoryTopic(botId, name) {
595
594
  if (!isMemoryTopicName(name))
596
595
  return null;
@@ -0,0 +1,72 @@
1
+ // server/server-launcher.ts
2
+ import { spawn } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ // server/restart.ts
8
+ var RESTART_EXIT_CODE = 75;
9
+ var MAX_RESTARTS = 5;
10
+ var STABLE_RUN_MS = 6e4;
11
+ function restartPolicy(now = Date.now) {
12
+ let restarts = 0, startedAt = now();
13
+ return {
14
+ again(code, stopping = false) {
15
+ if (now() - startedAt >= STABLE_RUN_MS) restarts = 0;
16
+ if (stopping || code !== RESTART_EXIT_CODE || restarts >= MAX_RESTARTS) return false;
17
+ restarts++;
18
+ startedAt = now();
19
+ return true;
20
+ }
21
+ };
22
+ }
23
+
24
+ // server/server-launcher.ts
25
+ function superviseServer(start, options = {}) {
26
+ const signals = options.signals ?? process, now = options.now ?? Date.now;
27
+ return new Promise((done) => {
28
+ let child, stopping = false;
29
+ const policy = restartPolicy(now);
30
+ const pass = (signal) => () => {
31
+ stopping = true;
32
+ child.kill(signal);
33
+ };
34
+ const onTerm = pass("SIGTERM"), onInt = pass("SIGINT");
35
+ const finish = (code) => {
36
+ signals.off("SIGTERM", onTerm);
37
+ signals.off("SIGINT", onInt);
38
+ done(code);
39
+ };
40
+ const run = () => {
41
+ child = start();
42
+ child.once("error", () => finish(1));
43
+ child.once("exit", (code, signal) => {
44
+ if (policy.again(code, stopping)) {
45
+ console.log("OpenMausBot is starting again to finish installing a copy from the desktop app\u2026");
46
+ run();
47
+ return;
48
+ }
49
+ finish(code ?? (stopping && (signal === "SIGTERM" || signal === "SIGINT") ? 0 : 1));
50
+ });
51
+ };
52
+ signals.on("SIGTERM", onTerm);
53
+ signals.on("SIGINT", onInt);
54
+ run();
55
+ });
56
+ }
57
+ function isEntry() {
58
+ try {
59
+ return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ if (isEntry()) {
65
+ const server = join(dirname(fileURLToPath(import.meta.url)), "index.js");
66
+ void superviseServer(() => spawn(process.execPath, [server], { stdio: "inherit" })).then((code) => {
67
+ process.exitCode = code;
68
+ });
69
+ }
70
+ export {
71
+ superviseServer
72
+ };
@@ -34,7 +34,7 @@ export const MAX_CUSTOM_ANSWER = 2000;
34
34
  export function isPersistentQuestionCard(card) {
35
35
  return Boolean(card && (card.requestType === "question" || card.questionRequest ||
36
36
  (!card.requestType && !card.tool && !card.routineRequest && !card.skillRequest && !card.profileRequest &&
37
- !card.modelRequest && !card.tighteningRequest && !card.teamSetupRequest)));
37
+ !card.modelRequest && !card.teamSetupRequest)));
38
38
  }
39
39
  /** System/provider completion ends the live run, not the human's question. */
40
40
  export function shouldSettleRequestCard(card, source) {
@@ -109,55 +109,6 @@ export function parseAskQuestions(input) {
109
109
  }
110
110
  return questions.length ? questions : null;
111
111
  }
112
- /** The fenced block a turn-boundary agent ends its run with when it wants
113
- * to ask the person something (the BoatAgent transport): the harness cannot
114
- * pause mid-run, so the questions ride the final output and OMB parses them
115
- * at settle. The block is model-authored — untrusted input like any tool
116
- * call — so its body runs through parseAskQuestions and the same caps. */
117
- const OMB_ASK_FENCE = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]{0,3}\2[ \t]*(?=\r?\n|$)/;
118
- const OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
119
- /** The questions inside the first fenced omb-ask block in an output, or
120
- * null when there is nothing worth showing (no fence, invalid JSON, or no
121
- * entry that parses as a question). The first fence wins: a second block
122
- * in the same output is ignored, not merged. */
123
- export function parseOmbAskQuestions(output) {
124
- const match = OMB_ASK_FENCE.exec(output);
125
- if (!match)
126
- return null;
127
- try {
128
- return parseAskQuestions(JSON.parse(match[3]));
129
- }
130
- catch {
131
- return null;
132
- }
133
- }
134
- /** The output with its omb-ask block(s) removed, for display: the person
135
- * reads the prose around the ask, never the raw protocol JSON. Text
136
- * without a block is returned untouched. */
137
- export function stripOmbAskBlock(output) {
138
- if (!OMB_ASK_FENCE.test(output))
139
- return output;
140
- return output.replace(OMB_ASK_FENCE_GLOBAL, "").replace(/\n{3,}/g, "\n\n").trim();
141
- }
142
- /** The prompt contract that teaches a model the turn-held ask transport: end
143
- * the reply with a fenced `omb-ask` block carrying the questions, and the
144
- * answers come back on the next prompt. A harness that cannot pause mid-run
145
- * (the BoatAgent transport) appends this to every prompt; the caps in the text
146
- * are the constants above, so the taught contract and the parser cannot
147
- * drift apart. */
148
- export function ombAskProtocolPrompt() {
149
- return [
150
- "",
151
- "## Asking the person a question",
152
- "When a decision belongs to the person, end your reply with a fenced block exactly like this:",
153
- "",
154
- "```omb-ask",
155
- '{"questions":[{"question":"Ship the release now?","header":"Release","options":[{"label":"Ship now"},{"label":"Wait for the QA signoff"}]}]}',
156
- "```",
157
- "",
158
- `The block must be the last thing in your reply. You may ask up to ${MAX_QUESTIONS} questions at once, each with up to ${MAX_OPTIONS} options; the person can always answer in their own words. Their answers arrive on your next prompt as \`Q:\`/\`A:\` lines — never invent them.`,
159
- ].join("\n");
160
- }
161
112
  /** The one line the card subtitle and a spoken prompt show. */
162
113
  export function askQuestionSummary(questions) {
163
114
  const first = questions[0]?.question ?? "";
@@ -254,10 +205,6 @@ export const ASK_USER_TOOL_DEFINITION = {
254
205
  },
255
206
  },
256
207
  };
257
- /** The synthetic tool string for an ask parsed out of model-authored final
258
- * output (the BoatAgent turn-held transport): there is no tool call to name,
259
- * but the event and the ASKS_A_PERSON backstop need one string. */
260
- export const OMB_ASK_TOOL = "omb-ask";
261
208
  /** The lead-in on a formatted answer. It exists for the model — the answer
262
209
  * is delivered on the deny channel, so it has to say what it is — and the
263
210
  * card strips it back off when it shows the person what they sent. */
@@ -283,23 +230,6 @@ export function formatQuestionAnswers(questions, answers) {
283
230
  export function answerWithoutPreamble(answer) {
284
231
  return answer.startsWith(`${ANSWER_PREAMBLE}\n\n`) ? answer.slice(ANSWER_PREAMBLE.length + 2) : answer;
285
232
  }
286
- /** The longest formatted answer OMB will echo back into a follow-up prompt
287
- * (the BoatAgent continuation). formatQuestionAnswers itself does not
288
- * truncate — the tool-result channel has no stated limit — but a prompt is
289
- * not the place to find one: six answers at the custom-answer cap is the
290
- * most a legitimate reply weighs, so that is the ceiling. */
291
- export const MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
292
- /** Cap a formatted answer for echoing into a follow-up prompt. An over-cap
293
- * echo is cut back to the last whole block so no partial answer reads as
294
- * one, and says it was truncated. */
295
- export function capAnswerEcho(answer, limit = MAX_ANSWER_ECHO) {
296
- if (answer.length <= limit)
297
- return answer;
298
- const cut = answer.slice(0, limit);
299
- const boundary = cut.lastIndexOf("\n\nQ: ");
300
- const kept = boundary > 0 ? cut.slice(0, boundary) : cut;
301
- return kept + "\n\n[answer truncated]";
302
- }
303
233
  /**
304
234
  * The answer text, read back as one value per question.
305
235
  *
@@ -0,0 +1,10 @@
1
+ // Which engines can work on a cloud computer: Hosted desktop on Boat, or a
2
+ // self-hosted VPS. One rule for the server (every attach, readiness check and
3
+ // select_computer offer) and the renderer (the Works on panel, the composer's
4
+ // place chip, Webhooks and Routines), so the two never disagree.
5
+ /** An engine with computer tools gets the cloud computer as one more stdio
6
+ * computer server, on a Boat and on a VPS alike. The turn always stays on the
7
+ * bot's own engine; nothing hands it to another one. */
8
+ export function canWorkOnCloud(engine) {
9
+ return engine?.computerMcp === true;
10
+ }
@@ -18,7 +18,7 @@ export const CREDENTIAL_TARGETS = {
18
18
  },
19
19
  opencodeGoApiKey: {
20
20
  label: "OpenCode API key",
21
- description: "Used for OpenCode Go and other key-backed OpenCode providers.",
21
+ description: "Used for OpenCode Zen and Go.",
22
22
  placeholder: "Paste your OpenCode API key",
23
23
  helpUrl: "https://opencode.ai/docs/providers/",
24
24
  },
@@ -0,0 +1,22 @@
1
+ const MARKER = "error:";
2
+ /** The longest cause a row keeps. Clients wrap the row and show it whole, so
3
+ * this only bounds a pathological message (a provider's HTML body, a stack
4
+ * trace); a sentence a person acts on fits, its next action included. */
5
+ export const FAILED_TURN_MAX_CHARS = 600;
6
+ /** The activity row a failed turn is stored as. */
7
+ export function failedTurnTool(cause, flags = {}) {
8
+ const words = cause.length > FAILED_TURN_MAX_CHARS ? `${cause.slice(0, FAILED_TURN_MAX_CHARS - 1)}…` : cause;
9
+ return {
10
+ name: `${MARKER} ${words}`,
11
+ ok: false,
12
+ ...(flags.setup ? { setup: true } : {}),
13
+ ...(flags.terminal ? { terminal: true } : {}),
14
+ ...(flags.claudeUpdate ? { claudeUpdate: true } : {}),
15
+ ...(flags.place ? { place: flags.place } : {}),
16
+ };
17
+ }
18
+ /** The cause a failed-turn row carries, without its marker; null for any
19
+ * other activity row. */
20
+ export function failedTurnCause(name) {
21
+ return name.startsWith(MARKER) ? name.slice(MARKER.length).trim() : null;
22
+ }
@@ -1,12 +1,12 @@
1
1
  /** "approval": a provider or peer ask the voice may decide with a strict yes/no.
2
2
  * "review": a harness-native proposal (skill, routine, profile, default
3
- * model, tightening, team setup) that must be reviewed on screen.
3
+ * model, team setup) that must be reviewed on screen.
4
4
  * "question": a provider question. An expired proposal is settled: nothing
5
5
  * can answer it any more. */
6
6
  export function liveCardKind(card) {
7
7
  if (!card?.requestId || card.answered || card.dismissed || card.expired)
8
8
  return null;
9
- if (card.skillRequest || card.routineRequest || card.profileRequest || card.modelRequest || card.tighteningRequest || card.teamSetupRequest) {
9
+ if (card.skillRequest || card.routineRequest || card.profileRequest || card.modelRequest || card.teamSetupRequest) {
10
10
  return "review";
11
11
  }
12
12
  return card.tool ? "approval" : "question";
@@ -55,9 +55,9 @@ const encoder = new TextEncoder();
55
55
  export function utf8Bytes(value) {
56
56
  return encoder.encode(value).length;
57
57
  }
58
- // ── shared field helpers (identical to the v1 reader) ─────────────────────
59
- const requiredText = (max) => z.string({ error: "must be text" }).trim().min(1, { message: "is required" }).max(max, { message: "is too long" });
60
- const optionalText = (max) => z
58
+ // ── shared field helpers (the team manifest reader uses these too) ────────
59
+ export const requiredText = (max) => z.string({ error: "must be text" }).trim().min(1, { message: "is required" }).max(max, { message: "is too long" });
60
+ export const optionalText = (max) => z
61
61
  .union([z.string({ error: "must be text" }), z.null(), z.undefined()])
62
62
  .transform((value) => value?.trim() || undefined)
63
63
  .refine((value) => value === undefined || value.length <= max, { message: "is too long" })