@bastani/atomic 0.9.43 → 0.9.44-alpha.2

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 (137) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +22 -0
  3. package/dist/builtin/intercom/broker/broker.ts +31 -7
  4. package/dist/builtin/intercom/broker/client-message-validation.ts +6 -0
  5. package/dist/builtin/intercom/broker/client.ts +69 -0
  6. package/dist/builtin/intercom/broker/send-signature.ts +4 -1
  7. package/dist/builtin/intercom/index.bundle.mjs +309 -109
  8. package/dist/builtin/intercom/live-route-refusal.ts +20 -0
  9. package/dist/builtin/intercom/package.json +1 -1
  10. package/dist/builtin/intercom/skills/intercom/SKILL.md +11 -6
  11. package/dist/builtin/intercom/types.ts +28 -1
  12. package/dist/builtin/mcp/CHANGELOG.md +7 -0
  13. package/dist/builtin/mcp/package.json +1 -1
  14. package/dist/builtin/subagents/CHANGELOG.md +9 -0
  15. package/dist/builtin/subagents/README.md +8 -8
  16. package/dist/builtin/subagents/agents/worker.md +1 -1
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/skills/agent-browser/SKILL.md +1 -1
  19. package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +21 -2
  20. package/dist/builtin/subagents/skills/cua-driver/LINUX.md +2 -2
  21. package/dist/builtin/subagents/skills/cua-driver/MACOS.md +342 -10
  22. package/dist/builtin/subagents/skills/cua-driver/README.md +4 -3
  23. package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +3 -1
  24. package/dist/builtin/subagents/skills/cua-driver/SKILL.md +7 -4
  25. package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +6 -5
  26. package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +15 -0
  27. package/dist/builtin/subagents/skills/herdr/SKILL.md +22 -10
  28. package/dist/builtin/subagents/skills/subagent/SKILL.md +8 -5
  29. package/dist/builtin/subagents/src/extension/index.bundle.mjs +6 -6
  30. package/dist/builtin/web-access/package.json +1 -1
  31. package/dist/builtin/workflows/CHANGELOG.md +20 -0
  32. package/dist/builtin/workflows/README.md +1 -1
  33. package/dist/builtin/workflows/builtin/{chunk-frqxd4d1.js → chunk-k8d7ehjx.js} +22 -0
  34. package/dist/builtin/workflows/builtin/{chunk-3s95f7ry.js → chunk-qxpymnaf.js} +1 -1
  35. package/dist/builtin/workflows/builtin/{chunk-edhf30ef.js → chunk-zjecnjxx.js} +1 -1
  36. package/dist/builtin/workflows/builtin/goal.js +2 -2
  37. package/dist/builtin/workflows/builtin/index.js +3 -3
  38. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  39. package/dist/builtin/workflows/package.json +1 -1
  40. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1844 -603
  41. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +1880 -605
  42. package/dist/builtin/workflows/src/index.js +1896 -655
  43. package/dist/builtin/workflows/src/local-worker-entry.mjs +1880 -605
  44. package/dist/cli/args.js +1 -1
  45. package/dist/cli/args.js.map +1 -1
  46. package/dist/core/agent-session-custom-message-commit.d.ts.map +1 -1
  47. package/dist/core/agent-session-custom-message-commit.js +59 -0
  48. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  49. package/dist/core/agent-session-methods.d.ts +1 -0
  50. package/dist/core/agent-session-methods.d.ts.map +1 -1
  51. package/dist/core/agent-session-methods.js.map +1 -1
  52. package/dist/core/agent-session-prompt.d.ts +4 -0
  53. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  54. package/dist/core/agent-session-prompt.js +10 -2
  55. package/dist/core/agent-session-prompt.js.map +1 -1
  56. package/dist/core/agent-session.d.ts +2 -0
  57. package/dist/core/agent-session.d.ts.map +1 -1
  58. package/dist/core/agent-session.js +9 -0
  59. package/dist/core/agent-session.js.map +1 -1
  60. package/dist/core/builtin-packages.d.ts.map +1 -1
  61. package/dist/core/builtin-packages.js +13 -13
  62. package/dist/core/builtin-packages.js.map +1 -1
  63. package/dist/core/exec.d.ts.map +1 -1
  64. package/dist/core/exec.js +11 -2
  65. package/dist/core/exec.js.map +1 -1
  66. package/dist/core/package-manager-resolver.d.ts.map +1 -1
  67. package/dist/core/package-manager-resolver.js +25 -14
  68. package/dist/core/package-manager-resolver.js.map +1 -1
  69. package/dist/core/package-manager-types.d.ts +8 -0
  70. package/dist/core/package-manager-types.d.ts.map +1 -1
  71. package/dist/core/package-manager-types.js.map +1 -1
  72. package/dist/core/package-manager.d.ts +1 -1
  73. package/dist/core/package-manager.d.ts.map +1 -1
  74. package/dist/core/package-manager.js.map +1 -1
  75. package/dist/core/resource-loader-extension-warnings.d.ts +4 -1
  76. package/dist/core/resource-loader-extension-warnings.d.ts.map +1 -1
  77. package/dist/core/resource-loader-extension-warnings.js +15 -0
  78. package/dist/core/resource-loader-extension-warnings.js.map +1 -1
  79. package/dist/core/resource-loader-package-resources.d.ts.map +1 -1
  80. package/dist/core/resource-loader-package-resources.js +1 -0
  81. package/dist/core/resource-loader-package-resources.js.map +1 -1
  82. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  83. package/dist/core/resource-loader-reload.js +5 -3
  84. package/dist/core/resource-loader-reload.js.map +1 -1
  85. package/dist/extensions/mcp/client/oauth/flow.d.ts.map +1 -1
  86. package/dist/extensions/mcp/client/oauth/flow.js +4 -1
  87. package/dist/extensions/mcp/client/oauth/flow.js.map +1 -1
  88. package/dist/extensions/mcp/client/oauth/types.d.ts +1 -0
  89. package/dist/extensions/mcp/client/oauth/types.d.ts.map +1 -1
  90. package/dist/extensions/mcp/client/oauth/types.js +8 -4
  91. package/dist/extensions/mcp/client/oauth/types.js.map +1 -1
  92. package/dist/extensions/mcp/client/transports/streamable-http.d.ts.map +1 -1
  93. package/dist/extensions/mcp/client/transports/streamable-http.js +2 -1
  94. package/dist/extensions/mcp/client/transports/streamable-http.js.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/main.d.ts.map +1 -1
  99. package/dist/main.js +20 -10
  100. package/dist/main.js.map +1 -1
  101. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/model-selector.js +4 -2
  103. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  104. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/scoped-models-selector.js +4 -2
  106. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  107. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  108. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/tool-execution.js +11 -1
  110. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  111. package/dist/modes/interactive/model-search.d.ts +10 -0
  112. package/dist/modes/interactive/model-search.d.ts.map +1 -1
  113. package/dist/modes/interactive/model-search.js +21 -0
  114. package/dist/modes/interactive/model-search.js.map +1 -1
  115. package/dist/package-manager-cli.d.ts.map +1 -1
  116. package/dist/package-manager-cli.js +5 -0
  117. package/dist/package-manager-cli.js.map +1 -1
  118. package/dist/utils/tools-manager.d.ts.map +1 -1
  119. package/dist/utils/tools-manager.js +30 -35
  120. package/dist/utils/tools-manager.js.map +1 -1
  121. package/docs/computer-use.md +5 -5
  122. package/docs/custom-provider/streaming.md +9 -14
  123. package/docs/environment-variables.md +1 -0
  124. package/docs/intercom/operations.md +1 -1
  125. package/docs/intercom.md +16 -2
  126. package/docs/packages.md +2 -0
  127. package/docs/settings.md +2 -2
  128. package/docs/skills.md +2 -2
  129. package/docs/subagents.md +47 -10
  130. package/docs/workflows/api-reference.md +1 -1
  131. package/docs/workflows/authoring.md +1 -1
  132. package/docs/workflows/operations.md +8 -6
  133. package/docs/workflows/reliable-design.md +23 -5
  134. package/examples/extensions/custom-provider-anthropic/index.ts +31 -31
  135. package/examples/extensions/todo.ts +7 -0
  136. package/npm-shrinkwrap.json +32 -32
  137. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.44-alpha.2] - 2026-10-24
