@bastani/atomic 0.9.28-alpha.4 → 0.9.28

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 (128) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +13 -0
  3. package/dist/builtin/intercom/README.md +3 -2
  4. package/dist/builtin/intercom/broker/broker.ts +90 -14
  5. package/dist/builtin/intercom/broker/paths.ts +4 -0
  6. package/dist/builtin/intercom/broker/socket-liveness.ts +32 -0
  7. package/dist/builtin/intercom/broker/socket-replacement-lock.ts +104 -0
  8. package/dist/builtin/intercom/broker/spawn.ts +108 -63
  9. package/dist/builtin/intercom/index.bundle.mjs +114 -61
  10. package/dist/builtin/intercom/package.json +1 -1
  11. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  12. package/dist/builtin/mcp/package.json +1 -1
  13. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  14. package/dist/builtin/subagents/package.json +1 -1
  15. package/dist/builtin/web-access/package.json +1 -1
  16. package/dist/builtin/workflows/CHANGELOG.md +41 -0
  17. package/dist/builtin/workflows/builtin/{chunk-aqv8ny38.js → chunk-9ywkdsa9.js} +1 -1
  18. package/dist/builtin/workflows/builtin/{chunk-p6jagvyf.js → chunk-bnz4knhy.js} +1 -1
  19. package/dist/builtin/workflows/builtin/{chunk-br4ac9a3.js → chunk-dx5sjaxe.js} +1 -1
  20. package/dist/builtin/workflows/builtin/{chunk-p3xmajmj.js → chunk-v4scsq3y.js} +41 -13
  21. package/dist/builtin/workflows/builtin/goal.js +2 -2
  22. package/dist/builtin/workflows/builtin/index.js +3 -3
  23. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  24. package/dist/builtin/workflows/package.json +1 -1
  25. package/dist/builtin/workflows/src/extension/index.bundle.mjs +38 -14
  26. package/dist/builtin/workflows/src/index.js +38 -14
  27. package/dist/cli/startup-ui.d.ts +3 -2
  28. package/dist/cli/startup-ui.d.ts.map +1 -1
  29. package/dist/cli/startup-ui.js +19 -4
  30. package/dist/cli/startup-ui.js.map +1 -1
  31. package/dist/core/agent-session-runtime.d.ts +5 -1
  32. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  33. package/dist/core/agent-session-runtime.js +13 -9
  34. package/dist/core/agent-session-runtime.js.map +1 -1
  35. package/dist/core/extensions/session-events.d.ts +2 -0
  36. package/dist/core/extensions/session-events.d.ts.map +1 -1
  37. package/dist/core/extensions/session-events.js.map +1 -1
  38. package/dist/core/provider-alias.d.ts.map +1 -1
  39. package/dist/core/provider-alias.js +8 -0
  40. package/dist/core/provider-alias.js.map +1 -1
  41. package/dist/core/provider-composer-internal.d.ts.map +1 -1
  42. package/dist/core/provider-composer-internal.js +6 -3
  43. package/dist/core/provider-composer-internal.js.map +1 -1
  44. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  45. package/dist/core/resource-loader-reload.js +1 -0
  46. package/dist/core/resource-loader-reload.js.map +1 -1
  47. package/dist/core/resource-loader-types.d.ts +1 -0
  48. package/dist/core/resource-loader-types.d.ts.map +1 -1
  49. package/dist/core/resource-loader-types.js.map +1 -1
  50. package/dist/extensions/herdr/index.d.ts.map +1 -1
  51. package/dist/extensions/herdr/index.js +20 -2
  52. package/dist/extensions/herdr/index.js.map +1 -1
  53. package/dist/extensions/herdr/pane-owner.d.ts +7 -0
  54. package/dist/extensions/herdr/pane-owner.d.ts.map +1 -1
  55. package/dist/extensions/herdr/pane-owner.js +53 -11
  56. package/dist/extensions/herdr/pane-owner.js.map +1 -1
  57. package/dist/main-deferred-startup.d.ts +8 -0
  58. package/dist/main-deferred-startup.d.ts.map +1 -1
  59. package/dist/main-deferred-startup.js +6 -0
  60. package/dist/main-deferred-startup.js.map +1 -1
  61. package/dist/main.d.ts.map +1 -1
  62. package/dist/main.js +11 -3
  63. package/dist/main.js.map +1 -1
  64. package/dist/modes/interactive/interactive-auth-routing.js +2 -2
  65. package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
  66. package/dist/modes/interactive/interactive-extension-dialogs.js +8 -0
  67. package/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
  68. package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
  69. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  70. package/dist/modes/interactive/interactive-mode-base.js +1 -0
  71. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  72. package/dist/modes/interactive/interactive-mode-helpers.js +1 -1
  73. package/dist/modes/interactive/interactive-mode-helpers.js.map +1 -1
  74. package/dist/modes/interactive/interactive-mode-types.d.ts +1 -0
  75. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  76. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  77. package/dist/modes/interactive/interactive-process-lifecycle.js +1 -1
  78. package/dist/modes/interactive/interactive-process-lifecycle.js.map +1 -1
  79. package/dist/modes/interactive/interactive-startup-trust.d.ts +6 -0
  80. package/dist/modes/interactive/interactive-startup-trust.d.ts.map +1 -0
  81. package/dist/modes/interactive/interactive-startup-trust.js +15 -0
  82. package/dist/modes/interactive/interactive-startup-trust.js.map +1 -0
  83. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  84. package/dist/modes/interactive/interactive-startup.js +3 -0
  85. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  86. package/dist/modes/interactive-engine/engine-child-liveness.d.ts +1 -0
  87. package/dist/modes/interactive-engine/engine-child-liveness.d.ts.map +1 -1
  88. package/dist/modes/interactive-engine/engine-child-liveness.js +15 -1
  89. package/dist/modes/interactive-engine/engine-child-liveness.js.map +1 -1
  90. package/dist/modes/interactive-engine/engine-monitor.d.ts +4 -0
  91. package/dist/modes/interactive-engine/engine-monitor.d.ts.map +1 -1
  92. package/dist/modes/interactive-engine/engine-monitor.js +17 -0
  93. package/dist/modes/interactive-engine/engine-monitor.js.map +1 -1
  94. package/dist/modes/interactive-engine/engine-quit-intent.d.ts +16 -0
  95. package/dist/modes/interactive-engine/engine-quit-intent.d.ts.map +1 -0
  96. package/dist/modes/interactive-engine/engine-quit-intent.js +27 -0
  97. package/dist/modes/interactive-engine/engine-quit-intent.js.map +1 -0
  98. package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +1 -0
  99. package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
  100. package/dist/modes/interactive-engine/extension-ui-bridge.js +5 -0
  101. package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
  102. package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -1
  103. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  104. package/dist/modes/interactive-engine/isolated-runtime.js +19 -2
  105. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  106. package/dist/modes/interactive-engine/protocol.d.ts +18 -1
  107. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  108. package/dist/modes/interactive-engine/protocol.js +6 -0
  109. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  110. package/dist/modes/rpc/rpc-client.d.ts +11 -0
  111. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  112. package/dist/modes/rpc/rpc-client.js +42 -0
  113. package/dist/modes/rpc/rpc-client.js.map +1 -1
  114. package/dist/modes/rpc/rpc-input-scheduler.js +2 -2
  115. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  116. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  117. package/dist/modes/rpc/rpc-mode.js +23 -6
  118. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  119. package/dist/modes/rpc/rpc-provider-auth.js +1 -1
  120. package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
  121. package/docs/extensions/events.md +3 -0
  122. package/docs/herdr.md +11 -2
  123. package/docs/models.md +1 -1
  124. package/docs/providers.md +2 -0
  125. package/docs/sdk.md +1 -1
  126. package/docs/settings.md +1 -1
  127. package/npm-shrinkwrap.json +32 -32
  128. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "child_process";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync } from "fs";
