@bastani/atomic 0.9.42 → 0.9.43-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,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.43-alpha.1] - 2026-10-24
6
+
7
+ Local workflow worker pools now survive slow, busy, and crashed workers, and large workflow journals open without running out of memory.
8
+
9
+ - **Pools that recover instead of failing:** a worker that dies mid-run is replaced and its child replays from the last checkpoint instead of crashing the root with `EPIPE`, a resumed root releases children leased to workers that died with the host, and unconfirmed worker exits are retried rather than failing every waiting child ([#3891](https://github.com/bastani-inc/atomic/pull/3891), [#3893](https://github.com/bastani-inc/atomic/pull/3893), [#3895](https://github.com/bastani-inc/atomic/pull/3895))
10
+ - **Leases that follow worker liveness:** a busy root no longer fences healthy workers with `Stale work lease`, and the new `workflows.localWorkers.leaseMs` setting controls how long an unresponsive worker keeps its child (30000 ms by default) ([#3894](https://github.com/bastani-inc/atomic/pull/3894))
11
+ - **Retry decisions you can make in code:** `WorkflowInfrastructureError` and `isRetryableWorkflowError` tell retryable pool and runtime faults apart from failures of a child's own logic ([#3895](https://github.com/bastani-inc/atomic/pull/3895))
12
+ - **Faster startup on long journals:** workers become ready without reading the whole journal, the readiness timeout is configurable with `workflows.localWorkers.startupTimeoutMs`, and sessions, `/workflow status`, and the `/workflow resume` picker read each run's latest state instead of every event, so large journals no longer exhaust the heap ([#3891](https://github.com/bastani-inc/atomic/pull/3891), [#3896](https://github.com/bastani-inc/atomic/pull/3896))
13
+ - **Easier worker diagnosis:** each worker's standard error is kept under `worker-logs/` next to the journal, and startup and retry-limit errors name the log file and show the end of its output ([#3891](https://github.com/bastani-inc/atomic/pull/3891))
14
+
15
+ ### Added
16
+
17
+ - Added `WorkflowInfrastructureError` and `isRetryableWorkflowError` to `@bastani/atomic/workflows`, so workflows can tell local worker pool and runtime faults (which are safe to retry) from failures of a child workflow's own logic ([#3889](https://github.com/bastani-inc/atomic/issues/3889))
18
+ - Added the `workflows.localWorkers.leaseMs` setting: how long, in milliseconds, a local workflow worker that is still running but has stopped responding keeps its child before the child is replayed on another worker (30000 by default, at least 2000) ([#3888](https://github.com/bastani-inc/atomic/issues/3888))
19
+
20
+ ### Fixed
21
+
22
+ - Local workflow worker pools no longer fail with `Local worker startup timed out` when workers are slow to become ready, for example on a workflow journal with a long history. The readiness timeout is now configurable with the `workflows.localWorkers.startupTimeoutMs` setting (30000 ms by default) ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
23
+
5
24
  ## [0.9.42] - 2026-10-23
6
25
 
7
26
  Sessions, workflow runs, and Intercom can now share one libSQL / Turso database across machines, and provider rate limits are respected across every request in a process.
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.43-alpha.1] - 2026-10-24
8
+
9
+ ### Fixed
10
+
11
+ - Fixed `Intercom event relay failed (atomic:workflow-pending-stage-route): Intercom runtime no longer active` appearing in the session after a local workflow worker died and its child was replayed. Route updates that arrive while a run's previous route connection is still closing or opening, as they do when the run's route is replaced, no longer race each other: the newest route is registered over one connection and an update the replacement made obsolete ends quietly. A route connection abandoned because the session itself shut down is still reported ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
12
+
7
13
  ## [0.9.42] - 2026-10-23
8
14
 
9
15
  ### Added
@@ -8151,23 +8151,30 @@ function piIntercomExtension(pi, testOverrides = {}) {
8151
8151
  pi.events.emit(PENDING_STAGE_ROUTE_REVOKED_EVENT, { runId, capability: state.route.capability });
8152
8152
  });
8153
8153
  }
8154
+ async function retirePendingStageRouteClient(state) {
8155
+ try {
8156
+ await state.promise;
8157
+ } catch {}
8158
+ if (state.client) {
8159
+ try {
8160
+ await state.client.disconnect();
8161
+ } catch {}
8162
+ }
8163
+ }
8164
+ function supersededPendingStageRouteError(context, generation) {
8165
+ return getLiveContext(context, generation) ? new IntercomClientDisconnectedError : new Error("Intercom runtime no longer active");
8166
+ }
8154
8167
  async function ensurePendingStageRouteClient(runId, route, options = {}) {
8155
8168
  const contextAtStart = getLiveContext();
8156
8169
  const generationAtStart = runtimeGeneration;
8157
8170
  if (!contextAtStart)
8158
8171
  throw new Error("Intercom runtime not initialized");
8159
8172
  let existing = pendingStageRouteClients.get(runId);
8173
+ let retired;
8160
8174
  if (existing && !samePendingStageRoute(existing.route, route)) {
8161
8175
  pendingStageRouteClients.delete(runId);
8162
8176
  endPendingStageRouteBackoff(existing, new IntercomClientDisconnectedError);
8163
- try {
8164
- await existing.promise;
8165
- } catch {}
8166
- if (existing.client) {
8167
- try {
8168
- await existing.client.disconnect();
8169
- } catch {}
8170
- }
8177
+ retired = retirePendingStageRouteClient(existing);
8171
8178
  existing = undefined;
8172
8179
  }
8173
8180
  const state = existing ?? {
@@ -8191,9 +8198,9 @@ function piIntercomExtension(pi, testOverrides = {}) {
8191
8198
  }
8192
8199
  if (state.reconnectTimer !== null && options.bypassBackoff !== true)
8193
8200
  return state.retryWaiters.join();
8194
- const promise = routeClientConnectGate.run(async () => {
8201
+ const connect = () => routeClientConnectGate.run(async () => {
8195
8202
  if (!pendingStageRouteClientIsCurrent(runId, state, contextAtStart, generationAtStart)) {
8196
- throw new Error("Intercom runtime no longer active");
8203
+ throw supersededPendingStageRouteError(contextAtStart, generationAtStart);
8197
8204
  }
8198
8205
  await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
8199
8206
  const routeReturnAddress = `${currentSessionId}:pending-stage-route:${runId}`;
@@ -8205,16 +8212,17 @@ function piIntercomExtension(pi, testOverrides = {}) {
8205
8212
  await nextClient.connect({ ...buildRegistration(), recipientPurpose: "control", name: undefined, groups: [normalizeGroup(route.group)], group: normalizeGroup(route.group) }, undefined, undefined, readSubagentMessageSource(runtimeContext?.subagentPolicy));
8206
8213
  if (!pendingStageRouteClientIsCurrent(runId, state, contextAtStart, generationAtStart)) {
8207
8214
  await nextClient.disconnect();
8208
- throw new Error("Intercom runtime no longer active");
8215
+ throw supersededPendingStageRouteError(contextAtStart, generationAtStart);
8209
8216
  }
8210
8217
  await nextClient.registerPendingStageRouteAcknowledged(runId, normalizeGroup(route.group), route.capability, route.stages, route.possibleStages, route.parent, route.dormant === true);
8211
8218
  if (!pendingStageRouteClientIsCurrent(runId, state, contextAtStart, generationAtStart)) {
8212
8219
  await nextClient.disconnect();
8213
- throw new Error("Intercom runtime no longer active");
8220
+ throw supersededPendingStageRouteError(contextAtStart, generationAtStart);
8214
8221
  }
8215
8222
  state.reconnectAttempt = 0;
8216
8223
  endPendingStageRouteBackoff(state);
8217
8224
  });
8225
+ const promise = retired === undefined ? connect() : retired.then(connect);
8218
8226
  state.promise = promise;
8219
8227
  try {
8220
8228
  await promise;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.42",
3
+ "version": "0.9.43-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.42",
3
+ "version": "0.9.43-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.42",
3
+ "version": "0.9.43-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.42",
3
+ "version": "0.9.43-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": [
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.43-alpha.1] - 2026-10-24
10
+
11
+ ### Added
12
+
13
+ - `ctx.workflow(child)` now throws a `WorkflowInfrastructureError` with `retryable: true` and a fault `code` when the local worker pool cannot run a child because of a fault in the pool or its runtime (for example `Local worker retry limit`, `Local worker cleanup was not confirmed`, `Stale work lease`, `Intercom runtime no longer active` or `Storage unavailable`), and `isRetryableWorkflowError(error)` recognizes it anywhere in an error's `cause` chain. Failures of a child's own logic keep throwing a plain `Error` ([#3889](https://github.com/bastani-inc/atomic/issues/3889))
14
+
15
+ ### Changed
16
+
17
+ - Local worker leases now follow worker liveness. A worker that is still running and responding keeps its child even when the root is too busy to process its heartbeats in time, so a slow root no longer fences healthy workers with `Stale work lease`. A worker that has stopped responding is fenced after `localWorkers.leaseMs` milliseconds, which defaults to 30000 instead of the previous fixed 5000 ([#3888](https://github.com/bastani-inc/atomic/issues/3888))
18
+
19
+ ### Fixed
20
+
21
+ - A local workflow worker that dies while the root still owes it replies no longer crashes the root with `Error: write EPIPE`. The root fences that worker's attempt and replays the child from its last committed checkpoint on a replacement worker ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
22
+ - Each local worker's standard error is now kept in `worker-logs/<root run ID>/<worker ID>.log` in the workflow journal's directory, and the `Local worker exited before readiness`, `Local worker startup timed out` and `Local worker retry limit` errors name that file and include the end of the worker's output. Logs of workers that are still running when the pool shuts down normally are removed ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
23
+ - Local worker pools no longer fail with `Local worker startup timed out` on a workflow journal with a long history. Workers no longer read every run in the journal before reporting ready, the pool starts running children once one worker is ready and fails only when none starts, and the readiness timeout can be raised with `localWorkers.startupTimeoutMs` (30000 ms by default). Once running, workers bring their view of the root up to date by reading only the journal events added since their last read, instead of re-reading the root's whole history after every checkpoint and claim. The timeout is not saved with the root, so it can change between resumes ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
24
+ - Resuming a pooled root after its host process crashed no longer leaves children leased to workers that died with it. The resumed root releases those attempts as soon as it starts its new pool, and their children are replayed from their last committed checkpoints on the new workers ([#3890](https://github.com/bastani-inc/atomic/issues/3890))
25
+ - Opening or resuming a session with a large workflow journal no longer shows a blank screen and then crashes with `JavaScript heap out of memory`. Sessions, `/workflow status` and the `/workflow resume` picker now read each run's latest state instead of every event in the journal, and load a run's full history only when that run is resumed, recovered, inspected or opened ([#3892](https://github.com/bastani-inc/atomic/issues/3892))
26
+ - A local worker whose process exit cannot be confirmed during lease recovery no longer fails every child waiting on the pool with `Local worker cleanup was not confirmed`. The pool keeps the attempt fenced and retries recovery until the worker exits ([#3889](https://github.com/bastani-inc/atomic/issues/3889))
27
+
9
28
  ## [0.9.42] - 2026-10-23
10
29
 
11
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.42",
3
+ "version": "0.9.43-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -36,6 +36,8 @@ export type RunOpts = Omit<AuthoringContract.RunOpts, "registry"> & {
36
36
  export declare const INTERACTIVE_WORKFLOW_POLICY: WorkflowExecutionPolicy;
37
37
  export declare const NON_INTERACTIVE_WORKFLOW_POLICY: WorkflowExecutionPolicy;
38
38
  export declare function run<TInputs extends WorkflowInputValues, TOutputs extends WorkflowOutputValues, TRunInputs extends WorkflowInputValues = TInputs>(definition: WorkflowDefinition<TInputs, TOutputs, TRunInputs>, inputs: Readonly<NoInfer<WorkflowRunInputArgument<TRunInputs>>>, opts?: RunOpts): Promise<RunResult<TOutputs>>;
39
+ export type { WorkflowInfrastructureFault } from "./shared/workflow-infrastructure-error.js";
40
+ export { isRetryableWorkflowError, WorkflowInfrastructureError } from "./shared/workflow-infrastructure-error.js";
39
41
  export declare function loadWorkerWorkflow(path: string, exportName?: string): Promise<AnyWorkflowDefinition>;
40
42
  export type { LocalWorkerOptions, WorkflowAdmissionOptions } from "./workers/public-options.js";
41
43
  /**
@@ -1,17 +1,33 @@
1
1
  import type { SqliteExecutor } from "@bastani/atomic/storage";
2
- import type { RunJournal } from "./types.js";
2
+ import { type JournalEntry, type RunJournal } from "./types.js";
3
3
  export declare const JOURNAL_READ_PAGE_SIZE = 256;
4
+ /** Event types the durable backend folds without replacing the run's handle; every other event carries it. */
5
+ export declare const NON_SNAPSHOT_EVENT_TYPES: readonly ["CheckpointRecorded", "MessageSettled", "MessageDelivered"];
4
6
  /** A run's latest event. A run whose history was deleted and recorded again ends with a different event. */
5
7
  export interface RunVersion {
6
8
  readonly seq: number;
7
9
  readonly at: number;
8
10
  }
11
+ /**
12
+ * A run's latest snapshot event (one carrying the run's handle) and the checkpoints recorded after it: enough to
13
+ * list the run in a catalog without reading its history.
14
+ */
15
+ export interface RunSnapshotTail {
16
+ /** The run's latest event of any type. */
17
+ readonly version: RunVersion;
18
+ readonly snapshot: JournalEntry;
19
+ readonly laterCheckpoints: number;
20
+ /** `completedAt` of the last of `laterCheckpoints`. */
21
+ readonly lastCheckpointAt?: number;
22
+ }
9
23
  /** Lets a durable backend re-read only the runs that changed; kept off the public journal shape. */
10
24
  export interface RunJournalVersions {
11
25
  /** Moves whenever any connection, in this process or another, commits a change to the journal's store. */
12
26
  changeToken(): Promise<string>;
13
27
  /** Every listed run with its latest event; a run with no events has version `{ seq: 0, at: 0 }`. */
14
28
  runVersions(): Promise<ReadonlyMap<string, RunVersion>>;
29
+ /** Undefined for a run with no snapshot event. */
30
+ snapshotTail(runId: string): Promise<RunSnapshotTail | undefined>;
15
31
  }
16
32
  export declare function registerRunJournalVersions(journal: RunJournal, versions: RunJournalVersions): void;
17
33
  /** Change tracking for a journal that registered it; other journals are re-read in full. */
@@ -29,6 +29,11 @@ export declare class SqlRunJournal implements RunJournal {
29
29
  */
30
30
  private changeToken;
31
31
  private runVersions;
32
+ /**
33
+ * The latest snapshot event is found by walking the run's `(run_id, seq)` key back from its last event, so
34
+ * only the events after it are visited and only its payload is read.
35
+ */
36
+ private snapshotTail;
32
37
  private requireRun;
33
38
  /** Whether this run exists only in the legacy tables, whose events are then read in place. */
34
39
  private isUnconvertedLegacyRun;