6
+
7
+ Workflow swarms now message each other without cancelling each other's work, and stages, replies and session exits behave reliably under fan-out.
8
+
9
+ - **Messages no longer cancel a peer's work:** Intercom `send` and `ask` now default to `"steer"`, so a message reaches a busy subagent or workflow stage after its current tool calls finish; pass `delivery: "followUp"` to wait until it stops, or `"interrupt"` for the old cancelling behavior ([#3921](https://github.com/bastani-inc/atomic/issues/3921), [#3924](https://github.com/bastani-inc/atomic/pull/3924))
10
+ - **Replies reach the right sender:** an implicit Intercom `reply` answers only messages the agent has actually seen, and every ask delivered in the same turn can still be answered ([#3921](https://github.com/bastani-inc/atomic/issues/3921), [#3927](https://github.com/bastani-inc/atomic/pull/3927))
11
+ - **Workflow stages start reliably:** a message that reaches a stage before its own prompt becomes context for the first turn instead of failing the stage with `Agent is already processing` ([#3919](https://github.com/bastani-inc/atomic/issues/3919), [#3923](https://github.com/bastani-inc/atomic/pull/3923))
12
+ - **Smaller journals under broadcast:** sticky broadcasts to many stages record only what changed, so the workflow journal grows linearly instead of quadratically ([#3920](https://github.com/bastani-inc/atomic/issues/3920), [#3922](https://github.com/bastani-inc/atomic/pull/3922))
13
+ - **Clean exits and binary startup:** closing a session while background subagents finish no longer fails with `Cannot dispose a session with a queued protected reconciliation`, and the standalone binary starts the Intercom broker for pooled stages even when a build tree exists on the host ([#3925](https://github.com/bastani-inc/atomic/issues/3925), [#3926](https://github.com/bastani-inc/atomic/pull/3926), [#3917](https://github.com/bastani-inc/atomic/issues/3917), [#3918](https://github.com/bastani-inc/atomic/pull/3918))
14
+
15
+ ### Fixed
16
+
17
+ - The standalone binary resolves its builtin packages only from the `builtin/` directory beside the executable, so the Intercom broker starts for pooled workflow stages even when the build tree still exists on the host ([#3917](https://github.com/bastani-inc/atomic/issues/3917))
18
+ - Fixed workflow stages failing with `Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.` when an Intercom message, such as a sibling's send or a sticky `workflow:<rootRunId>/**` broadcast, reached the stage after it went live but before the workflow sent the stage's own prompt. The message started a turn of its own and the stage prompt was rejected. Messages that arrive before the stage prompt now become context for the stage's first turn, and the stage prompt always starts that turn ([#3919](https://github.com/bastani-inc/atomic/issues/3919))
19
+ - Background subagent completions that arrive while the parent session is finishing a turn now always get their reply turn, so exiting or disposing a session saved to a file no longer fails with `Cannot dispose a session with a queued protected reconciliation` ([#3925](https://github.com/bastani-inc/atomic/issues/3925))
20
+
21
+ ## [0.9.44-alpha.1] - 2026-10-24
22
+
23
+ Large agent swarms and local workflow fan-outs now run without freezing the TUI or losing their Intercom routes, and background completion notices survive a restart.
24
+
25
+ - **A responsive TUI under heavy fan-out:** local worker pools run under their own coordinator process, journals no longer grow on every heartbeat, and `/workflow status` and resume handle roots whose children failed or were skipped ([#3905](https://github.com/bastani-inc/atomic/pull/3905))
26
+ - **Intercom that keeps working during fan-out:** pooled stages wait for their run's route instead of failing with `Live workflow-stage route has no registered workflow owner`, all runs of one invocation share one broker connection, and a refused route no longer drops the other runs' routes ([#3907](https://github.com/bastani-inc/atomic/pull/3907), [#3915](https://github.com/bastani-inc/atomic/pull/3915))
27
+ - **Peer-to-peer swarms by default:** subagents and workflow stages talk to each other directly, subagent `parallel.*` limits and workflow `defaultConcurrency` default to 1000, and workflow `maxDepth` now defaults to 3; set them lower to throttle ([#3912](https://github.com/bastani-inc/atomic/pull/3912))
28
+ - **Completion notices after a restart:** background subagent and shell completions reach the parent conversation after a session resumes in a new process ([#3909](https://github.com/bastani-inc/atomic/pull/3909))
29
+ - **Faster large tool calls:** streamed tool-call arguments are parsed once per delta, so large `write` calls no longer cost quadratic CPU and memory or re-render on every delta ([#3910](https://github.com/bastani-inc/atomic/pull/3910))
30
+ - **Sturdier startup and providers:** Atomic starts when a configured package fails to install, Claude effort limits, Kimi K3.1 Pro, and Gemini `const` schemas work, and MCP sign-in accepts only `http:` and `https:` OAuth URLs ([#3910](https://github.com/bastani-inc/atomic/pull/3910))
31
+ - **Cua Driver approves its own permission prompts on macOS:** the agent clicks Cua Driver's Accessibility and Screen Recording prompts itself when VNC credentials are available; set `ATOMIC_CUA_AUTO_APPROVE=0` to keep asking you ([#3911](https://github.com/bastani-inc/atomic/pull/3911))
32
+
33
+ **Breaking change:** `appendGrammarToolInputJsonDelta()` in `@bastani/pi-ai` now takes only the new input delta and no longer closes the input; call `closeGrammarToolInputJson()` when the input is complete ([#3910](https://github.com/bastani-inc/atomic/pull/3910))
34
+
35
+ ### Added
36
+
37
+ - `/model` and `/scoped-models` now show a model's name next to its ID when the name adds words the ID lacks, such as a renamed `models.json` override ([pi #5916](https://github.com/earendil-works/pi/issues/5916), [pi #11347](https://github.com/earendil-works/pi/pull/11347))
38
+
39
+ ### Fixed
40
+
41
+ - Fixed background subagent and shell completion notices never reaching the parent conversation after the session was restarted or resumed in a new process: the new process numbers its tasks from the start again, so a completion matched an earlier one already acknowledged in the session history and was silently dropped. Each completion now carries an identity unique to its process generation, and every settled task still delivers exactly one notice ([#3908](https://github.com/bastani-inc/atomic/issues/3908))
42
+ - Atomic now starts when a package configured in `settings.json` fails to install (for example when npm cannot reach the registry), instead of crashing with an uncaught error. It skips that package, warns with the `atomic remove` command that drops it (`--local` for project packages), and tries again on the next start or `/reload`. `--help`, `--list-models` and `atomic config` show the warning too ([pi #11088](https://github.com/earendil-works/pi/issues/11088))
43
+ - `pi.exec()` no longer corrupts multi-byte UTF-8 characters split across output chunks, which came back as `�` replacement characters ([pi #11312](https://github.com/earendil-works/pi/issues/11312))
44
+ - Automatic `fd` and `rg` downloads no longer fetch x86-64 binaries on ARM32 and other architectures without a prebuilt release. Atomic now warns and recommends installing the tool with the system package manager ([pi #11209](https://github.com/earendil-works/pi/issues/11209))
45
+ - Streamed tool calls no longer re-run their call renderer on every argument delta; the interactive mode renders the latest arguments once per frame, so a large streamed `write` no longer re-highlights its whole content per delta ([pi #9265](https://github.com/earendil-works/pi/issues/9265))
46
+
5
47
  ## [0.9.43] - 2026-10-24
6
48
 
7
49
  Local workflow worker pools now survive slow, busy, and crashed workers, and large workflow journals open without running out of memory.
@@ -4,6 +4,28 @@ 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.2] - 2026-10-24
8
+
9
+ ### Changed
10
+
11
+ - Intercom `send` and `ask` take a `delivery` option, and the default is now `"steer"`: a message to a working subagent or live workflow stage arrives after the recipient's current tool calls finish instead of cancelling its work, so peers in a swarm no longer abort each other's long-running tool calls. `"followUp"` waits until the recipient finishes and stops. `"interrupt"` keeps the previous behavior of cancelling the recipient's current model call or tool call, and is now opt-in ([#3921](https://github.com/bastani-inc/atomic/issues/3921))
12
+
13
+ ### Fixed
14
+
15
+ - An implicit Intercom `reply` no longer answers a message the agent has not seen yet. Before, a queued message (for example a `"followUp"` ask that arrived while a tool ran) could become the reply target as soon as any new turn started, including the continuation after that tool call, so the reply reached the wrong sender or thread. A message now becomes the reply target only when it enters the agent's context, including when Intercom is loaded through its lazy entry point, and every ask that enters the context in the same turn (for example several `"followUp"` asks delivered together) can still be answered with an implicit `reply` after the first one is answered ([#3921](https://github.com/bastani-inc/atomic/issues/3921), follow-up to [#3924](https://github.com/bastani-inc/atomic/pull/3924))
16
+
17
+ ## [0.9.44-alpha.1] - 2026-10-24
18
+
19
+ ### Changed
20
+
21
+ - The `intercom` skill now presents unlimited peer-to-peer (swarm) coordination as the default, with a broadcast example and guidance for agents to discover and talk to peers proactively. It also covers scaling a swarm to hundreds or thousands of agents organized into teams.
22
+
23
+ ### Fixed
24
+
25
+ - Workflow stages that start while their run's Intercom route is still registering, or is registering again after its connection dropped, no longer fail every Intercom call with `Live workflow-stage route has no registered workflow owner`. The stage waits for the route to come back before it uses Intercom, instead of giving up after a few quick retries ([#3904](https://github.com/bastani-inc/atomic/issues/3904))
26
+ - Workflow route owners now register the routes of every run in a workflow invocation over one broker connection instead of opening one connection per run. The connection is per workflow invocation rather than per host because the broker grants route ownership per invocation group. All child runs of one root share that connection, whatever the fan-out size, so a large fan-out of child workflows no longer builds a connection backlog at the broker, and a run whose route moves to a new capability keeps the connection the other runs use. A run whose route the broker refuses moves to a connection of its own, so it cannot take the other runs' routes down with it. Only refused runs get such a connection, so the number of extra connections is bounded by the refused runs, not by the number of runs ([#3904](https://github.com/bastani-inc/atomic/issues/3904))
27
+ - When the Intercom broker refuses one workflow run's route, for example a route that another session owns, it now refuses that route on its own and keeps the shared route connection open, so the routes of the other runs in the same workflow invocation are no longer dropped for a reconnect. Authorization is unchanged: the same registrations are refused for the same reasons. The route owner retries only the refused run, on its own backoff. With an older broker, which still ends the connection, the refused run still moves to a connection of its own. Clients that do not ask for this, including older ones, keep the previous behavior ([#3906](https://github.com/bastani-inc/atomic/issues/3906))
28
+
7
29
  ## [0.9.43] - 2026-10-24
8
30
 
9
31
  ### Fixed
@@ -38,6 +38,7 @@ import type {
38
38
  WorkflowPossibleStageAnnouncement,
39
39
  WorkflowFutureStageRosterEntry,
40
40
  WorkflowRunParentAnnouncement,
41
+ PendingStageRouteRefusalCode,
41
42
  } from "../types.js";
42
43
  import {
43
44
  formatWorkflowStageTarget,
@@ -1622,6 +1623,7 @@ class IntercomBroker {
1622
1623
  ...(supervisorId ? { supervisorSessionId: supervisorId } : {}),
1623
1624
  ...(clientMessage.acknowledgesReceipt === true ? { acknowledgesReceipt: true as const } : {}),
1624
1625
  acknowledgesPendingStageRoutes: true as const,
1626
+ refusesPendingStageRoutesInPlace: true as const,
1625
1627
  brokerStatus: true as const,
1626
1628
  });
1627
1629
  if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
@@ -1833,6 +1835,28 @@ class IntercomBroker {
1833
1835
  writeMessageIfOpen(socket, { type: "pending_stage_route_registered", requestId: routeRequestId });
1834
1836
  }
1835
1837
  };
1838
+ // #3906: a client whose connection carries the routes of many runs asks for a refusal of one of them to be
1839
+ // answered on its own. Authorization is the same either way; only whether the refusal ends the connection differs.
1840
+ const refusesInPlace = routeRequestId !== undefined && clientMessage.refusalKeepsConnection === true;
1841
+ // #3904: the refusal names the registration it answers, so a connection carrying many runs' routes knows which one.
1842
+ const refuseRoute = (code: PendingStageRouteRefusalCode, reason: string): void => {
1843
+ if (refusesInPlace) {
1844
+ writeMessageIfOpen(socket, {
1845
+ type: "pending_stage_route_refused",
1846
+ requestId: routeRequestId,
1847
+ runId: clientMessage.runId,
1848
+ code,
1849
+ reason,
1850
+ });
1851
+ return;
1852
+ }
1853
+ writeMessageIfOpen(socket, {
1854
+ type: "registration_failed",
1855
+ reason,
1856
+ ...(routeRequestId === undefined ? {} : { requestId: routeRequestId }),
1857
+ });
1858
+ this.endRefusedConnection(socket, currentId, setId);
1859
+ };
1836
1860
  const owner = this.sessions.get(currentId);
1837
1861
  const existing = this.pendingStageRoutes.get(clientMessage.runId);
1838
1862
  const ownerGroup = normalizeGroup(owner?.registrationGroup);
@@ -1851,14 +1875,16 @@ class IntercomBroker {
1851
1875
  ? liveExisting
1852
1876
  : undefined;
1853
1877
  const activeExisting = yieldedDormant === undefined ? liveExisting : undefined;
1878
+ // #3904: the owning session may move its own route to a new capability. One connection carries
1879
+ // the routes of many runs, so refusing a rotation would end the connection and drop all of them.
1854
1880
  if (
1855
1881
  owner === undefined ||
1856
1882
  normalizeGroup(clientMessage.group) !== ownerGroup ||
1857
1883
  (activeExisting !== undefined &&
1858
- (activeExisting.capability !== clientMessage.capability || activeExisting.group !== ownerGroup))
1884
+ (activeExisting.group !== ownerGroup ||
1885
+ (activeExisting.capability !== clientMessage.capability && activeExisting.sessionId !== currentId)))
1859
1886
  ) {
1860
- writeMessageIfOpen(socket, { type: "registration_failed", reason: "Pending-stage route is not authorized" });
1861
- this.endRefusedConnection(socket, currentId, setId);
1887
+ refuseRoute("not_authorized", "Pending-stage route is not authorized");
1862
1888
  return;
1863
1889
  }
1864
1890
  if (
@@ -1870,8 +1896,7 @@ class IntercomBroker {
1870
1896
  ) ||
1871
1897
  !clientMessage.stages.every((stage) => invocationOwnsGroup(ownerGroup, stage.group)))
1872
1898
  ) {
1873
- writeMessageIfOpen(socket, { type: "registration_failed", reason: "Invalid workflow-stage roster" });
1874
- this.endRefusedConnection(socket, currentId, setId);
1899
+ refuseRoute("invalid_stage_roster", "Invalid workflow-stage roster");
1875
1900
  return;
1876
1901
  }
1877
1902
  if (
@@ -1882,8 +1907,7 @@ class IntercomBroker {
1882
1907
  normalizeGroup(clientMessage.group),
1883
1908
  )
1884
1909
  ) {
1885
- writeMessageIfOpen(socket, { type: "registration_failed", reason: "Invalid workflow possible-stage roster" });
1886
- this.endRefusedConnection(socket, currentId, setId);
1910
+ refuseRoute("invalid_possible_stage_roster", "Invalid workflow possible-stage roster");
1887
1911
  return;
1888
1912
  }
1889
1913
  // D7 (slice 4): presence replaces, absence keeps — a terminal root publishes `[]`
@@ -22,6 +22,12 @@ export function isMessage(value: unknown): value is Message {
22
22
  if (typeof message.id !== "string" || typeof message.timestamp !== "number") return false;
23
23
  if (message.replyTo !== undefined && typeof message.replyTo !== "string") return false;
24
24
  if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean") return false;
25
+ if (
26
+ message.delivery !== undefined
27
+ && message.delivery !== "steer"
28
+ && message.delivery !== "followUp"
29
+ && message.delivery !== "interrupt"
30
+ ) return false;
25
31
  if (message.replyError !== undefined && typeof message.replyError !== "string") return false;
26
32
  if (message.source !== undefined && !isMessageSource(message.source)) return false;
27
33
  if (typeof message.content !== "object" || message.content === null) return false;
@@ -17,6 +17,8 @@ import type {
17
17
  WorkflowFutureStageRosterEntry,
18
18
  WorkflowPossibleStageAnnouncement,
19
19
  WorkflowRunParentAnnouncement,
20
+ PendingStageRouteRefusalCode,
21
+ IntercomDeliveryMode,
20
22
  } from "../types.js";
21
23
  import { buildSendSignature, PendingSendRegistry, PendingSendTimeoutError } from "./pending-send-registry.js";
22
24
  import { readSubagentMessageSource } from "../source-ownership.js";
@@ -42,6 +44,8 @@ export interface SendOptions {
42
44
  attachments?: Attachment[];
43
45
  replyTo?: string;
44
46
  expectsReply?: boolean;
47
+ /** How a busy recipient receives the message; absent means `steer`. */
48
+ delivery?: IntercomDeliveryMode;
45
49
  replyError?: string;
46
50
  messageId?: string;
47
51
  /** Internal caller-issued target identity; transport still uses the `to` argument. */
@@ -61,6 +65,11 @@ export interface IntercomClientOptions {
61
65
  readonly acknowledgesReceipt?: true;
62
66
  /** Registration answer bound for `connect()`; defaults to `BROKER_CONNECT_TIMEOUT_MS`. */
63
67
  readonly connectTimeoutMs?: number;
68
+ /**
69
+ * #3906: this connection carries the pending-stage routes of many runs, so it asks a broker that supports it to
70
+ * refuse one run's route on its own and keep the connection open for the others.
71
+ */
72
+ readonly multiplexesPendingStageRoutes?: true;
64
73
  }
65
74
 
66
75
  export interface SendResult {
@@ -168,6 +177,28 @@ function isWorkflowStageRosterEntries(value: unknown): value is WorkflowStageRos
168
177
  );
169
178
  }
170
179
 
180
+ /** The broker refused this pending-stage route registration itself, not the connection it was sent on (#3904). */
181
+ export class IntercomPendingStageRouteRefusedError extends Error {
182
+ /**
183
+ * #3906: the broker answered this refusal on its own and kept the connection, with the route of every other run on
184
+ * it. When false, the broker (an older one) ended the connection with the refusal.
185
+ */
186
+ readonly connectionKept: boolean;
187
+ readonly code: PendingStageRouteRefusalCode | undefined;
188
+ constructor(reason: string, refusal: { readonly connectionKept: boolean; readonly code?: PendingStageRouteRefusalCode } = { connectionKept: false }) {
189
+ super(reason);
190
+ this.name = "IntercomPendingStageRouteRefusedError";
191
+ this.connectionKept = refusal.connectionKept;
192
+ this.code = refusal.code;
193
+ }
194
+ }
195
+
196
+ const PENDING_STAGE_ROUTE_REFUSAL_CODES: ReadonlySet<string> = new Set<PendingStageRouteRefusalCode>([
197
+ "not_authorized",
198
+ "invalid_stage_roster",
199
+ "invalid_possible_stage_roster",
200
+ ]);
201
+
171
202
  export class IntercomClient extends EventEmitter {
172
203
  private readonly returnAddress: string;
173
204
  private readonly preferredSessionId: string | undefined;
@@ -177,6 +208,9 @@ export class IntercomClient extends EventEmitter {
177
208
  private brokerAcknowledgesReceipt = false;
178
209
  /** The broker of the current registration answers a pending-stage route registration that carries a `requestId`. */
179
210
  private brokerAcknowledgesPendingStageRoutes = false;
211
+ private readonly multiplexesPendingStageRoutes: boolean;
212
+ /** #3906: the broker of the current registration refuses a pending-stage route without ending the connection. */
213
+ private brokerRefusesPendingStageRoutesInPlace = false;
180
214
  private brokerAnswersStatus = false;
181
215
  private socket: net.Socket | null = null;
182
216
  private _sessionId: string | null = null;
@@ -251,6 +285,7 @@ export class IntercomClient extends EventEmitter {
251
285
  this.preferredSessionId = options.preferredSessionId;
252
286
  this.acknowledgesReceipt = options.acknowledgesReceipt === true;
253
287
  this.connectTimeoutMs = options.connectTimeoutMs ?? BROKER_CONNECT_TIMEOUT_MS;
288
+ this.multiplexesPendingStageRoutes = options.multiplexesPendingStageRoutes === true;
254
289
  }
255
290
 
256
291
  private failPending(error: Error): void {
@@ -376,6 +411,7 @@ export class IntercomClient extends EventEmitter {
376
411
  this._supervisorSessionId = null;
377
412
  this.brokerAcknowledgesReceipt = false;
378
413
  this.brokerAcknowledgesPendingStageRoutes = false;
414
+ this.brokerRefusesPendingStageRoutesInPlace = false;
379
415
  this.brokerAnswersStatus = false;
380
416
  this._messageSource = undefined;
381
417
  this.disconnectError = null;
@@ -486,6 +522,7 @@ export class IntercomClient extends EventEmitter {
486
522
  || (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
487
523
  || (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)
488
524
  || (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)
525
+ || (brokerMessage.refusesPendingStageRoutesInPlace !== undefined && brokerMessage.refusesPendingStageRoutesInPlace !== true)
489
526
  || (brokerMessage.brokerStatus !== undefined && brokerMessage.brokerStatus !== true)) {
490
527
  throw new Error("Invalid registered message");
491
528
  }
@@ -500,6 +537,7 @@ export class IntercomClient extends EventEmitter {
500
537
  // #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
501
538
  this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
502
539
  this.brokerAcknowledgesPendingStageRoutes = brokerMessage.acknowledgesPendingStageRoutes === true;
540
+ this.brokerRefusesPendingStageRoutesInPlace = brokerMessage.refusesPendingStageRoutesInPlace === true;
503
541
  this.brokerAnswersStatus = brokerMessage.brokerStatus === true;
504
542
  this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
505
543
  break;
@@ -528,6 +566,12 @@ export class IntercomClient extends EventEmitter {
528
566
  // disconnect cause, settle outstanding work with it, and destroy the socket
529
567
  // so no new work is accepted while we wait for the peer FIN. `onClose`
530
568
  // still owns session/socket teardown and the `disconnected` emission.
569
+ const refusedRouteId = typeof brokerMessage.requestId === "string" ? brokerMessage.requestId : "";
570
+ const refusedRoute = this.pendingStageRouteRegistrations.get(refusedRouteId);
571
+ if (refusedRoute !== undefined) {
572
+ this.pendingStageRouteRegistrations.delete(refusedRouteId);
573
+ refusedRoute.reject(new IntercomPendingStageRouteRefusedError(brokerMessage.reason));
574
+ }
531
575
  this.disconnectError ??= refusal;
532
576
  this.failPending(this.disconnectError);
533
577
  this.socket?.destroy();
@@ -679,6 +723,27 @@ export class IntercomClient extends EventEmitter {
679
723
  this.emit("pending_stage_route_revoked", { runId });
680
724
  break;
681
725
  }
726
+ case "pending_stage_route_refused": {
727
+ const { requestId, runId, code, reason } = brokerMessage;
728
+ if (
729
+ typeof requestId !== "string" ||
730
+ typeof runId !== "string" ||
731
+ typeof reason !== "string" ||
732
+ typeof code !== "string"
733
+ ) {
734
+ throw new Error("Invalid pending-stage route refusal");
735
+ }
736
+ const pending = this.pendingStageRouteRegistrations.get(requestId);
737
+ if (!pending) return;
738
+ this.pendingStageRouteRegistrations.delete(requestId);
739
+ pending.reject(
740
+ new IntercomPendingStageRouteRefusedError(reason, {
741
+ connectionKept: true,
742
+ ...(PENDING_STAGE_ROUTE_REFUSAL_CODES.has(code) ? { code: code as PendingStageRouteRefusalCode } : {}),
743
+ }),
744
+ );
745
+ break;
746
+ }
682
747
  case "pending_stage_route_registered": {
683
748
  const { requestId } = brokerMessage;
684
749
  if (typeof requestId !== "string") throw new Error("Invalid pending-stage route registration acknowledgement");
@@ -1197,6 +1262,9 @@ export class IntercomClient extends EventEmitter {
1197
1262
  possibleStages,
1198
1263
  ...(parent === undefined ? {} : { parent }),
1199
1264
  ...(dormant === true ? { dormant: true as const } : {}),
1265
+ ...(this.multiplexesPendingStageRoutes && this.brokerRefusesPendingStageRoutesInPlace
1266
+ ? { refusalKeepsConnection: true as const }
1267
+ : {}),
1200
1268
  });
1201
1269
  } catch (error) {
1202
1270
  clearTimeout(timeout);
@@ -1345,6 +1413,7 @@ export class IntercomClient extends EventEmitter {
1345
1413
  timestamp: Date.now(),
1346
1414
  replyTo: options.replyTo,
1347
1415
  expectsReply: options.expectsReply,
1416
+ ...(options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }),
1348
1417
  replyError: options.replyError,
1349
1418
  source: this._messageSource,
1350
1419
  content: { text: options.text, attachments: options.attachments },
@@ -1,10 +1,11 @@
1
- import type { Attachment, Message } from "../types.js";
1
+ import type { Attachment, IntercomDeliveryMode, Message } from "../types.js";
2
2
 
3
3
  export interface LogicalSendOptions {
4
4
  text: string;
5
5
  attachments?: Attachment[];
6
6
  replyTo?: string;
7
7
  expectsReply?: boolean;
8
+ delivery?: IntercomDeliveryMode;
8
9
  replyError?: string;
9
10
  }
10
11
 
@@ -28,6 +29,7 @@ export function buildSendSignature(to: string, options: LogicalSendOptions): str
28
29
  replyTo: options.replyTo ?? null,
29
30
  expectsReply: options.expectsReply ?? false,
30
31
  replyError: options.replyError ?? null,
32
+ ...(options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }),
31
33
  });
32
34
  }
33
35
 
@@ -37,6 +39,7 @@ export function buildMessageSendSignature(to: string, message: Message, senderId
37
39
  attachments: message.content.attachments,
38
40
  replyTo: message.replyTo,
39
41
  expectsReply: message.expectsReply,
42
+ delivery: message.delivery,
40
43
  replyError: message.replyError,
41
44
  });
42
45
  if (senderId === undefined && message.source === undefined) return logicalSignature;