2
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { createRequire } from "module";
6
- import net from "net";
6
+ import { isSocketAnswering } from "./socket-liveness.js";
7
7
  import { createChildProcessEnvironment, isBunBinary } from "@bastani/atomic";
8
8
  import {
9
9
  getBrokerLogPath,
@@ -57,6 +57,12 @@ export const BROKER_POLL_INITIAL_INTERVAL_MS = 10;
57
57
  /** Upper bound for the readiness poll interval, matching the historic flat poll. */
58
58
  export const BROKER_POLL_MAX_INTERVAL_MS = 100;
59
59
 
60
+ /** How long one spawn attempt waits for the broker socket to answer. */
61
+ export const BROKER_READY_TIMEOUT_MS = 5000;
62
+
63
+ /** Backstop only: a spawn lock normally ends when its broker is ready or the processes it names are gone. */
64
+ export const BROKER_SPAWN_LOCK_MAX_AGE_MS = 60_000;
65
+
60
66
  type BrokerRuntime = "node" | "bun-source" | "bun-binary";
61
67
 
62
68
  export const INTERNAL_INTERCOM_BROKER_ARG = "--atomic-internal-intercom-broker";
@@ -257,10 +263,20 @@ export function getBrokerSpawnOptions(
257
263
  };
258
264
  }
