@bastani/atomic 0.9.60 → 0.9.61-alpha.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.61] - 2026-11-02
6
+
7
+ A local worker pool that keeps going when one child or one worker goes wrong.
8
+
9
+ - **One failing child no longer takes down the pool:** a child that fails to dispatch fails alone instead of closing the whole local worker pool, and a child whose result is recorded keeps it whatever happens to the pool afterwards ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
10
+ - **Cleanup trouble is a warning, not a failure:** a worker or tool process group that survives SIGKILL shows as a warning on the root run; the worker is replaced and its child replays on another worker within its three attempts ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
11
+ - **Pool faults replay instead of failing:** a child that hits `stale_work_lease`, `worker_lost` or another pool fault replays from its last checkpoint, and fails with the retryable `worker_retry_limit` only after its third attempt ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
12
+
13
+ ### Fixed
14
+
15
+ - One failing child no longer takes down a local worker pool (`workflows.localWorkers`). Before, a single child that failed to dispatch, for example on a transient storage error, closed the whole pool. On a busy machine that close often could not confirm in time that a killed worker had exited, so every other child failed with the retryable `Local worker cleanup was not confirmed`. That included children whose work had already finished, and every child the root started afterwards, so a root that retries pooled children retried them in a tight loop until it gave up. Now that failure fails only that child; the pool closes only when the root ends or the pool itself fails. A child whose result is recorded keeps it whatever happens to the pool afterwards. A worker's exit after SIGKILL is confirmed from the operating system, not from a 5-second timer that a busy coordinator could miss. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
16
+ - Cleanup that a local worker pool really cannot confirm is no longer any child's failure. This covers a tool process group that survives SIGKILL and a worker that outlives it. Instead it shows as a warning on the root run and is written to that worker's log, which is kept. The worker is replaced, and its unfinished child replays on another worker within its usual three attempts. Process permits of the surviving groups stay quarantined, and closing the pool still reports them. Worker logs of a pool that failed are kept as well. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
17
+ - A child on a local worker whose run fails on a fault of the pool, such as `stale_work_lease`, `worker_lost`, `storage_unavailable` or `intercom_unavailable`, is replayed from its last checkpoint within its three attempts instead of ending failed with a recoverable fault recorded as final. After the third attempt it fails with the retryable `worker_retry_limit`, naming the fault. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
18
+ - A replayed child on a local worker whose `ctx.tool` call fails again, as it did on an earlier attempt, no longer fails with `Pool checkpoint identity conflict`. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
19
+
20
+ ## [0.9.61-alpha.1] - 2026-11-02
21
+
22
+ A local worker pool that keeps going when one child or one worker goes wrong.
23
+
24
+ - **One failing child no longer takes down the pool:** a child that fails to dispatch fails alone instead of closing the whole local worker pool, and a child whose result is recorded keeps it whatever happens to the pool afterwards ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
25
+ - **Cleanup trouble is a warning, not a failure:** a worker or tool process group that survives SIGKILL shows as a warning on the root run; the worker is replaced and its child replays on another worker within its three attempts ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
26
+ - **Pool faults replay instead of failing:** a child that hits `stale_work_lease`, `worker_lost` or another pool fault replays from its last checkpoint, and fails with the retryable `worker_retry_limit` only after its third attempt ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
27
+
28
+ ### Fixed
29
+
30
+ - One failing child no longer takes down a local worker pool (`workflows.localWorkers`). Before, a single child that failed to dispatch, for example on a transient storage error, closed the whole pool. On a busy machine that close often could not confirm in time that a killed worker had exited, so every other child failed with the retryable `Local worker cleanup was not confirmed`. That included children whose work had already finished, and every child the root started afterwards, so a root that retries pooled children retried them in a tight loop until it gave up. Now that failure fails only that child; the pool closes only when the root ends or the pool itself fails. A child whose result is recorded keeps it whatever happens to the pool afterwards. A worker's exit after SIGKILL is confirmed from the operating system, not from a 5-second timer that a busy coordinator could miss. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
31
+ - Cleanup that a local worker pool really cannot confirm is no longer any child's failure. This covers a tool process group that survives SIGKILL and a worker that outlives it. Instead it shows as a warning on the root run and is written to that worker's log, which is kept. The worker is replaced, and its unfinished child replays on another worker within its usual three attempts. Process permits of the surviving groups stay quarantined, and closing the pool still reports them. Worker logs of a pool that failed are kept as well. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
32
+ - A child on a local worker whose run fails on a fault of the pool, such as `stale_work_lease`, `worker_lost`, `storage_unavailable` or `intercom_unavailable`, is replayed from its last checkpoint within its three attempts instead of ending failed with a recoverable fault recorded as final. After the third attempt it fails with the retryable `worker_retry_limit`, naming the fault. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
33
+ - A replayed child on a local worker whose `ctx.tool` call fails again, as it did on an earlier attempt, no longer fails with `Pool checkpoint identity conflict`. ([#4159](https://github.com/bastani-inc/atomic/pull/4159))
34
+
5
35
  ## [0.9.60] - 2026-11-01
6
36
 
7
37
  Chats that keep working after a workflow wakes them, steadier subagent model choices, and copying that works inside Rex.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.60",
3
+ "version": "0.9.61-alpha.1",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.60",
3
+ "version": "0.9.61-alpha.1",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.60",
3
+ "version": "0.9.61-alpha.1",
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.60",
3
+ "version": "0.9.61-alpha.1",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.60",
3
+ "version": "0.9.61-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -20151,6 +20151,25 @@ function admissionProcessStopped(pid) {
20151
20151
  return error.code === "ESRCH";
20152
20152
  }
20153
20153
  }
