openmausbot 0.1.95 → 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 (140) hide show
  1. package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-7kei4Hsu.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-KygHRE3E.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-BG7uaEd4.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-CcR4pc0-.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +25 -12
  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 +103 -44
  65. package/dist-server/harness-mcp-proxy.js +1 -2
  66. package/dist-server/index.js +2633 -3396
  67. package/dist-server/openmausbot.js +606 -463
  68. package/dist-server/pair-cli.js +606 -463
  69. package/dist-server/server/admin-activity.js +1 -4
  70. package/dist-server/server/auto-approve.js +19 -4
  71. package/dist-server/server/boat.js +4 -4
  72. package/dist-server/server/checkpoints.js +90 -148
  73. package/dist-server/server/chief-of-staff.js +7 -6
  74. package/dist-server/server/cli.js +33 -26
  75. package/dist-server/server/cloud-home-start.js +12 -15
  76. package/dist-server/server/cloud-home.js +0 -4
  77. package/dist-server/server/cloud-move-http.js +44 -30
  78. package/dist-server/server/cloud-move.js +20 -18
  79. package/dist-server/server/composio.js +10 -3
  80. package/dist-server/server/config.js +92 -64
  81. package/dist-server/server/container-computer.js +16 -5
  82. package/dist-server/server/contracts.js +0 -9
  83. package/dist-server/server/decider/jev.js +1 -4
  84. package/dist-server/server/decider/log.js +1 -3
  85. package/dist-server/server/decision-log.js +1 -4
  86. package/dist-server/server/delegations.js +3 -2
  87. package/dist-server/server/drivers/acp/core.js +4 -3
  88. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  89. package/dist-server/server/drivers/agents-call.js +46 -63
  90. package/dist-server/server/drivers/agents-catalog.js +66 -0
  91. package/dist-server/server/drivers/agents-proxy.js +2 -1
  92. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  93. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  94. package/dist-server/server/drivers/codex.js +5 -4
  95. package/dist-server/server/drivers/grok.js +1 -1
  96. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  97. package/dist-server/server/drivers/openai-chat.js +15 -5
  98. package/dist-server/server/drivers/prompt-split.js +4 -4
  99. package/dist-server/server/harness/bus.js +49 -0
  100. package/dist-server/server/harness-mcp-proxy.js +1 -1
  101. package/dist-server/server/index.js +448 -762
  102. package/dist-server/server/mcp-registry.js +35 -0
  103. package/dist-server/server/message-file.js +4 -4
  104. package/dist-server/server/peer-delivery.js +4 -19
  105. package/dist-server/server/peer-roster.js +30 -4
  106. package/dist-server/server/profile-versions.js +0 -31
  107. package/dist-server/server/provider-key-check.js +2 -1
  108. package/dist-server/server/restart.js +40 -0
  109. package/dist-server/server/room-handoffs.js +12 -0
  110. package/dist-server/server/routes/bot-memory.js +0 -24
  111. package/dist-server/server/routine-requests.js +1 -4
  112. package/dist-server/server/routines.js +5 -4
  113. package/dist-server/server/server-launcher.js +53 -0
  114. package/dist-server/server/store.js +1 -1
  115. package/dist-server/server/surface.js +23 -27
  116. package/dist-server/server/system-prompt.js +13 -3
  117. package/dist-server/server/team-manifest.js +1 -6
  118. package/dist-server/server/tunnel.js +4 -1
  119. package/dist-server/server/turn-images.js +1 -21
  120. package/dist-server/server/turn-resources.js +19 -150
  121. package/dist-server/server/usage-ledger.js +3 -1
  122. package/dist-server/server/workspace.js +2 -3
  123. package/dist-server/server-launcher.js +72 -0
  124. package/dist-server/shared/ask-question.js +1 -1
  125. package/dist-server/shared/cloud-computer.js +12 -0
  126. package/dist-server/shared/failed-turn.js +25 -0
  127. package/dist-server/shared/live-approval.js +2 -2
  128. package/dist-server/shared/package-format.js +3 -3
  129. package/dist-server/shared/pairing-link.js +144 -0
  130. package/dist-server/vps-container-mcp.js +14 -27
  131. package/dist-server/workspace-backup.worker.js +14 -27
  132. package/package.json +1 -1
  133. package/dist/assets/channel-DS2NbT5x.js +0 -1
  134. package/dist/assets/index-NF7XCHAy.css +0 -1
  135. package/dist/assets/index-X_5ydXIE.js +0 -610
  136. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
  137. package/dist-server/server/claim-idle.js +0 -33
  138. package/dist-server/server/cloud-overflow.js +0 -167
  139. package/dist-server/server/tightening-requests.js +0 -435
  140. 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) {
@@ -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) {
@@ -0,0 +1,12 @@
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. The Computer engine (boxAgent) runs its whole turn on its
7
+ * Boat, so it can work on a Boat but not on a VPS. */
8
+ export function canWorkOnCloud(engine, backend) {
9
+ const runsOnBoat = engine?.driverKind === "boxAgent";
10
+ const hasComputerTools = engine?.computerMcp === true;
11
+ return backend === "vps" ? hasComputerTools && !runsOnBoat : hasComputerTools || runsOnBoat;
12
+ }
@@ -0,0 +1,25 @@
1
+ // A failed turn is stored as an activity row named "error: <what went
2
+ // wrong>", in a 1:1 chat and a room alike. The server writes every such row
3
+ // through failedTurnTool and the clients read it back through
4
+ // failedTurnCause, so the marker and the one length limit live here only.
5
+ const MARKER = "error:";
6
+ /** The longest cause a row keeps. Clients wrap the row and show it whole, so
7
+ * this only bounds a pathological message (a provider's HTML body, a stack
8
+ * trace); a sentence a person acts on fits, its next action included. */
9
+ export const FAILED_TURN_MAX_CHARS = 600;
10
+ /** The activity row a failed turn is stored as. */
11
+ export function failedTurnTool(cause, flags = {}) {
12
+ const words = cause.length > FAILED_TURN_MAX_CHARS ? `${cause.slice(0, FAILED_TURN_MAX_CHARS - 1)}…` : cause;
13
+ return {
14
+ name: `${MARKER} ${words}`,
15
+ ok: false,
16
+ ...(flags.setup ? { setup: true } : {}),
17
+ ...(flags.terminal ? { terminal: true } : {}),
18
+ ...(flags.claudeUpdate ? { claudeUpdate: true } : {}),
19
+ };
20
+ }
21
+ /** The cause a failed-turn row carries, without its marker; null for any
22
+ * other activity row. */
23
+ export function failedTurnCause(name) {
24
+ return name.startsWith(MARKER) ? name.slice(MARKER.length).trim() : null;
25
+ }
@@ -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" })
@@ -0,0 +1,144 @@
1
+ // The one `openmausbot://pair` link builder. The desktop's phone setup, the
2
+ // headless server's pairing endpoint and `openmausbot pair` all print this
3
+ // link; the Android and iOS scanners read it (android/core Connection.kt
4
+ // `PairingInvite.parse`, ios CompanionCore Client.swift `PairingInvite.parse`).
5
+ //
6
+ // Every value is written with encodeURIComponent, so a space is `%20`. The
7
+ // desktop used URLSearchParams until Oct 2026, which writes a space as `+`;
8
+ // neither phone read that back as a space, and "Miguel's computer" showed up
9
+ // as "Miguel's+computer". The phones now also read `+` as a space, for links
10
+ // from desktops still running the old builder.
11
+ /** How many fallback routes a link will carry. The list is tiny in practice;
12
+ * the cap only keeps a pathological interface table from bloating the QR. */
13
+ export const MAX_PAIRING_HOSTS = 8;
14
+ const ENDPOINT_KINDS = new Set(["hosted", "tailnet", "lan", "bonjour"]);
15
+ /** Keep the QR contract strict even though its input came from our own
16
+ * sidecar. A public URL with credentials or a path is not a companion base
17
+ * URL, and filtering it is safer than teaching the phone to reinterpret it.
18
+ * Sorted by priority, deduplicated by origin, capped. */
19
+ export function qrEndpoints(endpoints) {
20
+ const seen = new Set();
21
+ const valid = [];
22
+ for (const endpoint of endpoints ?? []) {
23
+ if (!endpoint ||
24
+ !ENDPOINT_KINDS.has(endpoint.kind) ||
25
+ !Number.isInteger(endpoint.priority) ||
26
+ endpoint.priority < 0 ||
27
+ endpoint.priority > 1_000_000) {
28
+ continue;
29
+ }
30
+ try {
31
+ const parsed = new URL(endpoint.url);
32
+ const expectedProtocol = endpoint.kind === "hosted" ? "https:" : "http:";
33
+ const explicitPort = parsed.port ? Number(parsed.port) : null;
34
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
35
+ if (parsed.protocol !== expectedProtocol ||
36
+ // No empty DNS label (".ts.net", "mac..local"): the phones refuse
37
+ // one, and one endpoint they refuse makes them refuse the whole QR.
38
+ hostname.split(".").includes("") ||
39
+ (endpoint.kind === "tailnet" && !hostname.endsWith(".ts.net")) ||
40
+ (explicitPort !== null && (!Number.isInteger(explicitPort) || explicitPort < 1 || explicitPort > 65_535)) ||
41
+ parsed.username ||
42
+ parsed.password ||
43
+ parsed.pathname !== "/" ||
44
+ parsed.search ||
45
+ parsed.hash ||
46
+ seen.has(parsed.origin)) {
47
+ continue;
48
+ }
49
+ seen.add(parsed.origin);
50
+ valid.push({ url: parsed.origin, kind: endpoint.kind, priority: endpoint.priority });
51
+ }
52
+ catch {
53
+ // One malformed advisory route must not invalidate an otherwise usable
54
+ // pairing QR. It is simply omitted from the route walk.
55
+ }
56
+ }
57
+ return valid.sort((left, right) => left.priority - right.priority).slice(0, MAX_PAIRING_HOSTS);
58
+ }
59
+ /** URL-safe, unpadded base64 keeps the structured JSON smaller than query
60
+ * escaping every quote and slash while remaining straightforward to decode
61
+ * with Foundation on iOS. */
62
+ function encodeEndpoints(endpoints) {
63
+ const bytes = new TextEncoder().encode(JSON.stringify(endpoints));
64
+ let binary = "";
65
+ for (const byte of bytes)
66
+ binary += String.fromCharCode(byte);
67
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
68
+ }
69
+ function validSecretPublicKey(value) {
70
+ if (!value || !/^[A-Za-z0-9_-]{87}$/.test(value))
71
+ return null;
72
+ try {
73
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/") + "=";
74
+ const decoded = atob(base64);
75
+ if (decoded.length !== 65 || decoded.charCodeAt(0) !== 4)
76
+ return null;
77
+ let binary = "";
78
+ for (let index = 0; index < decoded.length; index += 1)
79
+ binary += decoded[index];
80
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "") === value
81
+ ? value
82
+ : null;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ /** The `address` value: `host:port` (IPv6 bracketed) or a bare http(s)
89
+ * origin, or null when it is neither. Both phones refuse an address with
90
+ * credentials, a path, a query or a fragment, as `qrEndpoints` does. */
91
+ function linkAddress(address, port) {
92
+ const value = address.trim();
93
+ if (!value)
94
+ return null;
95
+ if (/^https?:\/\//i.test(value)) {
96
+ try {
97
+ const parsed = new URL(value);
98
+ if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash)
99
+ return null;
100
+ return parsed.origin;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ if (port === undefined || !Number.isInteger(port) || port < 1 || port > 65_535)
107
+ return null;
108
+ const authority = value.includes(":") && !value.startsWith("[") ? `[${value}]` : value;
109
+ return `${authority}:${port}`;
110
+ }
111
+ /**
112
+ * A short-lived handoff from a trusted pairing screen to the mobile app. The
113
+ * credential still has to be redeemed; putting it in the link does not
114
+ * create or expose the long-lived device token. Null when any required part
115
+ * is malformed, so a caller never prints a link a phone would refuse.
116
+ */
117
+ export function phonePairingLink({ address, port, token, code, name, hosts, endpoints, secretPublicKey, }) {
118
+ const dial = linkAddress(address, port);
119
+ if (!dial || !/^omb_pair_[A-Za-z0-9_-]{43}$/.test(token))
120
+ return null;
121
+ if (code !== undefined && !/^\d{6}$/.test(code))
122
+ return null;
123
+ const fields = [["address", dial], ["token", token]];
124
+ if (code !== undefined)
125
+ fields.push(["code", code]);
126
+ if (name?.trim())
127
+ fields.push(["name", name.trim()]);
128
+ // Comma-joined, which no hostname or IP literal can contain. Filtered
129
+ // rather than refused: a bad candidate costs the phone one failed dial,
130
+ // and dropping the whole link over it would break pairing entirely.
131
+ const candidates = (hosts ?? [])
132
+ .map((candidate) => candidate.trim())
133
+ .filter((candidate) => candidate && !/[\s/?#,[\]]/.test(candidate))
134
+ .slice(0, MAX_PAIRING_HOSTS);
135
+ if (candidates.length)
136
+ fields.push(["hosts", candidates.join(",")]);
137
+ const routes = qrEndpoints(endpoints);
138
+ if (routes.length)
139
+ fields.push(["endpoints", encodeEndpoints(routes)]);
140
+ const secretKey = validSecretPublicKey(secretPublicKey);
141
+ if (secretKey)
142
+ fields.push(["secretKey", secretKey]);
143
+ return `openmausbot://pair?${fields.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&")}`;
144
+ }
@@ -16202,10 +16202,6 @@ var remoteMutationSchema = remoteEntrySchema.extend({
16202
16202
  }).optional()
16203
16203
  });
16204
16204
 
16205
- // server/cloud-overflow.ts
16206
- var CLOUD_SEAT_IDLE_STOP_MS = 5 * 6e4;
16207
- var CLOUD_SEAT_SLEEP_RETRY_MAX_MS = 10 * 6e4;
16208
-
16209
16205
  // server/config.ts
16210
16206
  var optionalText = external_exports.string().optional();
16211
16207
  var SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
@@ -16403,17 +16399,6 @@ var featureConfigSchema = external_exports.object({
16403
16399
  * conversations that share words with the message ride into the turn.
16404
16400
  * Read-only; on unless explicitly switched off — see autoRecallEnabled. */
16405
16401
  autoRecall: external_exports.boolean().optional(),
16406
- /** Idle release for computer claims (#1653): a desktop seat that stays
16407
- * screen-quiet for 90 seconds is released to waiting turns while its
16408
- * holder's turn still lives; the previous holder re-claims directly
16409
- * for 10 minutes and yields to an occupied seat. Off until baked; see
16410
- * computerClaimIdleReleaseEnabled for how to enable it by hand. */
16411
- computerClaimIdleRelease: external_exports.boolean().optional(),
16412
- /** Consented cloud overflow for local computer waits (#1655): a wait
16413
- * may offer a per-second-billed cloud seat, but only through an explicit
16414
- * consent card or a configured allowlist — never a silent Auto default.
16415
- * Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
16416
- cloudOverflow: external_exports.boolean().optional(),
16417
16402
  /** Opt-in shared skills library: one store at the data dir that bots
16418
16403
  * read by assignment instead of per-workspace copies. Off until
16419
16404
  * explicitly enabled — see skillsLibraryEnabled. */
@@ -16539,8 +16524,12 @@ var appConfigSchema = external_exports.object({
16539
16524
  * provider is Boat now and the key is kept for compatibility. */
16540
16525
  box: external_exports.object({ token: optionalText }).optional(),
16541
16526
  vps: vpsConfigSchema.optional(),
16542
- /** Optional OpenCode key; persisted write-only and passed only to its child. */
16543
- opencodeGo: external_exports.object({ apiKey: optionalText }).optional(),
16527
+ /** Optional OpenCode key; persisted write-only and passed only to its child.
16528
+ * `providerKeys`: keys for OpenCode's other providers (Venice, Groq…) by the
16529
+ * environment name OpenCode reads, also write-only and only for its child.
16530
+ * Settings changes go through mergeOpenCodeProviderKeys; the stored copy
16531
+ * is read loosely (storedAppConfigSchema). */
16532
+ opencodeGo: external_exports.object({ apiKey: optionalText, providerKeys: external_exports.record(external_exports.string(), external_exports.string()).optional() }).optional(),
16544
16533
  /** Voice settings and the selected voice id. `provider` picks the
16545
16534
  * engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
16546
16535
  * `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
@@ -16620,15 +16609,6 @@ var appConfigSchema = external_exports.object({
16620
16609
  /** The authorization decision log (server/decision-log.ts): days of month
16621
16610
  * files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
16622
16611
  decisions: external_exports.object({ retentionDays: external_exports.number().int().min(1).max(3650).optional() }).strict().optional(),
16623
- /** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
16624
- * verified rate: with no price configured the feature stays inert rather
16625
- * than show an invented one. allowlistedThreads carries standing consent
16626
- * for exact thread ids. */
16627
- cloudOverflow: external_exports.object({
16628
- perSecondCostUsd: external_exports.number().positive().max(10).optional(),
16629
- idleStopMs: external_exports.number().int().positive().max(24 * 60 * 6e4).optional(),
16630
- allowlistedThreads: external_exports.array(external_exports.string().min(1)).max(1e3).optional()
16631
- }).strict().optional(),
16632
16612
  localVm: localVmConfigSchema.optional(),
16633
16613
  features: featureConfigSchema.optional(),
16634
16614
  onboarding: onboardingConfigSchema.optional(),
@@ -16643,7 +16623,14 @@ var appConfigSchema = external_exports.object({
16643
16623
  mcpServers: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
16644
16624
  });
16645
16625
  var storedAppConfigSchema = appConfigSchema.extend({
16646
- browserProfiles: storedBrowserProfilesSchema.optional()
16626
+ browserProfiles: storedBrowserProfilesSchema.optional(),
16627
+ /** Read loosely, like mcpServers: a hand-edited provider key that isn't
16628
+ * text is dropped here, and a bad name or key later
16629
+ * (openCodeProviderKeys), never the whole file. */
16630
+ opencodeGo: external_exports.object({
16631
+ apiKey: optionalText,
16632
+ providerKeys: external_exports.record(external_exports.string(), external_exports.unknown()).catch({}).transform((keys) => Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string"))).optional()
16633
+ }).optional()
16647
16634
  });
16648
16635
  var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true }).extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
16649
16636
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.json());