259
265
 
260
- /** Truncate (or create) the broker log so each spawn starts from a bounded, current file. */
261
- function resetBrokerLog(logPath: string = BROKER_LOG): void {
266
+ /**
267
+ * Truncate (or create) the broker log so each spawn starts from a bounded, current file. Returns false when a
268
+ * Windows broker that is still starting holds the log through its launcher's stderr redirect.
269
+ */
270
+ function resetBrokerLog(logPath: string = BROKER_LOG): boolean {
262
271
  ensurePrivateDirectory(dirname(logPath));
263
- closeSync(openSync(logPath, "w"));
272
+ try {
273
+ closeSync(openSync(logPath, "w"));
274
+ return true;
275
+ } catch (error) {
276
+ const code = (error as NodeJS.ErrnoException).code;
277
+ if (process.platform === "win32" && (code === "EBUSY" || code === "EPERM")) return false;
278
+ throw error;
279
+ }
264
280
  }
265
281
 
266
282
  /** Read at most {@link BROKER_LOG_TAIL_BYTES} trailing bytes of the broker log. */
@@ -299,19 +315,24 @@ function toError(error: unknown): Error {
299
315
  return error instanceof Error ? error : new Error(String(error));
300
316
  }
301
317
 
302
- export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: string[]): Promise<void> {
318
+ export async function spawnBrokerIfNeeded(
319
+ brokerCommand: string,
320
+ brokerArgs: string[],
321
+ readyTimeoutMs: number = BROKER_READY_TIMEOUT_MS,
322
+ ): Promise<void> {
303
323
  ensurePrivateDirectory(INTERCOM_DIR);
304
324
 
305
325
  if (await isBrokerRunning()) {
306
326
  return;
307
327
  }
308
328
 
309
- const ownsLock = acquireSpawnLock();
310
- if (!ownsLock) {
311
- await waitForBroker();
329
+ const lockOwner = acquireSpawnLock();
330
+ if (lockOwner === undefined) {
331
+ await waitForBroker(readyTimeoutMs);
312
332
  return;
313
333
  }
314
334
 
335
+ let keepLock = false;
315
336
  try {
316
337
  if (await isBrokerRunning()) {
317
338
  return;
@@ -323,7 +344,10 @@ export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: str
323
344
  writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
324
345
  }
325
346
  // Reset before spawning either way: the Windows launcher appends to this same path.
326
- resetBrokerLog();
347
+ if (!resetBrokerLog()) {
348
+ await waitForBroker(readyTimeoutMs);
349
+ return;
350
+ }
327
351
  // The Windows launcher redirects the broker's own stderr, so only the direct spawn
328
352
  // needs the descriptor. Node duplicates it during spawn, so the parent copy is closed
329
353
  // immediately afterwards rather than being held open for the broker's lifetime.
@@ -358,23 +382,38 @@ export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: str
358
382
  reject(new Error(`Intercom broker exited before startup with signal ${signal}\n${describeBrokerLog()}`));
359
383
  return;
360
384
  }
361
- reject(
362
- new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}\n${describeBrokerLog()}`),
363
- );
385
+ const exitedBeforeStartup = () =>
386
+ new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}\n${describeBrokerLog()}`);
387
+ if (code === 0) {
388
+ // A broker that finds a live broker on the socket yields with a clean exit.
389
+ void checkSocketConnectable().then((answering) => (answering ? resolve() : reject(exitedBeforeStartup())));
390
+ return;
391
+ }
392
+ reject(exitedBeforeStartup());
364
393
  };
365
394
 
366
395
  child.once("error", onError);
367
396
  child.once("exit", onExit);
368
- waitForBroker().then(() => {
397
+ waitForBroker(readyTimeoutMs).then(() => {
369
398
  cleanup();
370
399
  resolve();
371
400
  }, (error) => {
372
401
  cleanup();
402
+ // A broker still starting keeps the lock, so later spawners wait for it instead of starting another.
403
+ // Only now does the lock name the broker: until this hand-off it follows this live spawner, so no other
404
+ // spawner can judge it stale and replace it while this one may still release it.
405
+ // The Windows launcher hides the broker's pid, so its lock could only follow this live spawner and would
406
+ // block every later spawn; there the broker's own live-socket check is what prevents a takeover.
407
+ keepLock =
408
+ launch.kind === "direct" &&
409
+ child.exitCode === null &&
410
+ child.signalCode === null &&
411
+ handSpawnLockToBroker(child.pid, lockOwner);
373
412
  reject(toError(error));
374
413
  });
375
414
  });
376
415
  } finally {
377
- releaseSpawnLock();
416
+ if (!keepLock) releaseSpawnLock(lockOwner);
378
417
  }