20154
+ function workerProcessExited(pid, final = false) {
20155
+ if (admissionProcessStopped(pid))
20156
+ return true;
20157
+ if (process.platform === "linux") {
20158
+ try {
20159
+ const stat = readFileSync3(`/proc/${pid}/stat`, "utf8");
20160
+ return stat.slice(stat.lastIndexOf(")") + 2).startsWith("Z");
20161
+ } catch {
20162
+ return admissionProcessStopped(pid);
20163
+ }
20164
+ }
20165
+ if (!final)
20166
+ return false;
20167
+ try {
20168
+ return execFileSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8", timeout: 5000 }).trim().startsWith("Z");
20169
+ } catch {
20170
+ return admissionProcessStopped(pid);
20171
+ }
20172
+ }
20154
20173
  function scanProcessGroup(group) {
20155
20174
  const members = new Map;
20156
20175
  if (process.platform === "linux") {
@@ -62685,17 +62704,26 @@ function isSameCheckpointValue(stored, next) {
62685
62704
  const hash = (value2) => durableHash(value2);
62686
62705
  return hash(stored) === hash(next);
62687
62706
  }
62707
+ function withTimesOf(later, recorded) {
62708
+ const both = (key) => typeof recorded[key] === "number" && typeof later[key] === "number";
62709
+ return {
62710
+ ...later,
62711
+ ...both("startedAt") ? { startedAt: recorded.startedAt } : {},
62712
+ ...both("endedAt") ? { endedAt: recorded.endedAt } : {},
62713
+ ...both("completedAt") ? { completedAt: recorded.completedAt } : {}
62714
+ };
62715
+ }
62688
62716
  function isRecordedCheckpointValue(stored, next) {
62689
62717
  const recorded = stored;
62690
62718
  if (isSameCheckpointValue(recorded, next))
62691
62719
  return true;
62692
- const { completedAt, startedAt } = recorded;
62693
- if (typeof completedAt !== "number")
62720
+ if (typeof recorded.completedAt !== "number")
62694
62721
  return false;
62695
62722
  const later = next;
62696
- if (startedAt === undefined !== (later.startedAt === undefined))
62723
+ if (recorded.startedAt === undefined !== (later.startedAt === undefined))
62697
62724
  return false;
62698
- return isSameCheckpointValue({ ...later, completedAt, ...startedAt === undefined ? {} : { startedAt } }, recorded);
62725
+ const retimed = withTimesOf(later, recorded);
62726
+ return isSameCheckpointValue(recorded.topology !== undefined && later.topology !== undefined ? { ...retimed, topology: withTimesOf(later.topology, recorded.topology) } : retimed, recorded);
62699
62727
  }
62700
62728
  var MIRROR_FRESH_MS = 2000;
62701
62729
  var init_pool_remote_journal = __esm(() => {
@@ -63731,15 +63759,7 @@ class SqlPoolQueue {
63731
63759
  }
63732
63760
  }
63733
63761
  function isRecordedCheckpoint(stored, payload) {
63734
- if (stored === payload)
63735
- return true;
63736
- const { completedAt, startedAt } = JSON.parse(stored);
63737
- if (typeof completedAt !== "number")
63738
- return false;
63739
- const next = JSON.parse(payload);
63740
- if (startedAt === undefined !== (next.startedAt === undefined))
63741
- return false;
63742
- return serializeJson({ ...next, completedAt, ...startedAt === undefined ? {} : { startedAt } }) === stored;
63762
+ return stored === payload || isRecordedCheckpointValue(JSON.parse(stored), JSON.parse(payload));
63743
63763
  }
63744
63764
  function openSqlPoolQueue(journal, remote) {
63745
63765
  const storage = sqlPoolStorage(journal);
@@ -68268,7 +68288,7 @@ var init_startup_timeout = () => {};
68268
68288
  import { fork } from "node:child_process";
68269
68289
  import { randomUUID as randomUUID17 } from "node:crypto";
68270
68290
  import { createWriteStream } from "node:fs";
68271
- import { mkdir as mkdir6, rm as rm3, rmdir } from "node:fs/promises";
68291
+ import { appendFile, mkdir as mkdir6, rm as rm3, rmdir } from "node:fs/promises";
68272
68292
  import { dirname as dirname15, join as join30, resolve as resolve14 } from "node:path";
68273
68293
  import { setTimeout as delay2 } from "node:timers/promises";
68274
68294
  function isRootLeaseLost(error) {
@@ -68460,6 +68480,9 @@ class PoolCoordinator {
68460
68480
  incarnations = new Map;
68461
68481
  readyWorkers = new Set;
68462
68482
  logs = new Map;
68483
+ keptLogs = new Set;
68484
+ unconfirmedExits = new WeakSet;
68485
+ failed = false;
68463
68486
  cleanups = new Set;
68464
68487
  controlAcks = new Map;
68465
68488
  lastHeard = new Map;
@@ -68507,7 +68530,6 @@ class PoolCoordinator {
68507
68530
  nextRecoveryAt = 0;
68508
68531
  cleanupFailure;
68509
68532
  held = new Set;
68510
- quarantined = new Set;
68511
68533
  unreaped = [];
68512
68534
  retained = new Map;
68513
68535
  retainedTransitions = Promise.resolve();
@@ -68700,18 +68722,15 @@ class PoolCoordinator {
68700
68722
  try {
68701
68723
  const { survivors, unprobeable } = await reapProcessGroups(this.journal, this.root, { attempt: work });
68702
68724
  if (survivors.length > 0) {
68703
- this.quarantined.add(work.id);
68704
68725
  this.unreaped.push({ attempt: work });
68705
- this.port.send({ kind: "work-failure", workId: work.id, error: unconfirmedCleanup(survivors).message });
68706
- return;
68726
+ this.reportHealth(undefined, `${unconfirmedCleanup(survivors).message}; child ${work.childRunId} of an earlier incarnation replays`);
68707
68727
  }
68708
68728
  if (unprobeable.length === 0)
68709
68729
  await this.releaseRetained(work);
68710
68730
  } catch (error) {
68711
68731
  this.cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
68712
68732
  } finally {
68713
- if (!this.quarantined.has(work.id))
68714
- this.held.delete(work.id);
68733
+ this.held.delete(work.id);
68715
68734
  }
68716
68735
  }
68717
68736
  releaseRetained(work) {
@@ -68842,6 +68861,7 @@ class PoolCoordinator {
68842
68861
  });
68843
68862
  }
68844
68863
  fail(error) {
68864
+ this.failed = true;
68845
68865
  this.port.send({ kind: "failure", error: error.message });
68846
68866
  }
68847
68867
  failWork(work, error) {
@@ -69250,7 +69270,11 @@ class PoolCoordinator {
69250
69270
  terminate();
69251
69271
  });
69252
69272
  const closed = new Promise((resolve) => child.once("close", () => resolve()));
69253
- child.once("exit", () => {
69273
+ let retired = false;
69274
+ const retire = () => {
69275
+ if (retired)
69276
+ return;
69277
+ retired = true;
69254
69278
  Promise.race([closed, delay2(STDERR_DRAIN_GRACE_MS)]).then(() => reject(diagnosed(timedOut ? startupTimeoutMessage(startupTimeoutMs) : "Local worker exited before readiness")));
69255
69279
  this.children.delete(child);
69256
69280
  this.logs.delete(child);
@@ -69274,7 +69298,7 @@ class PoolCoordinator {
69274
69298
  const held = slot.work;
69275
69299
  if (held)
69276
69300
  this.held.add(held.id);
69277
- let childFailed = false;
69301
+ let unconfirmed;
69278
69302
  const cleanup = disconnected.then(() => serial).then(async () => {
69279
69303
  try {
69280
69304
  const admission = this.admission;
@@ -69287,18 +69311,13 @@ class PoolCoordinator {
69287
69311
  throw unconfirmedCleanup(alive);
69288
69312
  if (alive.length === 0)
69289
69313
  return;
69290
- const error = unconfirmedCleanup(alive);
69291
- if (held && await this.queue.result(this.root, held.id) === undefined) {
69292
- this.quarantined.add(held.id);
69293
- slot.work = undefined;
69294
- childFailed = true;
69295
- this.port.send({ kind: "work-failure", workId: held.id, error: error.message });
69296
- }
69314
+ unconfirmed = unconfirmedCleanup(alive);
69315
+ this.reportHealth(logPath, `${unconfirmed.message}; the worker was retired and replaced`);
69297
69316
  } catch (error) {
69298
69317
  this.cleanupFailure = error instanceof Error ? error : new Error(String(error));
69299
69318
  throw this.cleanupFailure;
69300
69319
  } finally {
69301
- if (held && !this.quarantined.has(held.id))
69320
+ if (held)
69302
69321
  this.held.delete(held.id);
69303
69322
  }
69304
69323
  }).then(async () => {
@@ -69319,8 +69338,8 @@ class PoolCoordinator {
69319
69338
  const settled = attempt !== undefined && await this.whileRootLate(queue, () => exhausted ? queue.failExited(lease, attempt) : queue.releaseExited(lease, attempt));
69320
69339
  const superseded = work !== undefined && !settled && await this.superseded(work);
69321
69340
  if (work !== undefined && exhausted && !superseded)
69322
- this.failWork(work, diagnosed(childRetryLimit(work, "lost its worker")));
69323
- if (work === undefined && !childFailed && ++this.replacements > 3)
69341
+ this.failWork(work, diagnosed(`${childRetryLimit(work, "lost its worker")}${unconfirmed === undefined ? "" : `; ${unconfirmed.message}`}`));
69342
+ if (work === undefined && ++this.replacements > 3)
69324
69343
  this.fail(diagnosed("Local worker retry limit"));
69325
69344
  else
69326
69345
  await this.spawn();
@@ -69329,7 +69348,9 @@ class PoolCoordinator {
69329
69348
  });
69330
69349
  this.cleanups.add(cleanup);
69331
69350
  cleanup.finally(() => this.cleanups.delete(cleanup));
69332
- });
69351
+ };
69352
+ slot.retire = retire;
69353
+ child.once("exit", retire);
69333
69354
  child.on("message", (message) => {
69334
69355
  this.observeResponsiveness();
69335
69356
  this.lastHeard.set(worker, { at: performance.now(), stalled: this.stalledMs });
@@ -69403,10 +69424,11 @@ class PoolCoordinator {
69403
69424
  const fenced = slot.work;
69404
69425
  slot.work = undefined;
69405
69426
  if (fenced && !await this.superseded(fenced)) {
69427
+ this.fence(fenced);
69406
69428
  if (this.attempts(fenced) < MAX_CHILD_ATTEMPTS)
69407
69429
  await leased(() => queue.releaseExited(lease, fenced));
69408
69430
  else if (await leased(() => queue.failExited(lease, fenced)))
69409
- this.failWork(fenced, new Error(childRetryLimit(fenced, "was refused by the root as stale on every attempt")));
69431
+ this.failWork(fenced, new Error(childRetryLimit(fenced, message.fault === undefined ? "was refused by the root as stale on every attempt" : `failed on a fault of the pool on every attempt (last: ${message.fault})`)));
69410
69432
  }
69411
69433
  return;
69412
69434
  }
@@ -69567,8 +69589,6 @@ class PoolCoordinator {
69567
69589
  return;
69568
69590
  }
69569
69591
  for (const work of expired) {
69570
- if (this.quarantined.has(work.id))
69571
- continue;
69572
69592
  if (this.held.has(work.id)) {
69573
69593
  await queue.extend(lease, work, leaseMs).catch((error) => {
69574
69594
  if (error.message !== STALE_WORK_LEASE)
@@ -69584,9 +69604,9 @@ class PoolCoordinator {
69584
69604
  });
69585
69605
  continue;
69586
69606
  }
69587
- if (child && !await this.stop(child).then(() => true, () => false))
69588
- continue;
69589
- if (this.held.has(work.id) || this.quarantined.has(work.id))
69607
+ if (child && !await this.stop(child))
69608
+ this.retireUnconfirmed(child);
69609
+ if (this.held.has(work.id))
69590
69610
  continue;
69591
69611
  if ((await unconfirmedProcessGroups(this.journal, root, { attempt: work })).length > 0)
69592
69612
  continue;
@@ -69613,17 +69633,49 @@ class PoolCoordinator {
69613
69633
  const heard = this.lastHeard.get(worker);
69614
69634
  return child.exitCode === null && child.signalCode === null && child.connected && heard !== undefined && performance.now() - heard.at - (this.stalledMs - heard.stalled) < this.options.leaseMs;
69615
69635
  }
69616
- stop(child) {
69617
- if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
69618
- return Promise.resolve();
69619
- return new Promise((resolve, reject) => {
69620
- const timeout = setTimeout(() => reject(new Error("Local worker cleanup was not confirmed")), 5000);
69621
- child.once("exit", () => {
69622
- clearTimeout(timeout);
69623
- resolve();
69624
- });
69625
- child.kill("SIGKILL");
69626
- });
69636
+ async stop(child) {
69637
+ const pid = child.pid;
69638
+ const exited = () => child.exitCode !== null || child.signalCode !== null;
69639
+ if (pid === undefined || exited())
69640
+ return true;
69641
+ const exit = new Promise((resolve) => child.once("exit", () => resolve()));
69642
+ const confirmed = async () => {
69643
+ await Promise.race([exit, delay2(WORKER_EXIT_CONFIRMATION_MS)]);
69644
+ return true;
69645
+ };
69646
+ this.observeResponsiveness();
69647
+ child.kill("SIGKILL");
69648
+ const since = performance.now();
69649
+ const stalledAt = this.stalledMs;
69650
+ for (;; ) {
69651
+ await Promise.race([exit, delay2(WORKER_EXIT_POLL_MS)]);
69652
+ if (exited())
69653
+ return true;
69654
+ if (workerProcessExited(pid))
69655
+ return confirmed();
69656
+ this.observeResponsiveness();
69657
+ if (performance.now() - since - (this.stalledMs - stalledAt) >= WORKER_EXIT_CONFIRMATION_MS)
69658
+ return exited() || workerProcessExited(pid, true) && confirmed();
69659
+ }
69660
+ }
69661
+ retireUnconfirmed(child) {
69662
+ if (this.unconfirmedExits.has(child))
69663
+ return;
69664
+ this.unconfirmedExits.add(child);
69665
+ this.reportHealth(this.logs.get(child), `Local worker cleanup was not confirmed: worker process ${child.pid} was still running ${WORKER_EXIT_CONFIRMATION_MS} ms after SIGKILL; it was retired and replaced`);
69666
+ if (child.connected)
69667
+ child.disconnect();
69668
+ for (const slot of this.slots)
69669
+ if (slot.child === child)
69670
+ slot.retire?.();
69671
+ }
69672
+ reportHealth(log, message) {
69673
+ if (log !== undefined) {
69674
+ this.keptLogs.add(log);
69675
+ appendFile(log, `[atomic ${new Date().toISOString()}] ${message}
69676
+ `).catch(() => {});
69677
+ }
69678
+ this.port.send({ kind: "health", message: log === undefined ? message : `${message} (worker log: ${log})` });
69627
69679
  }
69628
69680
  async close() {
69629
69681
  this.closed = true;
@@ -69633,10 +69685,15 @@ class PoolCoordinator {
69633
69685
  this.rateLimitOutbox.close();
69634
69686
  let failure;
69635
69687
  let stopping = [];
69688
+ let confirmed = [];
69636
69689
  try {
69637
69690
  await this.recovering?.catch(() => {});
69638
69691
  stopping = [...this.children].map((child) => ({ child, log: this.logs.get(child) }));
69639
- await Promise.all(stopping.map(({ child }) => this.stop(child)));
69692
+ confirmed = await Promise.all(stopping.map(({ child }) => this.stop(child)));
69693
+ stopping.forEach(({ child }, index) => {
69694
+ if (!confirmed[index])
69695
+ this.retireUnconfirmed(child);
69696
+ });
69640
69697
  await Promise.all([...this.cleanups]);
69641
69698
  if (this.cleanupFailure)
69642
69699
  throw this.cleanupFailure;
@@ -69648,15 +69705,15 @@ class PoolCoordinator {
69648
69705
  if (alive.length > 0)
69649
69706
  throw unconfirmedCleanup(alive);
69650
69707
  if (this.desiredControl !== "quit" && this.queue !== undefined && this.lease !== undefined)
69651
- await this.queue.releaseClosed(this.lease, [...this.quarantined]).catch((error) => {
69708
+ await this.queue.releaseClosed(this.lease).catch((error) => {
69652
69709
  if (!(error instanceof StaleLeaseError))
69653
69710
  throw error;
69654
69711
  });
69655
69712
  } catch (error) {
69656
69713
  failure = error;
69657
69714
  }
69658
- if (failure === undefined) {
69659
- const stopped = stopping.flatMap(({ child, log }) => log !== undefined && (child.exitCode !== null || child.signalCode !== null) ? [log] : []);
69715
+ if (failure === undefined && !this.failed) {
69716
+ const stopped = stopping.flatMap(({ log }, index) => log !== undefined && !this.keptLogs.has(log) && confirmed[index] === true ? [log] : []);
69660
69717
  await Promise.all(stopped.map((log) => rm3(log, { force: true }).catch(() => {})));
69661
69718
  if (this.logDirectory !== undefined)
69662
69719
  await rmdir(this.logDirectory).catch(() => {});
@@ -69682,7 +69739,7 @@ function childRetryLimit(work, reason) {
69682
69739
  const exportName = work.definition.slice(work.definition.lastIndexOf("#") + 1);
69683
69740
  return `Local worker retry limit: child ${work.childRunId} (${exportName}) ${reason} ${MAX_CHILD_ATTEMPTS} times`;
69684
69741
  }
69685
- var CONTROL_ACKNOWLEDGEMENT_STALL_MS = 5000, RECOVERY_INTERVAL_MS = 500, TICK_INTERVAL_MS = 100, STALL_THRESHOLD_MS = 500, COORDINATOR_LEASE_MS, LATE_ROOT_POLL_MS, MIRROR_INTERVAL_MS = 1e4, STDERR_TAIL_CHARACTERS = 8192, STDERR_DRAIN_GRACE_MS = 1000, MAX_CHILD_ATTEMPTS = 3, ROUTE_REGISTRATION_WAIT_MS = 120000, STAGE_WATCH_BACKLOG_EVENTS = 2000, STAGE_WATCH_BACKLOG_BYTES, PENDING_STAGE_MESSAGE_EVENT2 = "atomic:workflow-pending-stage-message", STICKY_LIVE_DELIVERED_EVENT2 = "atomic:workflow-sticky-live-delivered", PENDING_STAGE_UNDELIVERABLE_EVENT2 = "atomic:workflow-pending-stage-undeliverable", ENDED_WATCHES_REMEMBERED = 1024;
69742
+ var CONTROL_ACKNOWLEDGEMENT_STALL_MS = 5000, RECOVERY_INTERVAL_MS = 500, TICK_INTERVAL_MS = 100, STALL_THRESHOLD_MS = 500, COORDINATOR_LEASE_MS, LATE_ROOT_POLL_MS, MIRROR_INTERVAL_MS = 1e4, STDERR_TAIL_CHARACTERS = 8192, STDERR_DRAIN_GRACE_MS = 1000, WORKER_EXIT_CONFIRMATION_MS = 5000, WORKER_EXIT_POLL_MS = 50, MAX_CHILD_ATTEMPTS = 3, ROUTE_REGISTRATION_WAIT_MS = 120000, STAGE_WATCH_BACKLOG_EVENTS = 2000, STAGE_WATCH_BACKLOG_BYTES, PENDING_STAGE_MESSAGE_EVENT2 = "atomic:workflow-pending-stage-message", STICKY_LIVE_DELIVERED_EVENT2 = "atomic:workflow-sticky-live-delivered", PENDING_STAGE_UNDELIVERABLE_EVENT2 = "atomic:workflow-pending-stage-undeliverable", ENDED_WATCHES_REMEMBERED = 1024;
69686
69743
  var init_pool_coordinator = __esm(() => {
69687
69744
  init_internal();
69688
69745
  init_pool_remote_journal();
@@ -69697,6 +69754,7 @@ var init_pool_coordinator = __esm(() => {
69697
69754
  init_pending_stage_route_readiness();
69698
69755
  init_store();
69699
69756
  init_workflow_infrastructure_error();
69757
+ init_admission_host();
69700
69758
  init_pool_journal();
69701
69759
  init_pooled_watch_trace();
69702
69760
  init_resource_admission();
@@ -71562,6 +71620,7 @@ class LocalWorkerPool {
71562
71620
  works = new Map;
71563
71621
  attempts = new Map;
71564
71622
  results = new Map;
71623
+ healthEvents = [];
71565
71624
  workFailures = new Map;
71566
71625
  changed = Promise.withResolvers();
71567
71626
  waiting = 0;
@@ -71619,6 +71678,9 @@ class LocalWorkerPool {
71619
71678
  if (options.admission && options.rootId === undefined)
71620
71679
  workflowResourceAdmission.set(backend, options.admission);
71621
71680
  }
71681
+ get health() {
71682
+ return this.healthEvents;
71683
+ }
71622
71684
  get workerPids() {
71623
71685
  return [...this.pids];
71624
71686
  }
@@ -72212,14 +72274,8 @@ class LocalWorkerPool {
72212
72274
  try {
72213
72275
  return await this.dispatch(definition, inputs, options);
72214
72276
  } catch (error) {
72215
- if (error instanceof WorkflowInfrastructureError && [...this.workFailures.values()].includes(error))
72216
- throw error;
72217
- try {
72218
- if (this.desiredControl !== "quit" || !findWorkflowGracefulQuit(error))
72219
- await this.shutdown();
72220
- } catch (closeError) {
72221
- throw asWorkflowInfrastructureError(closeError) ?? closeError;
72222
- }
72277
+ if (this.failure !== undefined || this.coordinatorLost !== undefined)
72278
+ await this.shutdown().catch(() => {});
72223
72279
  throw asWorkflowInfrastructureError(error) ?? error;
72224
72280
  }
72225
72281
  }
@@ -72287,8 +72343,13 @@ class LocalWorkerPool {
72287
72343
  try {
72288
72344
  for (;; ) {
72289
72345
  options.signal?.throwIfAborted();
72290
- if (this.closed)
72291
- throw this.closedError(root);
72346
+ if (!this.results.has(work.id) && (this.closed || this.failure !== undefined)) {
72347
+ const recorded = await this.queue?.result(root, work.id).catch(() => {
72348
+ return;
72349
+ });
72350
+ if (recorded !== undefined)
72351
+ this.results.set(work.id, recorded);
72352
+ }
72292
72353
  const result = this.results.get(work.id);
72293
72354
  if (result) {
72294
72355
  this.completing++;
@@ -72312,6 +72373,8 @@ class LocalWorkerPool {
72312
72373
  this.endFailedChild(work, failed);
72313
72374
  throw failed;
72314
72375
  }
72376
+ if (this.closed)
72377
+ throw this.closedError(root);
72315
72378
  if (this.failure)
72316
72379
  throw this.failure;
72317
72380
  if (this.budgetBlocked)
@@ -72346,6 +72409,19 @@ class LocalWorkerPool {
72346
72409
  if (this.reconnectingViews.size > 0 || this.outboxes.size > 0)
72347
72410
  this.reconnectStageViews();
72348
72411
  }
72412
+ recordHealth(message) {
72413
+ this.healthEvents.push(message);
72414
+ const root = this.root;
72415
+ if (root === undefined || this.store === undefined)
72416
+ return;
72417
+ this.store.recordNotice({
72418
+ id: `local-worker-health:${root}:${this.healthEvents.length}`,
72419
+ runId: root,
72420
+ level: "warning",
72421
+ message: `Local worker pool: ${message}`,
72422
+ createdAt: Date.now()
72423
+ });
72424
+ }
72349
72425
  evaluateBudget() {
72350
72426
  const decision = this.rootBudget?.checkpoint();
72351
72427
  const blocked = decision?.kind === "exhausted" || decision?.kind === "wrap_up";
@@ -72464,6 +72540,9 @@ class LocalWorkerPool {
72464
72540
  this.failure ??= new Error(event.error);
72465
72541
  this.signalChange();
72466
72542
  return;
72543
+ case "health":
72544
+ this.recordHealth(event.message);
72545
+ return;
72467
72546
  case "stage-events":
72468
72547
  this.stageWatches.get(event.watch)?.({ kind: "events", events: event.events });
72469
72548
  this.notice({ kind: "stage-events-applied", watch: event.watch });