@bastani/atomic 0.9.44-alpha.5 → 0.9.44-alpha.6

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 (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/broker/broker.ts +13 -1
  4. package/dist/builtin/intercom/index.bundle.mjs +9 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/types.ts +7 -0
  7. package/dist/builtin/intercom/workflow-delivery-states.ts +48 -0
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/package.json +1 -1
  11. package/dist/builtin/workflows/CHANGELOG.md +20 -0
  12. package/dist/builtin/workflows/builtin/{chunk-zjecnjxx.js → chunk-6901nsd6.js} +1 -1
  13. package/dist/builtin/workflows/builtin/{chunk-k8d7ehjx.js → chunk-jvna28mq.js} +381 -130
  14. package/dist/builtin/workflows/builtin/{chunk-qxpymnaf.js → chunk-vwx023ge.js} +1 -1
  15. package/dist/builtin/workflows/builtin/goal.js +2 -2
  16. package/dist/builtin/workflows/builtin/index.js +3 -3
  17. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/durable/journal/internal.d.ts +15 -1
  20. package/dist/builtin/workflows/src/durable/journal/sql-run-journal.d.ts +11 -0
  21. package/dist/builtin/workflows/src/extension/index.bundle.mjs +2373 -703
  22. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +2245 -697
  23. package/dist/builtin/workflows/src/index.js +2244 -697
  24. package/dist/builtin/workflows/src/local-worker-entry.mjs +2245 -697
  25. package/dist/core/agent-session-custom-message-commit.d.ts.map +1 -1
  26. package/dist/core/agent-session-custom-message-commit.js +16 -0
  27. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  28. package/dist/core/storage/node-sqlite.d.ts.map +1 -1
  29. package/dist/core/storage/node-sqlite.js +130 -13
  30. package/dist/core/storage/node-sqlite.js.map +1 -1
  31. package/docs/intercom/reference.md +2 -0
  32. package/docs/workflows/operations.md +1 -1
  33. package/npm-shrinkwrap.json +32 -32
  34. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.44-alpha.6] - 2026-10-25