379
418
  }
380
419
 
@@ -397,39 +436,37 @@ async function isBrokerRunning(): Promise<boolean> {
397
436
  }
398
437
 
399
438
  function checkSocketConnectable(): Promise<boolean> {
400
- return new Promise((resolve) => {
401
- const socket = net.connect(BROKER_SOCKET);
402
- const finish = (isConnected: boolean) => {
403
- clearTimeout(timeout);
404
- socket.off("connect", onConnect);
405
- socket.off("error", onError);
406
- resolve(isConnected);
407
- };
408
- const onConnect = () => {
409
- // end() starts a half-close; a reset can still arrive before close. Keep a handler until then.
410
- socket.on("error", () => {});
411
- socket.end();
412
- finish(true);
413
- };
414
- const onError = () => {
415
- socket.destroy();
416
- finish(false);
417
- };
418
- socket.on("connect", onConnect);
419
- socket.on("error", onError);
420
- const timeout = setTimeout(() => {
421
- socket.destroy();
422
- finish(false);
423
- }, 1000);
424
- });
439
+ return isSocketAnswering(BROKER_SOCKET);
440
+ }
441
+
442
+ /** Records the still-starting broker on the lock so staleness follows that process instead of the spawner. */
443
+ function handSpawnLockToBroker(pid: number | undefined, owner: string): boolean {
444
+ if (pid === undefined || !ownsSpawnLock(owner)) return false;
445
+ try {
446
+ appendFileSync(BROKER_SPAWN_LOCK, `${pid}\n`);
447
+ return true;
448
+ } catch {
449
+ // The startup error must still reach the caller; an unrecorded lock is released like any other.
450
+ return false;
451
+ }
425
452
  }
426
453
 
