openmausbot 0.1.65 → 0.1.67

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 (54) hide show
  1. package/dist/assets/index-A3CBTxwl.css +1 -0
  2. package/dist/assets/{index-BZeH50da.js → index-BwpNqgCm.js} +1 -1
  3. package/dist/assets/index-CDuyoVPJ.js +278 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/browser-proxy.js +121 -0
  6. package/dist-server/computer-proxy.js +14 -3
  7. package/dist-server/container-mcp.js +43 -19
  8. package/dist-server/drivers/agents-proxy.js +31 -0
  9. package/dist-server/index.js +18985 -16059
  10. package/dist-server/local-computer-proxy.js +433 -0
  11. package/dist-server/local-computer.js +48 -5
  12. package/dist-server/mcp-server.js +38 -17
  13. package/dist-server/openmausbot.js +1666 -775
  14. package/dist-server/pair-cli.js +1666 -775
  15. package/dist-server/proxy-paths.js +2 -0
  16. package/dist-server/server/account-signin.js +103 -0
  17. package/dist-server/server/attachments.js +125 -13
  18. package/dist-server/server/browser-engine-release.js +10 -8
  19. package/dist-server/server/browser-engine.js +226 -26
  20. package/dist-server/server/browser-live.js +567 -0
  21. package/dist-server/server/browser-proxy.js +129 -0
  22. package/dist-server/server/browser-runtime.js +370 -0
  23. package/dist-server/server/caddy.js +203 -0
  24. package/dist-server/server/claude-accounts.js +103 -0
  25. package/dist-server/server/cli.js +163 -22
  26. package/dist-server/server/computer-proxy.js +11 -2
  27. package/dist-server/server/config.js +41 -19
  28. package/dist-server/server/control-client.js +7 -1
  29. package/dist-server/server/custom-domain.js +14 -0
  30. package/dist-server/server/drivers/agents-proxy.js +34 -0
  31. package/dist-server/server/drivers/claude-login-auth.js +334 -0
  32. package/dist-server/server/drivers/claude.js +85 -19
  33. package/dist-server/server/environment.js +1 -0
  34. package/dist-server/server/harness/bus.js +9 -4
  35. package/dist-server/server/harness/registry.js +15 -1
  36. package/dist-server/server/index.js +1120 -379
  37. package/dist-server/server/local-computer-proxy.js +28 -0
  38. package/dist-server/server/local-computer.js +17 -0
  39. package/dist-server/server/mcp-bridge.js +27 -11
  40. package/dist-server/server/peer-approval.js +19 -6
  41. package/dist-server/server/provider-auth-sessions.js +7 -0
  42. package/dist-server/server/proxy-paths.js +2 -0
  43. package/dist-server/server/sessions.js +42 -1
  44. package/dist-server/server/steer-queue.js +10 -8
  45. package/dist-server/server/store.js +234 -34
  46. package/dist-server/server/team-backup.js +1 -0
  47. package/dist-server/server/tunnel.js +27 -1
  48. package/dist-server/server/turn-resources.js +50 -0
  49. package/dist-server/server/workspace.js +53 -3
  50. package/dist-server/src/lib/composer-attachments.js +3 -3
  51. package/dist-server/vps-container-mcp.js +43 -19
  52. package/package.json +1 -1
  53. package/dist/assets/index-BOrNJcfG.js +0 -273
  54. package/dist/assets/index-Dwzk199I.css +0 -1
@@ -8,7 +8,9 @@ function resolveProxy(relative) {
8
8
  return existsSync(source) ? source : join(SERVER_ROOT, `${relative}.js`);
9
9
  }