6
+
7
+ Large workflow fan-outs stay fast for the whole run and recover cleanly from a crash, even with hundreds of pooled child workflows messaging each other.
8
+
9
+ - **Journal writes that keep up:** transactions queued behind a commit share it, delivery receipts are batched, and pending-message changes for a run are written once per batch, so Intercom sends no longer time out behind journal traffic ([#3942](https://github.com/bastani-inc/atomic/pull/3942), [#3943](https://github.com/bastani-inc/atomic/pull/3943), [#3947](https://github.com/bastani-inc/atomic/pull/3947))
10
+ - **Costs that stay flat as a run grows:** pool commits, delivery transitions, route announcements and receipt bookkeeping now cost what changed rather than the run's whole history, and each sticky delivery receipt is written once instead of repeatedly ([#3944](https://github.com/bastani-inc/atomic/pull/3944), [#3945](https://github.com/bastani-inc/atomic/pull/3945), [#3948](https://github.com/bastani-inc/atomic/pull/3948), [#3951](https://github.com/bastani-inc/atomic/pull/3951), [#3952](https://github.com/bastani-inc/atomic/pull/3952))
11
+ - **Fast, reliable recovery:** `/workflow status` and resume load large journals in seconds, resume no longer rebuilds every child's history, and resumed pooled children no longer fail with `Pool checkpoint identity conflict` or a spurious `Storage unavailable` ([#3950](https://github.com/bastani-inc/atomic/pull/3950), [#3951](https://github.com/bastani-inc/atomic/pull/3951), [#3954](https://github.com/bastani-inc/atomic/pull/3954), [#3955](https://github.com/bastani-inc/atomic/pull/3955))
12
+ - **Bounded delivery states:** route announcements, `intercom list`, `/workflow status` and inspect show at most 5 sticky delivery states per row plus totals ([#3951](https://github.com/bastani-inc/atomic/pull/3951))
13
+ - **Paused stages resume their work:** a background completion that arrives while a paused stage resumes now starts its next turn ([#3953](https://github.com/bastani-inc/atomic/pull/3953))
14
+
15
+ ### Fixed
16
+
17
+ - Under heavy write load a local SQLite store (`node-sqlite`, including the workflow journal) commits transactions in groups: transactions requested while another is committing share one `BEGIN IMMEDIATE … COMMIT`, each in its own savepoint and in request order. A transaction that fails rolls back only its own writes, and every caller still settles only after the shared commit. In a 480-child pooled workflow the pool coordinator was committing about 60 transcript transactions a second, each taking the write lock and syncing on its own, which starved the workflow host's writes until Intercom sends timed out and worker leases expired.
18
+ - A background task completion that arrives just as a paused workflow stage is resumed now starts the stage's next turn. Pausing a stage cancels its background shells, and their completion notices are written to the session before the stage acts on them. If the stage was resumed during that write, the notice was queued after the resume had already released the stage's held messages, so the stage sat idle with the notice unanswered until something else started a turn.
19
+
5
20
  ## [0.9.44-alpha.5] - 2026-10-25
6
21
 
7
22
  Large workflow fan-outs keep their Intercom routes and journal writes responsive under load.
@@ -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.44-alpha.6] - 2026-10-25
8
+
9
+ ### Changed
10
+
11
+ - `intercom list` lists at most 5 sticky delivery states per workflow row: states that still need action first, then the most recent deliveries. A final line reports how many were omitted and the total in each state, and the row details carry the same totals as `deliveryStateCounts` and `omittedDeliveryStates`. In a 640-child workflow, each list result had stored about 3 MB of delivery states in the session transcript. A test with 8,000 queued messages in 163 rows shrinks the list result from about 4.6 MB to about 0.2 MB. The broker applies the same bound to rows announced by older workflow hosts.
12
+
7
13
  ## [0.9.44-alpha.5] - 2026-10-25
8
14
 
9
15
  ### Fixed
@@ -15,6 +15,7 @@ import {
15
15
  writeMessageIfOpen,
16
16
  writeMessageWithOutcome,
17
17
  } from "./socket-writes.js";
18
+ import { boundWorkflowPossibleStageRow } from "../workflow-delivery-states.js";
18
19
  import { isSocketAnswering } from "./socket-liveness.js";
19
20
  import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
20
21
  import { startBrokerWatchdog } from "./watchdog.js";
@@ -303,6 +304,14 @@ function isWorkflowPossibleStageAnnouncements(
303
304
  Number.isInteger(announcement.queuedCount) &&
304
305
  announcement.queuedCount >= 0 &&
305
306
  (announcement.deliveryOnly === undefined || announcement.deliveryOnly === true) &&
307
+ (announcement.omittedDeliveryStates === undefined ||
308
+ (Number.isInteger(announcement.omittedDeliveryStates) && announcement.omittedDeliveryStates >= 0)) &&
309
+ (announcement.deliveryStateCounts === undefined ||
310
+ (typeof announcement.deliveryStateCounts === "object" &&
311
+ announcement.deliveryStateCounts !== null &&
312
+ Object.values(announcement.deliveryStateCounts).every(
313
+ (count) => Number.isInteger(count) && count >= 0,
314
+ ))) &&
306
315
  (announcement.deliveryStates === undefined || (Array.isArray(announcement.deliveryStates) && announcement.deliveryStates.every((delivery) =>
307
316
  delivery !== null && typeof delivery === "object" &&
308
317
  typeof delivery.messageId === "string" && typeof delivery.runId === "string" &&
@@ -724,6 +733,8 @@ class IntercomBroker {
724
733
  target: row.target,
725
734
  queuedCount: row.queuedCount,
726
735
  ...(row.deliveryStates === undefined ? {} : { deliveryStates: row.deliveryStates }),
736
+ ...(row.deliveryStateCounts === undefined ? {} : { deliveryStateCounts: row.deliveryStateCounts }),
737
+ ...(row.omittedDeliveryStates === undefined ? {} : { omittedDeliveryStates: row.omittedDeliveryStates }),
727
738
  ...(row.deliveryOnly === undefined ? {} : { deliveryOnly: row.deliveryOnly }),
728
739
  group: roster.group,
729
740
  });
@@ -1913,9 +1924,10 @@ class IntercomBroker {
1913
1924
  // D7 (slice 4): presence replaces, absence keeps — a terminal root publishes `[]`
1914
1925
  // so the broker drops every future row, while nested runs' re-announcements (which
1915
1926
  // never carry the field) preserve the root's rows.
1927
+ // Rows from hosts that announce every delivery state are bounded like current hosts' rows.
1916
1928
  const possibleStages =
1917
1929
  clientMessage.possibleStages !== undefined
1918
- ? clientMessage.possibleStages
1930
+ ? clientMessage.possibleStages.map((row) => boundWorkflowPossibleStageRow(row))
1919
1931
  : this.workflowRosters.get(clientMessage.runId)?.possibleStages;
1920
1932
  // Optional path metadata is advisory for old hosts; omission retains a
1921
1933
  // previously announced edge during two-session route replay.
@@ -4082,7 +4082,15 @@ function formatWorkflowStageRow(stage) {
4082
4082
  }
4083
4083
  function formatWorkflowFutureStageRow(stage) {
4084
4084
  return `- \`${stage.target}\` [${stage.deliveryOnly === true ? "delivery" : "future"}] ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}${(stage.deliveryStates ?? []).map((delivery) => `
4085
- delivery ${delivery.messageId} stage ${delivery.runId}/${delivery.stageId}${delivery.sessionId === undefined ? "" : ` session ${delivery.sessionId}`} ${delivery.state} — ${delivery.reason}`).join("")}`;
4085
+ delivery ${delivery.messageId} stage ${delivery.runId}/${delivery.stageId}${delivery.sessionId === undefined ? "" : ` session ${delivery.sessionId}`} ${delivery.state} — ${delivery.reason}`).join("")}${formatDeliveryStateSummary(stage)}`;
4086
+ }
4087
+ function formatDeliveryStateSummary(stage) {
4088
+ const omitted = stage.omittedDeliveryStates ?? 0;
4089
+ if (omitted === 0 || stage.deliveryStateCounts === undefined)
4090
+ return "";
4091
+ const counts = Object.entries(stage.deliveryStateCounts).map(([state, count]) => `${count} ${state}`).join(", ");
4092
+ return `
4093
+ … ${omitted} more delivery state${omitted === 1 ? "" : "s"} omitted (all: ${counts})`;
4086
4094
  }
4087
4095
  function registerIntercomTool(pi, deps) {
4088
4096
  const retryIdentities = new RetryIdentityReservations({ maxEntries: deps.retryIdentityMaxEntries });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.44-alpha.5",
3
+ "version": "0.9.44-alpha.6",
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": [
@@ -52,7 +52,12 @@ export interface WorkflowPossibleStageAnnouncement {
52
52
  readonly target: string;
53
53
  /** Current number of queued sticky entries matching this target. */
54
54
  readonly queuedCount: number;
55
+ /** At most a fixed number of exemplar states: unresolved states first, then the latest delivered. */
55
56
  readonly deliveryStates?: readonly WorkflowStickyDeliveryState[];
57
+ /** Delivery states per state, over every state of this row, including omitted ones. */
58
+ readonly deliveryStateCounts?: Readonly<Record<string, number>>;
59
+ /** Delivery states left out of `deliveryStates`. */
60
+ readonly omittedDeliveryStates?: number;
56
61
  readonly deliveryOnly?: true;
57
62
  }
58
63
 
@@ -63,6 +68,8 @@ export interface WorkflowFutureStageRosterEntry {
63
68
  readonly queuedCount: number;
64
69
  readonly group: string;
65
70
  readonly deliveryStates?: readonly WorkflowStickyDeliveryState[];
71
+ readonly deliveryStateCounts?: Readonly<Record<string, number>>;
72
+ readonly omittedDeliveryStates?: number;
66
73
  readonly deliveryOnly?: true;
67
74
  }
68
75
 
@@ -0,0 +1,48 @@
1
+ import type { WorkflowPossibleStageAnnouncement, WorkflowStickyDeliveryState } from "./types.js";
2
+
3
+ /** Most delivery states one possible-stage row carries; the rest are counted, not listed. */
4
+ export const WORKFLOW_DELIVERY_STATE_EXEMPLAR_LIMIT = 5;
5
+
6
+ /** States that still need action, in the order exemplars are chosen; `delivered` follows, most recent first. */
7
+ const UNRESOLVED_STATES = [
8
+ "queued",
9
+ "receipt-unverified",
10
+ "session-replaced",
11
+ "delivery-unavailable",
12
+ "root-unresolved",
13
+ "stage-terminal",
14
+ ];
15
+
16
+ /**
17
+ * Bound a possible-stage row's delivery states as workflow hosts do, for rows from hosts that announce
18
+ * every state. Rows already within the limit pass through unchanged.
19
+ */
20
+ export function boundWorkflowPossibleStageRow(
21
+ row: WorkflowPossibleStageAnnouncement,
22
+ limit = WORKFLOW_DELIVERY_STATE_EXEMPLAR_LIMIT,
23
+ ): WorkflowPossibleStageAnnouncement {
24
+ const states = row.deliveryStates;
25
+ if (states === undefined || states.length <= limit) return row;
26
+ const exemplars: WorkflowStickyDeliveryState[] = [];
27
+ for (const state of UNRESOLVED_STATES) {
28
+ for (const delivery of states) {
29
+ if (exemplars.length === limit) break;
30
+ if (delivery.state === state) exemplars.push(delivery);
31
+ }
32
+ }
33
+ for (let index = states.length - 1; index >= 0 && exemplars.length < limit; index--) {
34
+ if (states[index]!.state === "delivered") exemplars.push(states[index]!);
35
+ }
36
+ for (const delivery of states) {
37
+ if (exemplars.length === limit) break;
38
+ if (delivery.state !== "delivered" && !UNRESOLVED_STATES.includes(delivery.state)) exemplars.push(delivery);
39
+ }
40
+ const counts = new Map<string, number>();
41
+ for (const delivery of states) counts.set(delivery.state, (counts.get(delivery.state) ?? 0) + 1);
42
+ return {
43
+ ...row,
44
+ deliveryStates: exemplars,
45
+ deliveryStateCounts: row.deliveryStateCounts ?? Object.fromEntries(counts),
46
+ omittedDeliveryStates: (row.omittedDeliveryStates ?? 0) + states.length - exemplars.length,
47
+ };
48
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.44-alpha.5",
3
+ "version": "0.9.44-alpha.6",
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.44-alpha.5",
3
+ "version": "0.9.44-alpha.6",
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.44-alpha.5",
3
+ "version": "0.9.44-alpha.6",
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,26 @@ 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.44-alpha.6] - 2026-10-25
10
+
11
+ ### Fixed
12
+
13
+ - After a pooled root was killed and resumed, some child workflows no longer fail with `Extension startup failed: …intercom…: Storage unavailable: custom at custom journal`. While a resumed root and its workers were busy, a worker's lease could run out a moment before the pool extended it. When the root then recorded that a queued Intercom message had reached the worker's stage, it refused the write as if the attempt had been replaced and reported the refusal as unavailable storage, so the stage failed at startup. In a 640-child run this failed 21 children. The pool already keeps a live worker's child when its lease runs out (#3888), and the root now does the same when recording deliveries. Only a reassigned attempt or a new root epoch is refused, with `Stale work lease` or the session reason, and the replaced attempt's child is replayed. A failure of a custom journal now also names the cause's error class and code, though not its message, which may contain credentials.
14
+ - A long-running workflow fan-out whose stages keep sending Intercom messages to stage patterns (for example `workflow:<rootRunId>/child-*/**`) no longer slows down with every message it has sent. Pattern messages stay queued until the root run ends, and each delivery receipt made the workflow host re-check every queued message and every recorded delivery against every run. In a 480-child load test this saturated the host, Intercom sends timed out after 10 s, and about half of them were never delivered. A delivery now re-checks only the messages it changed and the messages delivered to stages that have since ended: with 3,000 queued messages, recording one delivery takes about 1.2 ms instead of 6.7 ms. Delivery results are unchanged.
15
+ - An Intercom send to a workflow stage no longer times out when a wave of stages starts at the same time. Every pending-message change for a run used to wait for the previous one's journal write, so a send that arrived while about 160 starting stages recorded their deliveries waited behind about 160 writes. Changes that arrive while a write is in progress are now applied together and saved in one write. In a test with a 40 ms write, a send issued in the middle of a 160-stage burst returned after 2 writes (about 90 ms) instead of 81 (about 3.3 s).
16
+ - Large workflow fan-outs no longer saturate the local worker pool's coordinator. Every pool commit, including each batch of session transcript entries, was checked for JSON-serializability with a recursive schema whose `$ref` evaluation cost far more than the write itself, so with 160 workers the coordinator spent about 90% of its time validating and stages stopped starting. Plain JSON values now pass a direct structural check; anything unusual still goes through the schema, with the same errors.
17
+ - Intercom sends to stage patterns no longer time out while a large workflow fan-out records delivery receipts. The workflow host wrote each receipt in its own journal transaction, one after another, and a pattern message waited behind every receipt queued before it. When another process also held the journal's write lock, that wait exceeded the 10-second send acknowledgement, and in a 480-child load test delivery fell to 0–2%. Receipts and settlements queued back to back for a run are now written together in one transaction, in the same order and with the same durability. In a test that queues a pattern message halfway through 2,000 receipts from 160 callers while a second process holds the write lock, the message persisted in under 30 ms instead of more than 4 seconds.
18
+ - Resuming a large workflow with many completed child workflows no longer takes minutes before any work restarts. Resume rebuilt every nested run from the whole checkpoint journal once for each cached child, and each rebuild itself rescanned the journal for every child boundary. Resume now rebuilds the nested runs once, reuses them while replay records only its own bookkeeping, and rebuilds each in a single pass. Resuming a pooled root with 640 completed children went from 156 s to 1.3 s (64 children: 271 ms to 37 ms), and resumed runs reach the same state as before.
19
+ - The local worker pool coordinator no longer slows down as a long pooled run grows. Each checkpoint a worker committed was validated against every checkpoint the root had ever recorded, and each commit re-added every usage report, so after about 15 minutes of a 640-child run the coordinator sat near full CPU and Intercom sends began to fail. Validation now reads only the checkpoints recorded since the previous commit, and the root's usage total is kept up to date as usage is written. Validation results and totals are unchanged. With 5,000 recorded checkpoints, a commit takes about 1.2 ms instead of 16 ms, and reading the usage total for 40,000 usage rows takes 0.02 ms instead of 8 ms.
20
+ - Late in a long workflow fan-out, Intercom sends no longer time out while the host announces workflow routes or records delivery receipts. Each root route announcement recomputed every queued pattern message's delivery state on every matching stage, and every recorded receipt re-read the delivery count of every queued message. Both grow for the whole run (about 7,700 queued messages after 21 minutes in a 640-child load test), so one announcement could block the host for seconds. Announcements and receipts now recompute only what changed. With 8,000 queued messages, an announcement after one delivery makes 2 receipt checks instead of 16,000, and recording a receipt reads 1 delivery count instead of 8,000. Delivery counts are unchanged.
21
+ - `/workflow status` from a new session, and resuming a workflow, no longer stall on a journal that holds many delivery receipts. Loading the journal re-read the delivery count of every queued message for every receipt. After a 640-child run with about 800,000 receipts, status had not rendered after 3 minutes. In a test with 200,000 receipts over 8,000 queued messages, opening and loading the run now takes about 3.4 s instead of 168 s.
22
+ - Each delivery of a pattern (sticky) Intercom message to a workflow stage is now written to the run's journal once. When a delivered stage ended, the workflow host archived that delivery's receipt together with every earlier receipt still kept for the message. Those earlier receipts stayed kept, so they were written again each time a later stage ended. In pooled load-test journals about 93% of delivery receipts were exact copies, with one receipt written up to 423 times, and each copy took a slot in the run's journal writes. Receipts that are already archived are no longer written again. Delivery counts are unchanged: the copies never raised the recorded count.
23
+ - A pooled child workflow resumed more than once no longer fails with `Pool checkpoint identity conflict`. Replaying a completed stage records a small replay-metadata checkpoint under an id derived from the stage, stamped with the time of that replay. The first resume recorded it. When a later resume, or a replacement worker, replayed the stage again, the pool coordinator compared the new record byte for byte with the stored one, saw a different time and failed the child. In a 640-child load test, 31 children failed this way after one SIGKILL and resume. A re-recorded checkpoint that differs only in its record time now keeps the first write. A checkpoint with any other difference is still rejected.
24
+
25
+ ### Changed
26
+
27
+ - Route announcements carry at most 5 sticky delivery states per possible-stage row, with per-state totals and the number omitted. Each state leaves out its target, which the row already names. Workflow status and run inspection list at most 5 delivery states per run in the same way, with the same omitted-states line. Previously each announcement, `intercom list` result and status result carried every delivery state. With 8,000 queued messages across 163 rows, an announcement shrinks from about 4.6 MB to about 0.19 MB.
28
+
9
29
  ## [0.9.44-alpha.5] - 2026-10-25
10
30
 
11
31
  ### Fixed
@@ -39,7 +39,7 @@ import {
39
39
  reviewerFailureText,
40
40
  summarizeReviewConvergence,
41
41
  reverify_consolidated_batch
42
- } from "./chunk-k8d7ehjx.js";
42
+ } from "./chunk-jvna28mq.js";
43
43
  import {
44
44
  fold_usage
45
45
  } from "./chunk-tgt0s5e5.js";