427
- function acquireSpawnLock(): boolean {
454
+ function ownsSpawnLock(owner: string, lockPath: string = BROKER_SPAWN_LOCK): boolean {
455
+ try {
456
+ return readFileSync(lockPath, "utf-8").startsWith(owner);
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ /** Returns the header this spawner wrote, which proves ownership on release, or undefined when another spawner holds the lock. */
463
+ function acquireSpawnLock(): string | undefined {
428
464
  const maxRetries = 5;
429
465
  for (let attempt = 0; attempt < maxRetries; attempt++) {
466
+ const owner = `${process.pid}\n${Date.now()}\n`;
430
467
  try {
431
- writeFileSync(BROKER_SPAWN_LOCK, `${process.pid}\n${Date.now()}\n`, { flag: "wx" });
432
- return true;
468
+ writeFileSync(BROKER_SPAWN_LOCK, owner, { flag: "wx" });
469
+ return owner;
433
470
  } catch (error) {
434
471
  if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") {
435
472
  throw error;
@@ -442,42 +479,50 @@ function acquireSpawnLock(): boolean {
442
479
  }
443
480
  continue;
444
481
  }
445
- return false;
482
+ return undefined;
446
483
  }
447
484
  }
448
- return false;
485
+ return undefined;
486
+ }
487
+
488
+ function isProcessAlive(pid: number): boolean {
489
+ try {
490
+ process.kill(pid, 0);
491
+ return true;
492
+ } catch (error) {
493
+ return (error as NodeJS.ErrnoException).code === "EPERM";
494
+ }
449
495
  }
450
496
 
451
- function isSpawnLockStale(): boolean {
452
- if (!existsSync(BROKER_SPAWN_LOCK)) {
497
+ /**
498
+ * A lock is stale once the broker it recorded is gone, or, while no broker is recorded, once its
499
+ * spawner is gone. A live broker keeps the lock even if its spawner died, until the age backstop.
500
+ */
501
+ export function isSpawnLockStale(lockPath: string = BROKER_SPAWN_LOCK, nowMs: number = Date.now()): boolean {
502
+ if (!existsSync(lockPath)) {
453
503
  return false;
454
504
  }
455
505
 
456
506
  try {
457
- const [pidLine = "", createdAtLine = "0"] = readFileSync(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
458
- const pid = Number.parseInt(pidLine, 10);
507
+ const [spawnerLine = "", createdAtLine = "", brokerLine = ""] = readFileSync(lockPath, "utf-8").trim().split("\n");
508
+ const spawnerPid = Number.parseInt(spawnerLine, 10);
459
509
  const createdAt = Number.parseInt(createdAtLine, 10);
460
- const ageMs = Date.now() - createdAt;
461
-
462
- if (Number.isFinite(pid)) {
463
- try {
464
- process.kill(pid, 0);
465
- } catch {
466
- // The process that created the lock is gone.
467
- return true;
468
- }
469
- }
510
+ const brokerPid = Number.parseInt(brokerLine, 10);
470
511
 
471
- return !Number.isFinite(createdAt) || ageMs > 10_000;
512
+ if (!Number.isFinite(spawnerPid) || !Number.isFinite(createdAt)) return true;
513
+ if (nowMs - createdAt > BROKER_SPAWN_LOCK_MAX_AGE_MS) return true;
514
+ return !isProcessAlive(Number.isFinite(brokerPid) ? brokerPid : spawnerPid);
472
515
  } catch {
473
516
  // Unreadable lock contents are treated as stale so a new broker can start.
474
517
  return true;
475
518
  }
476
519
  }
477
520
 
478
- function releaseSpawnLock(): void {
521
+ /** Removes the lock only while it is still the one `owner` wrote; a spawner that replaced a stale lock keeps its own. */
522
+ export function releaseSpawnLock(owner: string, lockPath: string = BROKER_SPAWN_LOCK): void {
523
+ if (!ownsSpawnLock(owner, lockPath)) return;
479
524
  try {
480
- unlinkSync(BROKER_SPAWN_LOCK);
525
+ unlinkSync(lockPath);
481
526
  } catch {
482
527
  // Another cleanup path may already have removed the lock.
483
528
  }
@@ -488,7 +533,7 @@ function releaseSpawnLock(): void {
488
533
  * Startup is tens of milliseconds, so the previous flat 100 ms sleep dominated it; the
489
534
  * overall timeout semantics are unchanged.
490
535
  */
491
- export async function waitForBroker(timeoutMs = 5000): Promise<void> {
536
+ export async function waitForBroker(timeoutMs = BROKER_READY_TIMEOUT_MS): Promise<void> {
492
537
  const start = Date.now();
493
538
  let interval = BROKER_POLL_INITIAL_INTERVAL_MS;
494
539
  while (Date.now() - start < timeoutMs) {
@@ -1406,13 +1406,43 @@ var init_client = __esm(() => {
1406
1406
  };
1407
1407
  });
1408
1408
 
1409
+ // dist/builtin/intercom/broker/socket-liveness.ts
1410
+ import net2 from "net";
1411
+ function isSocketAnswering(socketPath, timeoutMs = SOCKET_PROBE_TIMEOUT_MS) {
1412
+ return new Promise((resolve) => {
1413
+ const socket = net2.connect(socketPath);
1414
+ const finish = (isConnected) => {
1415
+ clearTimeout(timeout);
1416
+ socket.off("connect", onConnect);
1417
+ socket.off("error", onError);
1418
+ resolve(isConnected);
1419
+ };
1420
+ const onConnect = () => {
1421
+ socket.on("error", () => {});
1422
+ socket.end();
1423
+ finish(true);
1424
+ };
1425
+ const onError = () => {
1426
+ socket.destroy();
1427
+ finish(false);
1428
+ };
1429
+ socket.on("connect", onConnect);
1430
+ socket.on("error", onError);
1431
+ const timeout = setTimeout(() => {
1432
+ socket.destroy();
1433
+ finish(false);
1434
+ }, timeoutMs);
1435
+ });
1436
+ }
1437
+ var SOCKET_PROBE_TIMEOUT_MS = 1000;
1438
+ var init_socket_liveness = () => {};
1439
+
1409
1440
  // dist/builtin/intercom/broker/spawn.ts
1410
1441
  import { spawn } from "child_process";
1411
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync } from "fs";
1442
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync } from "fs";
1412
1443
  import { join as join2, dirname } from "path";
1413
1444
  import { fileURLToPath } from "url";
1414
1445
  import { createRequire } from "module";
1415
- import net2 from "net";
1416
1446
  import { createChildProcessEnvironment, isBunBinary } from "@bastani/atomic";
1417
1447
  function isIntercomPackageRoot(dir) {
1418
1448
  return existsSync(join2(dir, "package.json")) && existsSync(join2(dir, "broker"));
@@ -1537,7 +1567,15 @@ function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, stderr = "ignore")
1537
1567
  }
1538
1568
  function resetBrokerLog(logPath = BROKER_LOG) {
1539
1569
  ensurePrivateDirectory(dirname(logPath));
1540
- closeSync(openSync(logPath, "w"));
1570
+ try {
1571
+ closeSync(openSync(logPath, "w"));
1572
+ return true;
1573
+ } catch (error) {
1574
+ const code = error.code;
1575
+ if (process.platform === "win32" && (code === "EBUSY" || code === "EPERM"))
1576
+ return false;
1577
+ throw error;
1578
+ }
1541
1579
  }
1542
1580
  function readBrokerLogTail(logPath = BROKER_LOG, maxBytes = BROKER_LOG_TAIL_BYTES) {
1543
1581
  let fd;
@@ -1571,16 +1609,17 @@ ${tail}`;
1571
1609
  function toError2(error) {
1572
1610
  return error instanceof Error ? error : new Error(String(error));
1573
1611
  }
1574
- async function spawnBrokerIfNeeded(brokerCommand, brokerArgs) {
1612
+ async function spawnBrokerIfNeeded(brokerCommand, brokerArgs, readyTimeoutMs = BROKER_READY_TIMEOUT_MS) {
1575
1613
  ensurePrivateDirectory(INTERCOM_DIR);
1576
1614
  if (await isBrokerRunning()) {
1577
1615
  return;
1578
1616
  }
1579
- const ownsLock = acquireSpawnLock();
1580
- if (!ownsLock) {
1581
- await waitForBroker();
1617
+ const lockOwner = acquireSpawnLock();
1618
+ if (lockOwner === undefined) {
1619
+ await waitForBroker(readyTimeoutMs);
1582
1620
  return;
1583
1621
  }
1622
+ let keepLock = false;
1584
1623
  try {
1585
1624
  if (await isBrokerRunning()) {
1586
1625
  return;
@@ -1590,7 +1629,10 @@ async function spawnBrokerIfNeeded(brokerCommand, brokerArgs) {
1590
1629
  if (launch.kind === "windows-launcher") {
1591
1630
  writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
1592
1631
  }
1593
- resetBrokerLog();
1632
+ if (!resetBrokerLog()) {
1633
+ await waitForBroker(readyTimeoutMs);
1634
+ return;
1635
+ }
1594
1636
  const logFd = launch.kind === "direct" ? openSync(BROKER_LOG, "a") : undefined;
1595
1637
  let child;
1596
1638
  try {
@@ -1620,21 +1662,28 @@ ${describeBrokerLog()}`, { cause: error }));
1620
1662
  ${describeBrokerLog()}`));
1621
1663
  return;
1622
1664
  }
1623
- reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}
1624
- ${describeBrokerLog()}`));
1665
+ const exitedBeforeStartup = () => new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}
1666
+ ${describeBrokerLog()}`);
1667
+ if (code === 0) {
1668
+ checkSocketConnectable().then((answering) => answering ? resolve() : reject(exitedBeforeStartup()));
1669
+ return;
1670
+ }
1671
+ reject(exitedBeforeStartup());
1625
1672
  };
1626
1673
  child.once("error", onError);
1627
1674
  child.once("exit", onExit);
1628
- waitForBroker().then(() => {
1675
+ waitForBroker(readyTimeoutMs).then(() => {
1629
1676
  cleanup();
1630
1677
  resolve();
1631
1678
  }, (error) => {
1632
1679
  cleanup();
1680
+ keepLock = launch.kind === "direct" && child.exitCode === null && child.signalCode === null && handSpawnLockToBroker(child.pid, lockOwner);
1633
1681
  reject(toError2(error));
1634
1682
  });
1635
1683
  });
1636
1684
  } finally {
1637
- releaseSpawnLock();
1685
+ if (!keepLock)
1686
+ releaseSpawnLock(lockOwner);
1638
1687
  }
1639
1688
  }
1640
1689
  async function isBrokerRunning() {
@@ -1654,39 +1703,35 @@ async function isBrokerRunning() {
1654
1703
  }
1655
1704
  }
1656
1705
  function checkSocketConnectable() {
1657
- return new Promise((resolve) => {
1658
- const socket = net2.connect(BROKER_SOCKET2);
1659
- const finish = (isConnected) => {
1660
- clearTimeout(timeout);
1661
- socket.off("connect", onConnect);
1662
- socket.off("error", onError);
1663
- resolve(isConnected);
1664
- };
1665
- const onConnect = () => {
1666
- socket.on("error", () => {});
1667
- socket.end();
1668
- finish(true);
1669
- };
1670
- const onError = () => {
1671
- socket.destroy();
1672
- finish(false);
1673
- };
1674
- socket.on("connect", onConnect);
1675
- socket.on("error", onError);
1676
- const timeout = setTimeout(() => {
1677
- socket.destroy();
1678
- finish(false);
1679
- }, 1000);
1680
- });
1706
+ return isSocketAnswering(BROKER_SOCKET2);
1707
+ }
1708
+ function handSpawnLockToBroker(pid, owner) {
1709
+ if (pid === undefined || !ownsSpawnLock(owner))
1710
+ return false;
1711
+ try {
1712
+ appendFileSync(BROKER_SPAWN_LOCK, `${pid}
1713
+ `);
1714
+ return true;
1715
+ } catch {
1716
+ return false;
1717
+ }
1718
+ }
1719
+ function ownsSpawnLock(owner, lockPath = BROKER_SPAWN_LOCK) {
1720
+ try {
1721
+ return readFileSync(lockPath, "utf-8").startsWith(owner);
1722
+ } catch {
1723
+ return false;
1724
+ }
1681
1725
  }
1682
1726
  function acquireSpawnLock() {
1683
1727
  const maxRetries = 5;
1684
1728
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1685
- try {
1686
- writeFileSync(BROKER_SPAWN_LOCK, `${process.pid}
1729
+ const owner = `${process.pid}
1687
1730
  ${Date.now()}
1688
- `, { flag: "wx" });
1689
- return true;
1731
+ `;
1732
+ try {
1733
+ writeFileSync(BROKER_SPAWN_LOCK, owner, { flag: "wx" });
1734
+ return owner;
1690
1735
  } catch (error) {
1691
1736
  if (!(error instanceof Error) || error.code !== "EEXIST") {
1692
1737
  throw error;
@@ -1697,39 +1742,46 @@ ${Date.now()}
1697
1742
  } catch {}
1698
1743
  continue;
1699
1744
  }
1700
- return false;
1745
+ return;
1701
1746
  }
1702
1747
  }
1703
- return false;
1748
+ return;
1704
1749
  }
1705
- function isSpawnLockStale() {
1706
- if (!existsSync(BROKER_SPAWN_LOCK)) {
1750
+ function isProcessAlive(pid) {
1751
+ try {
1752
+ process.kill(pid, 0);
1753
+ return true;
1754
+ } catch (error) {
1755
+ return error.code === "EPERM";
1756
+ }
1757
+ }
1758
+ function isSpawnLockStale(lockPath = BROKER_SPAWN_LOCK, nowMs = Date.now()) {
1759
+ if (!existsSync(lockPath)) {
1707
1760
  return false;
1708
1761
  }
1709
1762
  try {
1710
- const [pidLine = "", createdAtLine = "0"] = readFileSync(BROKER_SPAWN_LOCK, "utf-8").trim().split(`
1763
+ const [spawnerLine = "", createdAtLine = "", brokerLine = ""] = readFileSync(lockPath, "utf-8").trim().split(`
1711
1764
  `);
1712
- const pid = Number.parseInt(pidLine, 10);
1765
+ const spawnerPid = Number.parseInt(spawnerLine, 10);
1713
1766
  const createdAt = Number.parseInt(createdAtLine, 10);
1714
- const ageMs = Date.now() - createdAt;
1715
- if (Number.isFinite(pid)) {
1716
- try {
1717
- process.kill(pid, 0);
1718
- } catch {
1719
- return true;
1720
- }
1721
- }
1722
- return !Number.isFinite(createdAt) || ageMs > 1e4;
1767
+ const brokerPid = Number.parseInt(brokerLine, 10);
1768
+ if (!Number.isFinite(spawnerPid) || !Number.isFinite(createdAt))
1769
+ return true;
1770
+ if (nowMs - createdAt > BROKER_SPAWN_LOCK_MAX_AGE_MS)
1771
+ return true;
1772
+ return !isProcessAlive(Number.isFinite(brokerPid) ? brokerPid : spawnerPid);
1723
1773
  } catch {
1724
1774
  return true;
1725
1775
  }
1726
1776
  }