10
10
  var SPAWNED_PROXIES = {
11
+ browser: resolveProxy("browser-proxy"),
11
12
  computer: resolveProxy("computer-proxy"),
13
+ localComputer: resolveProxy("local-computer-proxy"),
12
14
  permission: resolveProxy("permission-proxy"),
13
15
  containerMcp: resolveProxy("container-mcp"),
14
16
  vpsContainerMcp: resolveProxy("vps-container-mcp"),
@@ -0,0 +1,103 @@
1
+ // Sign in with your email on a hosted server. The emailed code comes from the
2
+ // OpenMausBot control plane (the account service the desktop companion and
3
+ // `openmausbot login` already use), and this server decides who is welcome
4
+ // with an allow-list its owner controls. The result is an ordinary local
5
+ // session, the same thing a pairing code produces, so every gate applies.
6
+ //
7
+ // Why through the control plane rather than a mail provider per server: a
8
+ // self-hoster then needs no email credentials at all; the code arrives from
9
+ // accounts.openmausbot.com. The exchange happens server-side, so a browser
10
+ // only ever talks to this server, and a server with an empty allow-list does
11
+ // not expose the routes.
12
+ import { resolveCompanionControlPlaneURL } from "../electron/companion-account-service.mjs";
13
+ import { ControlPlaneError, createControlPlaneClient, normalizeAccountEmail } from "../electron/control-plane-client.mjs";
14
+ export const ADMIN_EMAILS_ENV = "OMB_SIGNIN_EMAILS";
15
+ export const MEMBER_EMAILS_ENV = "OMB_SIGNIN_MEMBER_EMAILS";
16
+ /** Commas, spaces or newlines between entries; case does not matter. */
17
+ export function parseAllowList(value) {
18
+ return (value ?? "")
19
+ .split(/[,\s]+/)
20
+ .map((entry) => entry.trim().toLowerCase())
21
+ .filter(Boolean);
22
+ }
23
+ export function allowedScopes(rawEmail, list) {
24
+ const email = rawEmail.trim().toLowerCase();
25
+ if (!email.includes("@"))
26
+ return null;
27
+ const matches = (entry) => (entry.startsWith("@") ? email.endsWith(entry) && email.length > entry.length : entry === email);
28
+ if (list.admins.some(matches))
29
+ return ["admin", "client"];
30
+ if (list.members.some(matches))
31
+ return ["client"];
32
+ return null;
33
+ }
34
+ export function signInEnabled(list) {
35
+ return list.admins.length + list.members.length > 0;
36
+ }
37
+ const NOT_WELCOME = { ok: false, status: 403, error: "this email is not on this server's sign-in list; ask the server's owner to add it" };
38
+ export function createEmailSignIn(options) {
39
+ const env = options.env ?? process.env;
40
+ const allow = () => (typeof options.allow === "function" ? options.allow() : options.allow);
41
+ let client = options.client ?? null;
42
+ const controlPlane = () => {
43
+ if (client)
44
+ return client;
45
+ const url = resolveCompanionControlPlaneURL({ isPackaged: true, environment: env });
46
+ if (!url)
47
+ throw new Error("OMB_CONTROL_PLANE_URL is set but is not an https address");
48
+ client = createControlPlaneClient({ baseURL: url, fetchImpl: options.fetchImpl });
49
+ return client;
50
+ };
51
+ return {
52
+ enabled: () => signInEnabled(allow()),
53
+ async start(rawEmail) {
54
+ const email = normalizeAccountEmail(rawEmail);
55
+ if (!email)
56
+ return { ok: false, status: 400, error: "enter a valid email address" };
57
+ if (!allowedScopes(email, allow()))
58
+ return NOT_WELCOME;
59
+ try {
60
+ await controlPlane().requestOTP(email);
61
+ }
62
+ catch (error) {
63
+ return unavailable(error);
64
+ }
65
+ return { ok: true };
66
+ },
67
+ async verify(rawEmail, code) {
68
+ const email = normalizeAccountEmail(rawEmail);
69
+ if (!email || !allowedScopes(email, allow()))
70
+ return NOT_WELCOME;
71
+ let verified;
72
+ try {
73
+ verified = await controlPlane().verifyOTP(email, code);
74
+ }
75
+ catch (error) {
76
+ // The client refuses a malformed code before any request (status 0);
77
+ // the control plane's own "invalid_otp" is a wrong or expired code.
78
+ if (error instanceof ControlPlaneError && error.code === "invalid_otp" && !error.status) {
79
+ return { ok: false, status: 400, error: "enter the 8-digit code from the email" };
80
+ }
81
+ if (error instanceof ControlPlaneError && error.status === 429) {
82
+ return { ok: false, status: 429, error: "too many attempts; wait a minute and request a new code" };
83
+ }
84
+ if (error instanceof ControlPlaneError && (error.status === 400 || error.status === 401 || error.status === 403)) {
85
+ return { ok: false, status: 401, error: "that code is wrong or has expired; request a new one" };
86
+ }
87
+ return unavailable(error);
88
+ }
89
+ // The account session on the control plane has done its job.
90
+ void controlPlane()
91
+ .signOut(verified.accountToken)
92
+ .catch(() => undefined);
93
+ const scopes = allowedScopes(verified.user.email, allow());
94
+ if (!scopes)
95
+ return NOT_WELCOME;
96
+ return { ok: true, email: verified.user.email, userId: verified.user.id, scopes };
97
+ },
98
+ };
99
+ }
100
+ function unavailable(error) {
101
+ const detail = error instanceof ControlPlaneError ? `${error.code}${error.status ? ` (${error.status})` : ""}` : error instanceof Error ? error.message : String(error);
102
+ return { ok: false, status: 502, error: `the sign-in service could not be reached (${detail}); try again in a moment` };
103
+ }
@@ -27,6 +27,26 @@ const activePartials = new Set();
27
27
  const uploadLocks = new Map();
28
28
  let reservedAttachmentBytes = 0;
29
29
  const STREAM_RESERVATION_INCREMENT_BYTES = 1024 * 1024;
30
+ /** Cached total of on-disk bytes that count against the quota: committed
31
+ * attachments plus any partial file this process did not itself write (a
32
+ * crash leftover, until it is cleaned up). `null` means a fresh scan is needed
33
+ * after cold start or a cleanup failure. Every normal commit
34
+ * and delete after that updates it in place instead of rescanning, which is
35
+ * what makes reservation growth O(1) rather than O(directory size).
36
+ *
37
+ * This is single-process, in-memory state: it does not see files another
38
+ * process adds or removes in ATTACHMENTS_DIR. That is an accepted limit
39
+ * (the app runs one server per data dir; uploadLocks already assumes the
40
+ * same), not a design gap this fix covers. */
41
+ let committedAttachmentBytes = null;
42
+ // An async link can finish while another upload invalidates or refreshes the
43
+ // cache. Its bytes must not be added again if a scan may already include it.
44
+ let attachmentAccountingVersion = 0;
45
+ /** Force a fresh disk total and invalidate any in-flight commit's snapshot. */
46
+ function invalidateAttachmentAccounting() {
47
+ committedAttachmentBytes = null;
48
+ attachmentAccountingVersion += 1;
49
+ }
30
50
  /** Mimes the endpoint accepts, mapped to the extension stored on disk.
31
51
  * Sniffing is not attempted — a lie here only changes the filename. */
32
52
  const IMAGE_MIMES = {
@@ -88,7 +108,10 @@ export function validateAttachmentUploadId(value) {
88
108
  }
89
109
  return normalized;
90
110
  }
91
- function attachmentUsageAndCleanup(now = Date.now()) {
111
+ /** The one full directory walk. Run from cold or invalidated accounting,
112
+ * or from an explicit cleanup sweep — never
113
+ * from the per-reservation hot path, which is what made PERF-03 quadratic. */
114
+ function scanAttachmentUsageAndCleanup(now = Date.now()) {
92
115
  ensureAttachmentsDir();
93
116
  let bytes = 0;
94
117
  let removed = 0;
@@ -117,10 +140,62 @@ function attachmentUsageAndCleanup(now = Date.now()) {
117
140
  }
118
141
  return { bytes, removed };
119
142
  }
120
- /** Remove only abandoned temporary uploads. Active partials are protected
121
- * even if a very slow request crosses the age threshold. */
143
+ /** Return cached usage, initializing it from disk after cold start or
144
+ * invalidation. Reservations must use this rather than trusting a cold cache. */
145
+ function committedBytes() {
146
+ if (committedAttachmentBytes === null) {
147
+ committedAttachmentBytes = scanAttachmentUsageAndCleanup().bytes;
148
+ attachmentAccountingVersion += 1;
149
+ }
150
+ return committedAttachmentBytes;
151
+ }
152
+ /** A newly committed file (saveFile/saveImage success path only — never the
153
+ * idempotent-retry branches, which reuse bytes already counted) adds to the
154
+ * cache. If accounting changed during an async link, a scan may already have
155
+ * counted that file. Let the next reservation rescan instead of adding twice. */
156
+ function addCommittedBytes(delta, version = attachmentAccountingVersion) {
157
+ if (version !== attachmentAccountingVersion || committedAttachmentBytes === null) {
158
+ invalidateAttachmentAccounting();
159
+ return;
160
+ }
161
+ committedAttachmentBytes += delta;
162
+ }
163
+ /** Remove only abandoned temporary uploads, and refresh the cached usage
164
+ * total from the same fresh scan. Outside accounting recovery, this is the
165
+ * only full directory walk after start-up; nothing on the upload path calls it
166
+ * per byte or per chunk anymore, so callers that want stale partials
167
+ * reclaimed promptly should invoke it on their own schedule. */
122
168
  export function cleanupStaleAttachmentPartials(now = Date.now()) {
123
- return attachmentUsageAndCleanup(now).removed;
169
+ const { bytes, removed } = scanAttachmentUsageAndCleanup(now);
170
+ committedAttachmentBytes = bytes;
171
+ attachmentAccountingVersion += 1;
172
+ return removed;
173
+ }
174
+ /** Delete a committed attachment (e.g. a generated image whose owning
175
+ * message/turn was retired) and keep the quota cache in sync. Every caller
176
+ * outside this module that removes a file saveImage/saveFile created must
177
+ * route through here instead of a raw unlink, or the cache will overcount
178
+ * forever and eventually reject uploads that should fit. */
179
+ export function deleteAttachment(path) {
180
+ try {
181
+ // Only bother sizing it if the cache is already warm — if it isn't, the
182
+ // eventual first scan just won't see this file, which is correct.
183
+ const size = committedAttachmentBytes !== null ? statSync(path).size : 0;
184
+ unlinkSync(path);
185
+ if (committedAttachmentBytes !== null) {
186
+ committedAttachmentBytes = Math.max(0, committedAttachmentBytes - size);
187
+ }
188
+ }
189
+ catch {
190
+ // Already gone, or never existed — nothing to reconcile.
191
+ }
192
+ }
193
+ /** Test-only: force the next quota check to rescan the directory instead of
194
+ * trusting the cache. Needed because tests mutate ATTACHMENTS_DIR directly
195
+ * (truncateSync, rmSync, writeFileSync) to simulate quota states, which the
196
+ * cache can't observe. */
197
+ export function __resetAttachmentAccountingForTests() {
198
+ invalidateAttachmentAccounting();
124
199
  }
125
200
  /** A retry owns the same UUID namespace as the interrupted attempt. Once it
126
201
  * holds that UUID's in-process lock, any inactive partial with the prefix is
@@ -136,7 +211,14 @@ function cleanupAttachmentPartialsForUpload(uploadId) {
136
211
  if (activePartials.has(path))
137
212
  continue;
138
213
  try {
214
+ // Only bother sizing it if the cache is already warm — if it isn't,
215
+ // the eventual first scan runs after this unlink and simply never
216
+ // sees the file, so there is nothing to reconcile.
217
+ const size = committedAttachmentBytes !== null ? statSync(path).size : 0;
139
218
  unlinkSync(path);
219
+ if (committedAttachmentBytes !== null) {
220
+ committedAttachmentBytes = Math.max(0, committedAttachmentBytes - size);
221
+ }
140
222
  }
141
223
  catch {
142
224
  // A concurrent cleanup already removed the abandoned attempt.
@@ -152,7 +234,7 @@ class AttachmentReservation {
152
234
  this.reserveAtLeast(bytes, bytes);
153
235
  }
154
236
  reserveAtLeast(required, preferred) {
155
- const used = attachmentUsageAndCleanup().bytes;
237
+ const used = committedBytes();
156
238
  const available = ATTACHMENTS_MAX_BYTES - used - reservedAttachmentBytes;
157
239
  if (available < required) {
158
240
  throw statusError(507, `attachments storage is full (limit ${ATTACHMENTS_MAX_BYTES} bytes)`);
@@ -199,15 +281,34 @@ async function withUploadLock(uploadId, operation) {
199
281
  uploadLocks.delete(uploadId);
200
282
  }
201
283
  }
284
+ /** Every committed attachment name is `${uuid}${extension}` for one of these
285
+ * fixed extensions — saveFile/saveImage never write any other suffix — so
286
+ * whether a given upload ID is already committed, and under which content
287
+ * type, can be answered with a handful of direct stats instead of a scan
288
+ * over every file in the directory. */
289
+ const KNOWN_ATTACHMENT_EXTENSIONS = Array.from(new Set([...Object.values(IMAGE_MIMES), ...Object.values(FILE_MIMES)]));
290
+ /** Missing or unreadable candidates count as absent here; the atomic link
291
+ * still detects a collision if a file exists when an upload commits. */
292
+ function statSizeIfFile(path) {
293
+ try {
294
+ const stat = statSync(path);
295
+ return stat.isFile() ? stat.size : null;
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
301
+ /** Find an idempotent retry without listing the directory, rejecting an ID
302
+ * already committed under a different accepted content type. */
202
303
  function committedPathForUpload(uploadId, extension) {
203
304
  ensureAttachmentsDir();
204
- const exact = `${uploadId}${extension}`;
205
- for (const entry of readdirSync(ATTACHMENTS_DIR, { withFileTypes: true })) {
206
- if (!entry.isFile() || PARTIAL_NAME.test(entry.name))
305
+ const exactPath = join(ATTACHMENTS_DIR, `${uploadId}${extension}`);
306
+ if (statSizeIfFile(exactPath) !== null)
307
+ return exactPath;
308
+ for (const other of KNOWN_ATTACHMENT_EXTENSIONS) {
309
+ if (other === extension)
207
310
  continue;
208
- if (entry.name === exact)
209
- return join(ATTACHMENTS_DIR, entry.name);
210
- if (entry.name.startsWith(`${uploadId}.`)) {
311
+ if (statSizeIfFile(join(ATTACHMENTS_DIR, `${uploadId}${other}`)) !== null) {
211
312
  throw statusError(409, "uploadId was already used for another content type");
212
313
  }
213
314
  }
@@ -317,6 +418,7 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
317
418
  let file;
318
419
  let bytes = 0;
319
420
  let closed = false;
421
+ let partialCleanupFailed = false;
320
422
  try {
321
423
  file = await open(partialPath, "wx", 0o600);
322
424
  for await (const value of chunks) {
@@ -343,7 +445,9 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
343
445
  await file.close();
344
446
  closed = true;
345
447
  try {
448
+ const accountingVersion = attachmentAccountingVersion;
346
449
  await link(partialPath, path);
450
+ addCommittedBytes(bytes, accountingVersion);
347
451
  await unlink(partialPath);
348
452
  }
349
453
  catch (error) {
@@ -359,11 +463,15 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
359
463
  catch (error) {
360
464
  if (file && !closed)
361
465
  await file.close().catch(() => undefined);
362
- await unlink(partialPath).catch(() => undefined);
466
+ await unlink(partialPath).catch(() => { partialCleanupFailed = true; });
363
467
  throw error;
364
468
  }
365
469
  finally {
366
470
  activePartials.delete(partialPath);
471
+ // A leftover partial was excluded from the cache while active. Rescan
472
+ // after releasing it so a retry cannot subtract bytes never counted.
473
+ if (partialCleanupFailed)
474
+ invalidateAttachmentAccounting();
367
475
  reservation.release();
368
476
  }
369
477
  });
@@ -397,10 +505,12 @@ export function saveImage(bytes, mime, requestedUploadId) {
397
505
  const path = join(ATTACHMENTS_DIR, name);
398
506
  const partialPath = join(ATTACHMENTS_DIR, `.openmaus-upload-${id}-${randomUUID()}.partial`);
399
507
  activePartials.add(partialPath);
508
+ let partialCleanupFailed = false;
400
509
  try {
401
510
  writeFileSync(partialPath, bytes, { mode: 0o600, flag: "wx" });
402
511
  try {
403
512
  linkSync(partialPath, path);
513
+ addCommittedBytes(bytes.byteLength);
404
514
  }
405
515
  catch (error) {
406
516
  if (error.code !== "EEXIST")
@@ -418,12 +528,14 @@ export function saveImage(bytes, mime, requestedUploadId) {
418
528
  unlinkSync(partialPath);
419
529
  }
420
530
  catch {
421
- // The successful path already removed it, or the write never created it.
531
+ partialCleanupFailed = true;
422
532
  }
423
533
  throw error;
424
534
  }
425
535
  finally {
426
536
  activePartials.delete(partialPath);
537
+ if (partialCleanupFailed)
538
+ invalidateAttachmentAccounting();
427
539
  reservation.release();
428
540
  }
429
541
  }
@@ -1,37 +1,39 @@
1
1
  // The pinned agent-browser release the harness downloads for the bots'
2
2
  // browser (docs/plans/browser-engine.md). Digests were computed from the
3
- // official GitHub assets on 2026-09-06, except the explicitly versioned Windows
3
+ // official GitHub assets on 2026-09-08, except the explicitly versioned Windows
4
4
  // vendor build below. Update pins through a reviewed native end-to-end run.
5
5
  // The Dockerfile retains the official Linux version.
6
- export const AGENT_BROWSER_VERSION = "0.36.0";
6
+ export const AGENT_BROWSER_VERSION = "0.37.0";
7
7
  const RELEASES = new Map([
8
8
  [
9
9
  "darwin-arm64",
10
- { target: "darwin-arm64", asset: "agent-browser-darwin-arm64", sha256: "b2106ab39db0838e7b1772f7f26f760518de56d09053150c56f9dddf15af997d", bytes: 12363200 },
10
+ { target: "darwin-arm64", asset: "agent-browser-darwin-arm64", sha256: "da5a2b4ef7be8ba279b1258c542c33877f480b1951d0d94de607fc1528edd380", bytes: 12429360 },
11
11
  ],
12
12
  [
13
13
  "darwin-x64",
14
- { target: "darwin-x64", asset: "agent-browser-darwin-x64", sha256: "45d9ac061a7d72e61eaff905326e2e19365f4dadb12142ea2f2d76d84689c708", bytes: 13510280 },
14
+ { target: "darwin-x64", asset: "agent-browser-darwin-x64", sha256: "f402c96350ffd2adc68d0e5e4d83aa49cf7735fef29882a8866be36cc2fc67e9", bytes: 13588296 },
15
15
  ],
16
16
  [
17
17
  "linux-arm64",
18
- { target: "linux-arm64", asset: "agent-browser-linux-arm64", sha256: "aeb556addca3903601a433de1acad3ace1c9c61d170084bf58d875884599a990", bytes: 12442720 },
18
+ { target: "linux-arm64", asset: "agent-browser-linux-arm64", sha256: "0315a8c4f7bf167cc5fd5eeaea79009b0e6b5d2ae8b3057bf6f96be978483426", bytes: 12507840 },
19
19
  ],
20
20
  [
21
21
  "linux-musl-arm64",
22
- { target: "linux-musl-arm64", asset: "agent-browser-linux-musl-arm64", sha256: "1ca7e003c9cb185f174fc81e51a609db27c77e3bfe00a0edff60688f8cd14f88", bytes: 12297000 },
22
+ { target: "linux-musl-arm64", asset: "agent-browser-linux-musl-arm64", sha256: "db67c0e84e0668c052c2cbdca325c57f896e309c3723755dcdd65b700f8a2b52", bytes: 12369352 },
23
23
  ],
24
24
  [
25
25
  "linux-musl-x64",
26
- { target: "linux-musl-x64", asset: "agent-browser-linux-musl-x64", sha256: "a20cc2a5202a48f5820372803dedbcd5f556dff7a89421f1b0f2612962b10718", bytes: 13995728 },
26
+ { target: "linux-musl-x64", asset: "agent-browser-linux-musl-x64", sha256: "5cff3bc7b2486867aba2901f92c9ce6029bd2a37951630d9feec1c5b3fee402e", bytes: 14092888 },
27
27
  ],
28
28
  [
29
29
  "linux-x64",
30
- { target: "linux-x64", asset: "agent-browser-linux-x64", sha256: "56d15181e51e00213f907fcf39707cfc76bfa804ff20f5a9373661c73f96de5e", bytes: 14156776 },
30
+ { target: "linux-x64", asset: "agent-browser-linux-x64", sha256: "78e0c5a14a7fa1f3d1ae2acdbdcc94a047b435b998a8c505fcc49d7fa4935a49", bytes: 14253776 },
31
31
  ],
32
32
  [
33
33
  "win32-x64",
34
34
  {
35
+ // Upstream 0.37.0 still lacks PR #1781's Windows cold-start fix.
36
+ // Retain the native-verified revision until its replacement is tested.
35
37
  target: "win32-x64", version: "0.36.0-omb.1",
36
38
  asset: "agent-browser-win32-x64-0.36.0-omb.1.exe",
37
39
  url: "https://github.com/milind-soni/OpenMausBot/releases/download/browser-engine-v0.36.0-omb.1/agent-browser-win32-x64-0.36.0-omb.1.exe",