@bastani/atomic 0.9.45-alpha.1 → 0.9.46-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.
Files changed (166) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +11 -0
  3. package/dist/builtin/intercom/broker/broker.ts +124 -10
  4. package/dist/builtin/intercom/broker/client.ts +177 -25
  5. package/dist/builtin/intercom/broker/multinode-router.ts +228 -27
  6. package/dist/builtin/intercom/broker/multinode-state-store.ts +10 -0
  7. package/dist/builtin/intercom/broker/recipient-receipts.ts +38 -4
  8. package/dist/builtin/intercom/broker/send-handler.ts +9 -2
  9. package/dist/builtin/intercom/broker/sql-outbox-store.ts +29 -4
  10. package/dist/builtin/intercom/group.ts +25 -0
  11. package/dist/builtin/intercom/index.bundle.mjs +177 -35
  12. package/dist/builtin/intercom/package.json +1 -1
  13. package/dist/builtin/intercom/retry-policy.ts +7 -0
  14. package/dist/builtin/intercom/types.ts +21 -0
  15. package/dist/builtin/mcp/package.json +1 -1
  16. package/dist/builtin/subagents/CHANGELOG.md +10 -0
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/src/extension/index.bundle.mjs +150 -54
  19. package/dist/builtin/web-access/package.json +1 -1
  20. package/dist/builtin/workflows/CHANGELOG.md +45 -0
  21. package/dist/builtin/workflows/builtin/{chunk-6901nsd6.js → chunk-6jpwc09k.js} +1 -1
  22. package/dist/builtin/workflows/builtin/{chunk-vwx023ge.js → chunk-qhm35ntx.js} +1 -1
  23. package/dist/builtin/workflows/builtin/{chunk-jvna28mq.js → chunk-zhyqnf5g.js} +331 -68
  24. package/dist/builtin/workflows/builtin/goal.js +2 -2
  25. package/dist/builtin/workflows/builtin/index.js +3 -3
  26. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  27. package/dist/builtin/workflows/package.json +1 -1
  28. package/dist/builtin/workflows/skills/prompt-engineer/SKILL.md +1 -0
  29. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6.md +2 -2
  30. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_sol.md +2 -2
  31. package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_terra.md +77 -0
  32. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  33. package/dist/builtin/workflows/src/durable/journal/internal.d.ts +2 -0
  34. package/dist/builtin/workflows/src/extension/index.bundle.mjs +7076 -3386
  35. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +11075 -7261
  36. package/dist/builtin/workflows/src/index.js +6768 -3605
  37. package/dist/builtin/workflows/src/local-worker-entry.mjs +11075 -7261
  38. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +19 -0
  39. package/dist/core/agent-session-boundaries.d.ts.map +1 -1
  40. package/dist/core/agent-session-boundaries.js +1 -0
  41. package/dist/core/agent-session-boundaries.js.map +1 -1
  42. package/dist/core/agent-session-events.d.ts.map +1 -1
  43. package/dist/core/agent-session-events.js +2 -1
  44. package/dist/core/agent-session-events.js.map +1 -1
  45. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  46. package/dist/core/agent-session-extension-bindings.js +15 -2
  47. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  48. package/dist/core/agent-session-methods.d.ts +3 -2
  49. package/dist/core/agent-session-methods.d.ts.map +1 -1
  50. package/dist/core/agent-session-methods.js.map +1 -1
  51. package/dist/core/agent-session-prompt.d.ts +4 -2
  52. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  53. package/dist/core/agent-session-prompt.js +13 -8
  54. package/dist/core/agent-session-prompt.js.map +1 -1
  55. package/dist/core/agent-session-recovery.d.ts.map +1 -1
  56. package/dist/core/agent-session-recovery.js +92 -1
  57. package/dist/core/agent-session-recovery.js.map +1 -1
  58. package/dist/core/agent-session-transfer.d.ts.map +1 -1
  59. package/dist/core/agent-session-transfer.js +5 -0
  60. package/dist/core/agent-session-transfer.js.map +1 -1
  61. package/dist/core/agent-session.d.ts +2 -0
  62. package/dist/core/agent-session.d.ts.map +1 -1
  63. package/dist/core/agent-session.js +2 -0
  64. package/dist/core/agent-session.js.map +1 -1
  65. package/dist/core/copilot-errors.d.ts +6 -0
  66. package/dist/core/copilot-errors.d.ts.map +1 -0
  67. package/dist/core/copilot-errors.js +17 -0
  68. package/dist/core/copilot-errors.js.map +1 -0
  69. package/dist/core/execution-model-router.d.ts.map +1 -1
  70. package/dist/core/execution-model-router.js +12 -2
  71. package/dist/core/execution-model-router.js.map +1 -1
  72. package/dist/core/extensions/context-types.d.ts +7 -0
  73. package/dist/core/extensions/context-types.d.ts.map +1 -1
  74. package/dist/core/extensions/context-types.js.map +1 -1
  75. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  76. package/dist/core/extensions/runner-context.js +6 -0
  77. package/dist/core/extensions/runner-context.js.map +1 -1
  78. package/dist/core/extensions/workflow-activity-hub.d.ts +4 -0
  79. package/dist/core/extensions/workflow-activity-hub.d.ts.map +1 -1
  80. package/dist/core/extensions/workflow-activity-hub.js +26 -5
  81. package/dist/core/extensions/workflow-activity-hub.js.map +1 -1
  82. package/dist/core/extensions/workflow-events.d.ts +5 -0
  83. package/dist/core/extensions/workflow-events.d.ts.map +1 -1
  84. package/dist/core/extensions/workflow-events.js.map +1 -1
  85. package/dist/core/messages.d.ts +5 -0
  86. package/dist/core/messages.d.ts.map +1 -1
  87. package/dist/core/messages.js +1 -1
  88. package/dist/core/messages.js.map +1 -1
  89. package/dist/core/model-fallback-failures.d.ts +1 -1
  90. package/dist/core/model-fallback-failures.d.ts.map +1 -1
  91. package/dist/core/model-fallback-failures.js +37 -3
  92. package/dist/core/model-fallback-failures.js.map +1 -1
  93. package/dist/core/model-runtime-streaming.js +2 -2
  94. package/dist/core/model-runtime-streaming.js.map +1 -1
  95. package/dist/core/provider-composer.d.ts.map +1 -1
  96. package/dist/core/provider-composer.js +19 -11
  97. package/dist/core/provider-composer.js.map +1 -1
  98. package/dist/core/provider-rate-limits.d.ts +55 -6
  99. package/dist/core/provider-rate-limits.d.ts.map +1 -1
  100. package/dist/core/provider-rate-limits.js +182 -29
  101. package/dist/core/provider-rate-limits.js.map +1 -1
  102. package/dist/core/provider-request-admission.d.ts.map +1 -1
  103. package/dist/core/provider-request-admission.js +42 -18
  104. package/dist/core/provider-request-admission.js.map +1 -1
  105. package/dist/core/storage/node-sqlite.d.ts.map +1 -1
  106. package/dist/core/storage/node-sqlite.js +176 -19
  107. package/dist/core/storage/node-sqlite.js.map +1 -1
  108. package/dist/core/storage/provider.d.ts +4 -2
  109. package/dist/core/storage/provider.d.ts.map +1 -1
  110. package/dist/core/storage/provider.js +94 -5
  111. package/dist/core/storage/provider.js.map +1 -1
  112. package/dist/core/structured-output/index.d.ts.map +1 -1
  113. package/dist/core/structured-output/index.js +15 -8
  114. package/dist/core/structured-output/index.js.map +1 -1
  115. package/dist/core/structured-output/provider-failure.d.ts +17 -0
  116. package/dist/core/structured-output/provider-failure.d.ts.map +1 -0
  117. package/dist/core/structured-output/provider-failure.js +28 -0
  118. package/dist/core/structured-output/provider-failure.js.map +1 -0
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js +13 -7
  121. package/dist/core/tools/bash.js.map +1 -1
  122. package/dist/core/tools/shell-task-render.d.ts +1 -0
  123. package/dist/core/tools/shell-task-render.d.ts.map +1 -1
  124. package/dist/core/tools/shell-task-render.js +7 -2
  125. package/dist/core/tools/shell-task-render.js.map +1 -1
  126. package/dist/extensions/tool-search/tool.d.ts +5 -0
  127. package/dist/extensions/tool-search/tool.d.ts.map +1 -1
  128. package/dist/extensions/tool-search/tool.js +38 -5
  129. package/dist/extensions/tool-search/tool.js.map +1 -1
  130. package/dist/index.d.ts +1 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +1 -1
  133. package/dist/index.js.map +1 -1
  134. package/dist/modes/interactive/components/chat-session-host-actions.js +3 -2
  135. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  136. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/login-dialog.js +42 -14
  138. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  139. package/dist/modes/interactive/interactive-extension-context.js +2 -1
  140. package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
  141. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  142. package/dist/modes/interactive/interactive-startup.js +5 -6
  143. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  144. package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
  145. package/dist/modes/rpc/rpc-client-process.js +8 -1
  146. package/dist/modes/rpc/rpc-client-process.js.map +1 -1
  147. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  148. package/dist/modes/rpc/rpc-client.js +7 -1
  149. package/dist/modes/rpc/rpc-client.js.map +1 -1
  150. package/dist/utils/ansi.d.ts +2 -0
  151. package/dist/utils/ansi.d.ts.map +1 -1
  152. package/dist/utils/ansi.js +5 -0
  153. package/dist/utils/ansi.js.map +1 -1
  154. package/docs/codemode.md +1 -1
  155. package/docs/intercom/reference.md +3 -3
  156. package/docs/models/evals.md +31 -12
  157. package/docs/models/reference.md +23 -51
  158. package/docs/models.md +3 -4
  159. package/docs/providers.md +5 -15
  160. package/docs/settings.md +1 -1
  161. package/docs/storage.md +2 -2
  162. package/docs/tools.md +1 -1
  163. package/docs/workflows/api-reference.md +20 -1
  164. package/docs/workflows/operations.md +17 -4
  165. package/npm-shrinkwrap.json +64 -44
  166. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.46-alpha.1] - 2026-10-27
6
+
7
+ Stages on local workers can now be watched, steered, paused and answered live; large pooled workflows stay responsive and recover cleanly from rate limits, worker crashes and resumes; and GPT-6.1 Terra is available.
8
+
9
+ - **Live chat for pooled stages:** opening a running stage on a local worker shows its live transcript. Enter steers, Ctrl+F queues a follow-up, `/workflow pause <run> <stage>` pauses just that stage, and `ctx.ui` and `ask_user_question` prompts are answered from the stage chat. When a worker dies, the chat reconnects to its replacement, a pending prompt is asked again, and every pending message is delivered exactly once.
10
+ - **Responsive at scale:** a 150-worker fan-out no longer stalls the root while children start or finish, even with the graph and a stage chat open. One child that keeps losing its worker now fails alone instead of taking its siblings with it.
11
+ - **Rate limits handled the way providers ask:** `Retry-After` is honored on every provider path, including Google and Bedrock. A quota-exhausted model moves straight to the next fallback, a chain where every model is throttled waits for the earliest cooldown, and a limit one local worker meets now holds back the other workers and the root too.
12
+ - **Failures classified by what happened:** a child workflow's failure no longer depends on its name, and SDK callers get the classification in `RunResult.failure`.
13
+ - **Pools that survive crashes and resumes:** resuming a quit root no longer fails in-flight children, journal writers are served in order instead of timing out, and workflow runs survive a deleted `jiti` cache.
14
+ - **New models:** GPT-6.1 Terra on OpenAI, Azure and Codex, with a Fast tier on the OpenAI API, and correct reasoning-effort levels for GPT-6 models on Bedrock.
15
+ - **Hardening from upstream pi:** OAuth logins accept only trusted URLs, terminal titles and the login dialog strip control characters, and the CLI auth file is written with mode 0600.
16
+
17
+ ### Added
18
+
19
+ - GPT-6.1 Terra is available as `openai/gpt-6.1-terra`, `openai-codex/gpt-6.1-terra` and `azure/gpt-6.1-terra`, with a derived `openai/gpt-6.1-terra-fast` choice. It supports `off` through `max` (not `minimal`), and `openai-codex/gpt-6.1-terra` has a 500K context window ([pi 363ad78b](https://github.com/earendil-works/pi/commit/363ad78bf0625e1dfbe848429782f5e07bb1584e))
20
+ - `@bastani/atomic` exports `observeProviderRateLimits()`, `providerRateLimitObservations()` and `applyProviderRateLimitObservation()` with the `ProviderRateLimitObservation` type, so another process on the same host can apply the provider and model cooldowns and quota-exhausted marks this process recorded. An applied limit only ever extends the matching provider or model limit and counts no failure ([#4023](https://github.com/bastani-inc/atomic/issues/4023))
21
+
22
+ ### Fixed
23
+
24
+ - The `Full output:` path of a truncated shell result, both a foreground command and a `wait` on a background task, now sits on its own line and links to the file. Before, it followed its label on the same line, so word wrap could split a path containing spaces (such as `C:\Users\Jane Doe\AppData\Local\Temp\…`) partway through and the copied path no longer worked. A path wider than the terminal still wraps, but its link still opens the whole file ([#3828](https://github.com/bastani-inc/atomic/issues/3828))
25
+ - A workflow child run whose stage had launched a background subagent no longer fails with `This extension ctx is stale after session replacement or reload` after the stage's model fallback replaces its session. The subagent's auto-routed model check, which the child also uses to pick its next fallback model, no longer reads the replaced session. The child now moves on to its next fallback model, and the run keeps the stage's real error, such as a rate limit. A subagent that waited for a free slot and starts after the replacement no longer fails to launch: it takes its settings from the replacement session, or from the launching session's last state when that session closed with no replacement ([#3981](https://github.com/bastani-inc/atomic/issues/3981))
26
+ - When the model that picks `auto` models (the router) answers with an HTTP 429 or an exhausted quota (402), the routing error now keeps that status, a rate-limit or quota code and the server's `Retry-After` hint. Before, it reported only `provider request failed`, so a workflow stage whose routing hit a rate limit was blocked as a provider outage. Now it is blocked as a rate limit or exhausted quota. The provider's error text is still never kept ([#4031](https://github.com/bastani-inc/atomic/issues/4031))
27
+ - Provider cooldowns now honor `Retry-After` on every provider path, including Anthropic-format models, instead of guessing an exponential backoff. HTTP 402 and quota-exhaustion codes (`insufficient_quota`, `quota_exceeded`, `usage_limit*`) are classified as the new `quota_exhausted` failure kind: the account counts as unavailable until its reset, and the turn moves to a configured fallback model instead of retrying the same model. When a rate-limit response names the model that hit the limit, only that model cools down; other models on the provider stay usable. Limits that do not name a model still cool the whole provider ([#3984](https://github.com/bastani-inc/atomic/issues/3984))
28
+ - A workflow stage whose model fallback replaced its session no longer fails the replacement's prompt with `Agent is already processing. Specify streamingBehavior…` when an Intercom message reaches the stage during the swap. Like a fresh stage, the replacement now holds turn-starting messages as context for the stage prompt's turn ([#3982](https://github.com/bastani-inc/atomic/issues/3982))
29
+ - Fixed a `.ts` storage adapter that serves both `sessions.storage` and `workflows.journal` failing to import `@bastani/atomic/workflows` when sessions loaded first; the journal now gets the running Atomic's workflows module ([#3973](https://github.com/bastani-inc/atomic/issues/3973))
30
+ - ES module and TypeScript files a storage adapter imports by relative path now get the running Atomic's `@bastani/atomic/workflows`, `@bastani/atomic/storage`, and `@bastani/atomic`, as the adapter's entry file does. Before, a `.mjs` file or a `.js` file in a `"type": "module"` adapter package could resolve these imports from `node_modules` instead, so errors such as `StaleLeaseError` thrown from it failed Atomic's `instanceof` checks. CommonJS files other than the entry file still resolve these imports from `node_modules` ([#3972](https://github.com/bastani-inc/atomic/issues/3972))
31
+ - Fixed subagents launched with `context: "fork"` finishing instantly as "completed" with the parent's last message as their output. A forked child's history now stops before the parent's in-flight turn that is launching it, so providers that sign thinking no longer reject the child's first request as a modified turn. A child's output now comes only from messages it produced itself, and a child whose own turns all failed, or that ran none, is reported as failed with the provider's error instead of completing with inherited text. A child that completed a turn without final text, such as a tool-only child, still completes ([#3987](https://github.com/bastani-inc/atomic/issues/3987))
32
+ - GitHub Copilot offers fewer models that fail on the first request with `model_not_available_for_integrator`: models enabled during `/login` are offered only if the model list re-read afterwards marks them usable, and an older login without a stored model list re-reads it on the next networked model refresh (until then, including offline, it keeps offering the catalog). Models you define for `github-copilot` in `models.json` stay offered. If Copilot still rejects a model this way, a fallback chain moves on to its next model, and the error reads "<model> isn't available to Atomic's GitHub Copilot integration; pick another model or run /login to refresh the model list" followed by the raw error ([#3998](https://github.com/bastani-inc/atomic/issues/3998))
33
+ - `tool_search` and codemode `searchTools()` rank a tool first when the query is its name, either the full name or the part after a `__` separator (`thread_read` for `mcp__app__thread_read`). Before, a shorter description that shared the name's words could rank above it ([pi #11331](https://github.com/earendil-works/pi/issues/11331))
34
+ - Writers to one SQLite journal in different processes now take its write lock in turn, so a busy process that commits and begins again can no longer starve the others, and a transaction waiting for the lock fails only when the lock is held for the whole lock wait with no other connection committing, instead of after 30 s however busy the lock was with real work. Large local worker pools no longer lose children to `database is locked … gave up after waiting 30000 ms for the write lock` under contention ([#3978](https://github.com/bastani-inc/atomic/issues/3978))
35
+ - The login dialog opens only web (`http:`/`https:`) sign-in URLs, and no longer prints control characters from sign-in, device-code or info URLs, which could end a terminal hyperlink and send escape sequences to the terminal. The terminal title likewise replaces control characters in session names and extension titles ([pi 89c921d8](https://github.com/earendil-works/pi/commit/89c921d8b56b59e768b26e7f48b6832978119310))
36
+ - A Bedrock `ServiceQuotaExceededException` and a Google error body reporting a per-day quota violation now count as an exhausted quota, so model fallback moves straight to the next model instead of retrying the same one, and a throttled Bedrock request (`ThrottlingException`, shown as `Throttling error: …`) now counts as a rate limit, so its `Retry-After` cools the model down for other requests. The classifier also reads the error code a provider failure diagnostic records.
37
+ - `tool_search` and codemode `searchTools()` now find a tool whose name is made only of stop words (for example `is`) when the query names it; other stop-word-only queries still return nothing.
38
+
39
+ ## [0.9.45] - 2026-10-25
40
+
41
+ Custom storage adapters now plug into workflow journals as first-class citizens, and large local worker pools start reliably on busy machines.
42
+
43
+ - **Journal adapters that build on Atomic's own journal:** a storage adapter loaded by path can import `@bastani/atomic/workflows` and get the running Atomic's module, so `SqlRunJournal` subclasses and journal errors such as `StaleLeaseError` work instead of failing with `Storage unavailable` ([#3963](https://github.com/bastani-inc/atomic/issues/3963), [#3969](https://github.com/bastani-inc/atomic/pull/3969))
44
+ - **Fast status, resume and receipts for custom journals:** a run journal that implements the optional `changeToken()`, `runVersions()`, `snapshotTail()` and `appendMany()` members gets the same faster `/workflow status`, resume and batched delivery receipts as the built-in SQL journal ([#3964](https://github.com/bastani-inc/atomic/issues/3964), [#3968](https://github.com/bastani-inc/atomic/pull/3968))
45
+ - **Worker pools that start on small or busy machines:** without an explicit `workflows.localWorkers.startupTimeoutMs`, the startup deadline scales with pool size and machine load instead of failing after 30 s ([#3962](https://github.com/bastani-inc/atomic/issues/3962), [#3965](https://github.com/bastani-inc/atomic/pull/3965))
46
+
47
+ ### Added
48
+
49
+ - `StorageProvider` journal factories accept `hostModules`, and `loadStorageAdapter()` takes an optional third `hostModules` argument, to give adapter modules extra live host modules by specifier ([#3963](https://github.com/bastani-inc/atomic/issues/3963))
50
+ - The `RunJournal` interface in `@bastani/atomic/storage` has optional `changeToken()`, `runVersions()`, `snapshotTail()` and `appendMany()` members, with the `RunVersion`, `RunSnapshotTail` and `RunJournalBatchAppend` types. A run journal from a storage adapter, or any custom `RunJournal`, that implements them gets the same faster `/workflow status`, resume and batched delivery receipts as the built-in SQL journal, and `registerRunJournalConformance` checks them when present ([#3964](https://github.com/bastani-inc/atomic/issues/3964))
51
+
52
+ ### Fixed
53
+
54
+ - A journal adapter loaded by path (`ATOMIC_WORKFLOWS_JOURNAL`, `workflows.journal`, or a run's `journal: { adapter }`) can now import `@bastani/atomic/workflows`. It gets the workflows module of the running Atomic that opened the journal, so an `openRunJournal` hook can build on `SqlRunJournal` and its errors, such as `StaleLeaseError`, pass Atomic's `instanceof` checks. Before, the import resolved to a path inside `dist/index.js` and the journal failed with `Storage unavailable` ([#3963](https://github.com/bastani-inc/atomic/issues/3963))
55
+
5
56
  ## [0.9.45-alpha.1] - 2026-10-25
6
57
 
7
58
  Custom storage adapters now plug into workflow journals as first-class citizens, and large local worker pools start reliably on busy machines.
@@ -4,6 +4,17 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.46-alpha.1] - 2026-10-27
8
+
9
+ ### Fixed
10
+
11
+ - The session that launched a workflow run can now send to that run's stages by exact path (`workflow:<rootRunId>/<stage>`) and by pattern (`workflow:<rootRunId>/**`), and sees them in `intercom list`, without joining the run's invocation group. Before, the send was refused with `Target workflow run is in a different intercom group`, because the run's route is registered on a separate connection and the broker could not tell that the sender had launched the run. The launching session now proves this with a secret that only its own process holds, never the run's stages. Its group memberships do not change, so the run's stages still cannot message it. A session that joins `workflow:<rootRunId>` keeps the same authority, and every other session is still refused, now with an error that names the group to join ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
12
+ - A send to a recipient that is still saving the message, such as a stage on a local worker whose journal commit waits behind other writers, now waits for that save instead of reporting after 8 s that the outcome is uncertain. The recipient reports progress while it saves; a recipient that dies stops reporting, so its sender still hears the outcome is uncertain, and no save holds a sender longer than 2 minutes. This also holds when the recipient is connected through another broker node ([#3983](https://github.com/bastani-inc/atomic/issues/3983))
13
+ - A connection to the Intercom broker that the operating system refuses because the broker socket's listen backlog is full, as happens when a large pooled fan-out starts many sessions at once, is now retried briefly instead of failing that session's Intercom ([#3904](https://github.com/bastani-inc/atomic/issues/3904))
14
+ - A session that launched a workflow run, or that joined `workflow:<rootRunId>` from another group, now receives the failure notice when a message it queued to one of the run's pending stages becomes undeliverable (for example because the stage was skipped). The notice used to reach only senders registered in the run's own group, so these senders never learned their message was dropped. The notice still goes only to the session that sent the message ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
15
+ - A send to a session on another broker node that quits cleanly while the send is being routed is now queued for that session and delivered when it reconnects, as a send made after the quit is. Before, when the sender's broker still saw the session as connected, the sender waited 8 s for a receipt that could never come and was told the outcome was uncertain, although the message had not reached anyone.
16
+ - A workflow stage registered outside a run (another run's stage, or an isolated subgroup stage of the same run) that tries to send to the run's stages is now told `A workflow stage in <its group> cannot send to stages of workflow:<rootRunId>; joining that group does not grant it control`, instead of being told to join a group that would not help ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
17
+
7
18
  ## [0.9.44] - 2026-10-25
8
19
 
9
20
  ### Changed
@@ -72,7 +72,7 @@ import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
72
72
  import { SupervisorChannelCache } from "./supervisor-channel.js";
73
73
  import { SessionIdReclaim } from "./session-id-reclaim.js";
74
74
  import { type HeldSend, RestoringRecipients } from "./restoring-recipients.js";
75
- import { hasGroup, normalizeGroup, normalizeGroups } from "../group.js";
75
+ import { hasGroup, normalizeGroup, normalizeGroups, workflowInvocationRefusalFor } from "../group.js";
76
76
  import { handleBrokerPresence } from "./presence-handler.js";
77
77
  import {
78
78
  knownGroupSummaries,
@@ -121,6 +121,11 @@ interface PendingStageRouteRegistration {
121
121
  readonly capability: string;
122
122
  /** Registered by a restarted owner for an interrupted run; yields to the process that resumes it (#3760). */
123
123
  readonly dormant?: true;
124
+ /**
125
+ * #4006: held only by the process that launched the run, never by its stages. Presenting it in
126
+ * `claim_workflow_invocation` grants that session the invocation's control without membership.
127
+ */
128
+ readonly launcherCapability?: string;
124
129
  }
125
130
 
126
131
  interface WorkflowRosterRegistration {
@@ -347,6 +352,11 @@ class IntercomBroker {
347
352
  private liveWorkflowStageRoutes = new Map<string, LiveWorkflowStageRouteRegistration>();
348
353
  private workflowRosters = new Map<string, WorkflowRosterRegistration>();
349
354
  private liveWorkflowStageRouteActivations = new Map<string, LiveWorkflowStageRouteActivation>();
355
+ /**
356
+ * #4006: runs each connected session launched, proven with the run's launcher capability. Keyed by session,
357
+ * then run, holding the capability claimed; a claim lapses when the run's route carries another one.
358
+ */
359
+ private workflowInvocationClaims = new Map<string, Map<string, string>>();
350
360
  /** Multi-node S5: set only when the agent directory's settings select a top-level shared `storage`. */
351
361
  private readonly sharedStorageSetting = readSharedStorageSetting(dirname(INTERCOM_DIR));
352
362
  private readonly routeRemote: RemoteSendRouter | undefined =
@@ -649,17 +659,57 @@ class IntercomBroker {
649
659
  }
650
660
 
651
661
  /**
652
- * Invocation control may come from the invocation owner or from an ordinary
653
- * host session that explicitly joined the invocation group. A workflow stage
654
- * registered under any workflow group cannot turn mutable membership into a
655
- * parent-control capability for a sibling (or for another workflow run).
662
+ * Invocation control may come from the invocation owner, from the session that
663
+ * launched the run (#4006), or from an ordinary host session that explicitly
664
+ * joined the invocation group. A workflow stage registered under any workflow
665
+ * group cannot turn mutable membership into a parent-control capability for a
666
+ * sibling (or for another workflow run).
656
667
  */
657
668
  private canControlWorkflowInvocation(sender: ConnectedSession, invocationGroup: string): boolean {
669
+ return this.controlsWorkflowInvocationByMembership(sender, invocationGroup) || this.launchedWorkflowInvocation(sender, invocationGroup);
670
+ }
671
+
672
+ private controlsWorkflowInvocationByMembership(sender: ConnectedSession, invocationGroup: string): boolean {
658
673
  if (!hasGroup(sessionGroups(sender.info), invocationGroup)) return false;
659
674
  const registrationGroup = normalizeGroup(sender.registrationGroup);
660
675
  return registrationGroup === invocationGroup || !registrationGroup.startsWith("workflow:");
661
676
  }
662
677
 
678
+ /** #4006: the sender holds a current claim on a run whose route is registered under `invocationGroup`. */
679
+ private launchedWorkflowInvocation(sender: ConnectedSession, invocationGroup: string): boolean {
680
+ const claims = this.workflowInvocationClaims.get(sender.info.id);
681
+ if (claims === undefined) return false;
682
+ const group = normalizeGroup(invocationGroup);
683
+ for (const [runId, capability] of claims) {
684
+ const route = this.pendingStageRoutes.get(runId);
685
+ if (route?.launcherCapability === capability && normalizeGroup(route.group) === group) return true;
686
+ }
687
+ return false;
688
+ }
689
+
690
+ /**
691
+ * Only the holder of a run's launcher capability can claim to have launched it. That capability is
692
+ * separate from the route capability every stage of the run holds, so a stage cannot claim.
693
+ */
694
+ private claimWorkflowInvocation(sessionId: string, runId: string, capability: string): boolean {
695
+ const route = this.pendingStageRoutes.get(runId);
696
+ if (
697
+ route?.launcherCapability === undefined ||
698
+ route.launcherCapability !== capability ||
699
+ !normalizeGroup(route.group).startsWith("workflow:")
700
+ ) {
701
+ return false;
702
+ }
703
+ const claims = this.workflowInvocationClaims.get(sessionId) ?? new Map<string, string>();
704
+ // Claims on runs whose routes are gone can never authorize again.
705
+ for (const claimedRunId of claims.keys()) {
706
+ if (!this.pendingStageRoutes.has(claimedRunId)) claims.delete(claimedRunId);
707
+ }
708
+ claims.set(runId, capability);
709
+ this.workflowInvocationClaims.set(sessionId, claims);
710
+ return true;
711
+ }
712
+
663
713
  private canControlLiveWorkflowStage = (
664
714
  sender: ConnectedSession,
665
715
  target: ConnectedSession,
@@ -723,7 +773,12 @@ class IntercomBroker {
723
773
  const entries: WorkflowFutureStageRosterEntry[] = [];
724
774
  for (const [runId, roster] of this.workflowRosters) {
725
775
  if (roster.possibleStages === undefined || roster.possibleStages.length === 0) continue;
726
- if (![...requesterGroups].some((group) => invocationOwnsGroup(roster.group, group))) continue;
776
+ if (
777
+ ![...requesterGroups].some((group) => invocationOwnsGroup(roster.group, group)) &&
778
+ !this.launchedWorkflowInvocation(requester, roster.group)
779
+ ) {
780
+ continue;
781
+ }
727
782
  if (selected !== undefined && selected !== roster.group) continue;
728
783
  for (const row of roster.possibleStages) {
729
784
  if (this.isNonAgentWorkflowTarget(row.target)) continue;
@@ -899,10 +954,15 @@ class IntercomBroker {
899
954
  type: "delivery_failed",
900
955
  messageId: route.message.id,
901
956
  ...(route.attemptId ? { attemptId: route.attemptId } : {}),
902
- reason: "Target workflow run is in a different intercom group",
957
+ // A workflow stage registered outside the invocation can't gain control by joining; say so.
958
+ reason: workflowInvocationRefusalFor(route.from.registrationGroup, ownerRegistration.group),
903
959
  });
904
960
  return true;
905
961
  }
962
+ // #4006: the run's host checks membership again before queueing; say when the broker authorized the launcher instead.
963
+ const invocationControl =
964
+ !this.controlsWorkflowInvocationByMembership(route.from, ownerRegistration.group) &&
965
+ this.launchedWorkflowInvocation(route.from, ownerRegistration.group);
906
966
  const requestId = randomUUID();
907
967
  const timeout = setTimeout(() => {
908
968
  const pending = this.pendingStageAcknowledgments.get(requestId);
@@ -944,6 +1004,7 @@ class IntercomBroker {
944
1004
  target: route.target,
945
1005
  message: route.message,
946
1006
  ...(route.liveTargetId === undefined ? {} : { live: true }),
1007
+ ...(invocationControl ? { invocationControl: true as const } : {}),
947
1008
  })
948
1009
  ) {
949
1010
  // The owner's socket stopped accepting frames between resolving the route
@@ -1220,6 +1281,14 @@ class IntercomBroker {
1220
1281
  reason: UNCERTAIN_RECEIPT_REASON,
1221
1282
  });
1222
1283
  },
1284
+ hold: (waiting, timeoutMs) => {
1285
+ writeMessageIfOpen(waiting.socket, {
1286
+ type: "send_held",
1287
+ messageId: pending.messageId,
1288
+ ...(waiting.attemptId === undefined ? {} : { attemptId: waiting.attemptId }),
1289
+ timeoutMs,
1290
+ });
1291
+ },
1223
1292
  },
1224
1293
  sender,
1225
1294
  );
@@ -1375,8 +1444,7 @@ class IntercomBroker {
1375
1444
  target === undefined ||
1376
1445
  !isAgentRecipient(target.info) ||
1377
1446
  target.info.id === currentId ||
1378
- normalizeGroup(target.registrationGroup) !== routeGroup ||
1379
- !hasGroup(sessionGroups(target.info), routeGroup)
1447
+ !this.canReceivePendingStageNotification(target, clientMessage.to, routeGroup)
1380
1448
  ) {
1381
1449
  this.failPendingStageNotification(socket, message.id, attemptId, "Session not found");
1382
1450
  return;
@@ -1459,6 +1527,19 @@ class IntercomBroker {
1459
1527
  }
1460
1528
  }
1461
1529
 
1530
+ /**
1531
+ * A pending-stage notice reaches a session registered in the run's group, or (#4006) the exact
1532
+ * sender session that controls the run from outside it: the launcher holding a current claim, or
1533
+ * an ordinary session that joined `workflow:<rootRunId>`. Isolated stages never qualify, and the
1534
+ * legacy name/return-address fallbacks stay limited to the run's own group.
1535
+ */
1536
+ private canReceivePendingStageNotification(target: ConnectedSession, to: string, routeGroup: string): boolean {
1537
+ if (normalizeGroup(target.registrationGroup) === routeGroup && hasGroup(sessionGroups(target.info), routeGroup)) {
1538
+ return true;
1539
+ }
1540
+ return target.info.id === to && this.canControlWorkflowInvocation(target, routeGroup);
1541
+ }
1542
+
1462
1543
  private settlePendingStageNotification(
1463
1544
  currentId: string,
1464
1545
  requestId: string,
@@ -1636,6 +1717,8 @@ class IntercomBroker {
1636
1717
  acknowledgesPendingStageRoutes: true as const,
1637
1718
  refusesPendingStageRoutesInPlace: true as const,
1638
1719
  brokerStatus: true as const,
1720
+ claimsWorkflowInvocations: true as const,
1721
+ extendsSaves: true as const,
1639
1722
  });
1640
1723
  if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
1641
1724
  // #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
@@ -1835,10 +1918,13 @@ class IntercomBroker {
1835
1918
  typeof clientMessage.group !== "string" ||
1836
1919
  typeof clientMessage.capability !== "string" ||
1837
1920
  !clientMessage.capability ||
1838
- (clientMessage.requestId !== undefined && typeof clientMessage.requestId !== "string")
1921
+ (clientMessage.requestId !== undefined && typeof clientMessage.requestId !== "string") ||
1922
+ (clientMessage.launcherCapability !== undefined &&
1923
+ (typeof clientMessage.launcherCapability !== "string" || !clientMessage.launcherCapability))
1839
1924
  ) {
1840
1925
  throw new Error("Invalid pending-stage route registration");
1841
1926
  }
1927
+ const launcherCapability = clientMessage.launcherCapability;
1842
1928
  const routeRequestId = clientMessage.requestId;
1843
1929
  // The route is applied synchronously, so this answer proves the update is visible to every later request.
1844
1930
  const acknowledgeRoute = (): void => {
@@ -1955,6 +2041,7 @@ class IntercomBroker {
1955
2041
  group: ownerGroup,
1956
2042
  capability: clientMessage.capability,
1957
2043
  ...(dormant ? { dormant: true as const } : {}),
2044
+ ...(launcherCapability === undefined ? {} : { launcherCapability }),
1958
2045
  });
1959
2046
  if (yieldedDormant !== undefined) {
1960
2047
  const revoked = this.sessions.get(yieldedDormant.sessionId);
@@ -1975,6 +2062,24 @@ class IntercomBroker {
1975
2062
  break;
1976
2063
  }
1977
2064
 
2065
+ case "claim_workflow_invocation": {
2066
+ if (
2067
+ typeof clientMessage.requestId !== "string" ||
2068
+ typeof clientMessage.runId !== "string" ||
2069
+ typeof clientMessage.capability !== "string" ||
2070
+ !clientMessage.capability ||
2071
+ currentId === null
2072
+ ) {
2073
+ throw new Error("Invalid workflow invocation claim");
2074
+ }
2075
+ writeMessageIfOpen(socket, {
2076
+ type: "workflow_invocation_claimed",
2077
+ requestId: clientMessage.requestId,
2078
+ claimed: this.claimWorkflowInvocation(currentId, clientMessage.runId, clientMessage.capability),
2079
+ });
2080
+ break;
2081
+ }
2082
+
1978
2083
  case "register_live_workflow_stage_route": {
1979
2084
  if (
1980
2085
  typeof clientMessage.requestId !== "string" ||
@@ -2092,6 +2197,14 @@ class IntercomBroker {
2092
2197
  break;
2093
2198
  }
2094
2199
 
2200
+ case "saving": {
2201
+ if (typeof clientMessage.messageId !== "string") throw new Error("Invalid saving message");
2202
+ const recipient = this.sessions.get(currentId);
2203
+ if (recipient?.acknowledgesReceipt === true && !this.multiNode?.saving(recipient, clientMessage.messageId))
2204
+ this.receipts.extend(clientMessage.messageId, deliveryTargetIdentity(recipient));
2205
+ break;
2206
+ }
2207
+
2095
2208
  case "send_pending_stage_notification": {
2096
2209
  this.handlePendingStageNotificationSend(socket, clientMessage, currentId);
2097
2210
  break;
@@ -2384,6 +2497,7 @@ class IntercomBroker {
2384
2497
  for (const [requestId, activation] of this.liveWorkflowStageRouteActivations) {
2385
2498
  if (activation.sessionId === sessionId) this.liveWorkflowStageRouteActivations.delete(requestId);
2386
2499
  }
2500
+ this.workflowInvocationClaims.delete(sessionId);
2387
2501
  this.sessions.delete(sessionId);
2388
2502
  this.multiNode?.sessionLeft(departed);
2389
2503
  this.sessionIdReclaim.retire(sessionId, departed.registrationReturnAddress);