1727
- function releaseSpawnLock() {
1777
+ function releaseSpawnLock(owner, lockPath = BROKER_SPAWN_LOCK) {
1778
+ if (!ownsSpawnLock(owner, lockPath))
1779
+ return;
1728
1780
  try {
1729
- unlinkSync(BROKER_SPAWN_LOCK);
1781
+ unlinkSync(lockPath);
1730
1782
  } catch {}
1731
1783
  }
1732
- async function waitForBroker(timeoutMs = 5000) {
1784
+ async function waitForBroker(timeoutMs = BROKER_READY_TIMEOUT_MS) {
1733
1785
  const start = Date.now();
1734
1786
  let interval = BROKER_POLL_INITIAL_INTERVAL_MS;
1735
1787
  while (Date.now() - start < timeoutMs) {
@@ -1742,8 +1794,9 @@ async function waitForBroker(timeoutMs = 5000) {
1742
1794
  throw new Error(`Broker failed to start within timeout
1743
1795
  ${describeBrokerLog()}`);
1744
1796
  }
1745
- var INTERCOM_DIR, EXTENSION_DIR, BROKER_SOCKET2, BROKER_PID, BROKER_SPAWN_LOCK, BROKER_LOG, BROKER_LOG_TAIL_BYTES, BROKER_POLL_INITIAL_INTERVAL_MS = 10, BROKER_POLL_MAX_INTERVAL_MS = 100, INTERNAL_INTERCOM_BROKER_ARG = "--atomic-internal-intercom-broker";
1797
+ var INTERCOM_DIR, EXTENSION_DIR, BROKER_SOCKET2, BROKER_PID, BROKER_SPAWN_LOCK, BROKER_LOG, BROKER_LOG_TAIL_BYTES, BROKER_POLL_INITIAL_INTERVAL_MS = 10, BROKER_POLL_MAX_INTERVAL_MS = 100, BROKER_READY_TIMEOUT_MS = 5000, BROKER_SPAWN_LOCK_MAX_AGE_MS = 60000, INTERNAL_INTERCOM_BROKER_ARG = "--atomic-internal-intercom-broker";
1746
1798
  var init_spawn = __esm(() => {
1799
+ init_socket_liveness();
1747
1800
  init_paths();
1748
1801
  INTERCOM_DIR = getIntercomDirPath();
1749
1802
  EXTENSION_DIR = resolveIntercomPackageRoot(import.meta.url);
@@ -6393,7 +6446,7 @@ __export(exports_index_heavy, {
6393
6446
  default: () => piIntercomExtension
6394
6447
  });
6395
6448
  import { APP_NAME, sessionScopedExtensionState } from "@bastani/atomic";
6396
- import { appendFileSync } from "node:fs";
6449
+ import { appendFileSync as appendFileSync2 } from "node:fs";
6397
6450
  function isPendingStageUndeliverableEvent(value) {
6398
6451
  if (typeof value !== "object" || value === null)
6399
6452
  return false;
@@ -7352,7 +7405,7 @@ var init_index_heavy = __esm(() => {
7352
7405
  process.env.ATOMIC_INTERCOM_HEAVY_IMPORTED = "1";
7353
7406
  }
7354
7407
  if (process.env.ATOMIC_TEST_LAZY_IMPORT_SENTINEL_FILE) {
7355
- appendFileSync(process.env.ATOMIC_TEST_LAZY_IMPORT_SENTINEL_FILE, `intercom
7408
+ appendFileSync2(process.env.ATOMIC_TEST_LAZY_IMPORT_SENTINEL_FILE, `intercom
7356
7409
  `);
7357
7410
  }
7358
7411
  INTERCOM_SESSION_ID_ENV = `${APP_NAME.toUpperCase()}_INTERCOM_SESSION_ID`;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.28-alpha.4",
3
+ "version": "0.9.28",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.28] - 2026-10-08
11
+
12
+ ### Fixed
13
+
14
+ - Fixed MCP OAuth dynamic registration omitting `application_type`, which caused redirect URI rejections on OpenID Connect servers ([#10493](https://github.com/earendil-works/pi/issues/10493)).
15
+
10
16
  ## [0.9.28-alpha.1] - 2026-10-05
11
17
 
12
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.28-alpha.4",
3
+ "version": "0.9.28",
4
4
  "private": true,
5
5
  "description": "Atomic's pi-compatible MCP integration for server connections, OAuth, tools, resources, and terminal management.",
6
6
  "contributors": [
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.28] - 2026-10-08
6
+
7
+ ### Changed
8
+
9
+ - Updated the bundled Cua Driver skill to upstream `cua-driver-rs-v0.34.0`, with focused runtime, workflow, and visual guides covering authorization, exact targets, observation, and outcome verification.
10
+
5
11
  ## [0.9.28-alpha.4] - 2026-10-07
6
12
 
7
13
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.28-alpha.4",
3
+ "version": "0.9.28",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.28-alpha.4",
3
+ "version": "0.9.28",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [