@bastani/atomic 0.9.43-alpha.1 → 0.9.44-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 (117) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +18 -0
  3. package/dist/builtin/intercom/broker/broker.ts +31 -7
  4. package/dist/builtin/intercom/broker/client.ts +65 -0
  5. package/dist/builtin/intercom/index.bundle.mjs +227 -95
  6. package/dist/builtin/intercom/live-route-refusal.ts +20 -0
  7. package/dist/builtin/intercom/package.json +1 -1
  8. package/dist/builtin/intercom/skills/intercom/SKILL.md +10 -6
  9. package/dist/builtin/intercom/types.ts +20 -1
  10. package/dist/builtin/mcp/CHANGELOG.md +7 -0
  11. package/dist/builtin/mcp/package.json +1 -1
  12. package/dist/builtin/subagents/CHANGELOG.md +9 -0
  13. package/dist/builtin/subagents/README.md +8 -8
  14. package/dist/builtin/subagents/agents/worker.md +1 -1
  15. package/dist/builtin/subagents/package.json +1 -1
  16. package/dist/builtin/subagents/skills/agent-browser/SKILL.md +1 -1
  17. package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +21 -2
  18. package/dist/builtin/subagents/skills/cua-driver/LINUX.md +2 -2
  19. package/dist/builtin/subagents/skills/cua-driver/MACOS.md +342 -10
  20. package/dist/builtin/subagents/skills/cua-driver/README.md +4 -3
  21. package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +3 -1
  22. package/dist/builtin/subagents/skills/cua-driver/SKILL.md +7 -4
  23. package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +6 -5
  24. package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +15 -0
  25. package/dist/builtin/subagents/skills/herdr/SKILL.md +22 -10
  26. package/dist/builtin/subagents/skills/subagent/SKILL.md +8 -5
  27. package/dist/builtin/subagents/src/extension/index.bundle.mjs +6 -6
  28. package/dist/builtin/web-access/package.json +1 -1
  29. package/dist/builtin/workflows/CHANGELOG.md +33 -0
  30. package/dist/builtin/workflows/README.md +1 -1
  31. package/dist/builtin/workflows/builtin/{chunk-frqxd4d1.js → chunk-k8d7ehjx.js} +22 -0
  32. package/dist/builtin/workflows/builtin/{chunk-3s95f7ry.js → chunk-qxpymnaf.js} +1 -1
  33. package/dist/builtin/workflows/builtin/{chunk-edhf30ef.js → chunk-zjecnjxx.js} +1 -1
  34. package/dist/builtin/workflows/builtin/goal.js +2 -2
  35. package/dist/builtin/workflows/builtin/index.js +3 -3
  36. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  37. package/dist/builtin/workflows/package.json +1 -1
  38. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1750 -598
  39. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +1786 -600
  40. package/dist/builtin/workflows/src/index.js +1802 -650
  41. package/dist/builtin/workflows/src/local-worker-entry.mjs +1786 -600
  42. package/dist/cli/args.js +1 -1
  43. package/dist/cli/args.js.map +1 -1
  44. package/dist/core/exec.d.ts.map +1 -1
  45. package/dist/core/exec.js +11 -2
  46. package/dist/core/exec.js.map +1 -1
  47. package/dist/core/package-manager-resolver.d.ts.map +1 -1
  48. package/dist/core/package-manager-resolver.js +25 -14
  49. package/dist/core/package-manager-resolver.js.map +1 -1
  50. package/dist/core/package-manager-types.d.ts +8 -0
  51. package/dist/core/package-manager-types.d.ts.map +1 -1
  52. package/dist/core/package-manager-types.js.map +1 -1
  53. package/dist/core/package-manager.d.ts +1 -1
  54. package/dist/core/package-manager.d.ts.map +1 -1
  55. package/dist/core/package-manager.js.map +1 -1
  56. package/dist/core/resource-loader-extension-warnings.d.ts +4 -1
  57. package/dist/core/resource-loader-extension-warnings.d.ts.map +1 -1
  58. package/dist/core/resource-loader-extension-warnings.js +15 -0
  59. package/dist/core/resource-loader-extension-warnings.js.map +1 -1
  60. package/dist/core/resource-loader-package-resources.d.ts.map +1 -1
  61. package/dist/core/resource-loader-package-resources.js +1 -0
  62. package/dist/core/resource-loader-package-resources.js.map +1 -1
  63. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  64. package/dist/core/resource-loader-reload.js +5 -3
  65. package/dist/core/resource-loader-reload.js.map +1 -1
  66. package/dist/extensions/mcp/client/oauth/flow.d.ts.map +1 -1
  67. package/dist/extensions/mcp/client/oauth/flow.js +4 -1
  68. package/dist/extensions/mcp/client/oauth/flow.js.map +1 -1
  69. package/dist/extensions/mcp/client/oauth/types.d.ts +1 -0
  70. package/dist/extensions/mcp/client/oauth/types.d.ts.map +1 -1
  71. package/dist/extensions/mcp/client/oauth/types.js +8 -4
  72. package/dist/extensions/mcp/client/oauth/types.js.map +1 -1
  73. package/dist/extensions/mcp/client/transports/streamable-http.d.ts.map +1 -1
  74. package/dist/extensions/mcp/client/transports/streamable-http.js +2 -1
  75. package/dist/extensions/mcp/client/transports/streamable-http.js.map +1 -1
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/main.d.ts.map +1 -1
  80. package/dist/main.js +20 -10
  81. package/dist/main.js.map +1 -1
  82. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/model-selector.js +4 -2
  84. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  85. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/scoped-models-selector.js +4 -2
  87. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  88. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  89. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/tool-execution.js +11 -1
  91. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  92. package/dist/modes/interactive/model-search.d.ts +10 -0
  93. package/dist/modes/interactive/model-search.d.ts.map +1 -1
  94. package/dist/modes/interactive/model-search.js +21 -0
  95. package/dist/modes/interactive/model-search.js.map +1 -1
  96. package/dist/package-manager-cli.d.ts.map +1 -1
  97. package/dist/package-manager-cli.js +5 -0
  98. package/dist/package-manager-cli.js.map +1 -1
  99. package/dist/utils/tools-manager.d.ts.map +1 -1
  100. package/dist/utils/tools-manager.js +30 -35
  101. package/dist/utils/tools-manager.js.map +1 -1
  102. package/docs/computer-use.md +5 -5
  103. package/docs/custom-provider/streaming.md +9 -14
  104. package/docs/environment-variables.md +1 -0
  105. package/docs/intercom.md +5 -1
  106. package/docs/packages.md +2 -0
  107. package/docs/settings.md +2 -2
  108. package/docs/skills.md +2 -2
  109. package/docs/subagents.md +38 -9
  110. package/docs/workflows/api-reference.md +1 -1
  111. package/docs/workflows/authoring.md +1 -1
  112. package/docs/workflows/operations.md +8 -6
  113. package/docs/workflows/reliable-design.md +23 -5
  114. package/examples/extensions/custom-provider-anthropic/index.ts +31 -31
  115. package/examples/extensions/todo.ts +7 -0
  116. package/npm-shrinkwrap.json +32 -32
  117. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.44-alpha.1] - 2026-10-24
6
+
7
+ 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.
8
+
9
+ - **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))
10
+ - **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))
11
+ - **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))
12
+ - **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))
13
+ - **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))
14
+ - **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))
15
+ - **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))
16
+
17
+ **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))
18
+
19
+ ### Added
20
+
21
+ - `/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))
22
+
23
+ ### Fixed
24
+
25
+ - 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))
26
+ - 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))
27
+ - `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))
28
+ - 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))
29
+ - 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))
30
+
31
+ ## [0.9.43] - 2026-10-24
32
+
33
+ Local workflow worker pools now survive slow, busy, and crashed workers, and large workflow journals open without running out of memory.
34
+
35
+ - **Pools that recover instead of failing:** a worker that dies mid-run is replaced and its child replays from the last checkpoint instead of crashing the root with `EPIPE`, a resumed root releases children leased to workers that died with the host, unconfirmed worker exits are retried rather than failing every waiting child, and a replayed child no longer leaves an `Intercom event relay failed` error in the session ([#3891](https://github.com/bastani-inc/atomic/pull/3891), [#3893](https://github.com/bastani-inc/atomic/pull/3893), [#3895](https://github.com/bastani-inc/atomic/pull/3895))
36
+ - **Leases that follow worker liveness:** a busy root no longer fences healthy workers with `Stale work lease`, and the new `workflows.localWorkers.leaseMs` setting controls how long an unresponsive worker keeps its child (30000 ms by default) ([#3894](https://github.com/bastani-inc/atomic/pull/3894))
37
+ - **Retry decisions you can make in code:** `WorkflowInfrastructureError` and `isRetryableWorkflowError` tell retryable pool and runtime faults apart from failures of a child's own logic ([#3895](https://github.com/bastani-inc/atomic/pull/3895))
38
+ - **Faster startup on long journals:** workers become ready without reading the whole journal, the readiness timeout is configurable with `workflows.localWorkers.startupTimeoutMs`, and sessions, `/workflow status`, and the `/workflow resume` picker read each run's latest state instead of every event, so large journals no longer exhaust the heap ([#3891](https://github.com/bastani-inc/atomic/pull/3891), [#3896](https://github.com/bastani-inc/atomic/pull/3896))
39
+ - **Easier worker diagnosis:** each worker's standard error is kept under `worker-logs/` next to the journal, and startup and retry-limit errors name the log file and show the end of its output ([#3891](https://github.com/bastani-inc/atomic/pull/3891))
40
+
41
+ ### Added
42
+
43
+ - Added `WorkflowInfrastructureError` and `isRetryableWorkflowError` to `@bastani/atomic/workflows`, so workflows can tell local worker pool and runtime faults (which are safe to retry) from failures of a child workflow's own logic ([#3889](https://github.com/bastani-inc/atomic/issues/3889))
44
+ - Added the `workflows.localWorkers.leaseMs` setting: how long, in milliseconds, a local workflow worker that is still running but has stopped responding keeps its child before the child is replayed on another worker (30000 by default, at least 2000) ([#3888](https://github.com/bastani-inc/atomic/issues/3888))
45
+
46
+ ### Fixed
47
+
48
+ - Local workflow worker pools no longer fail with `Local worker startup timed out` when workers are slow to become ready, for example on a workflow journal with a long history. The readiness timeout is now configurable with the `workflows.localWorkers.startupTimeoutMs` setting (30000 ms by default) ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
49
+
5
50
  ## [0.9.43-alpha.1] - 2026-10-24
6
51
 
7
52
  Local workflow worker pools now survive slow, busy, and crashed workers, and large workflow journals open without running out of memory.
@@ -4,6 +4,24 @@ 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.1] - 2026-10-24
8
+
9
+ ### Changed
10
+
11
+ - 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.
12
+
13
+ ### Fixed
14
+
15
+ - 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))
16
+ - 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))
17
+ - 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))
18
+
19
+ ## [0.9.43] - 2026-10-24
20
+
21
+ ### Fixed
22
+
23
+ - Fixed `Intercom event relay failed (atomic:workflow-pending-stage-route): Intercom runtime no longer active` appearing in the session after a local workflow worker died and its child was replayed. Route updates that arrive while a run's previous route connection is still closing or opening, as they do when the run's route is replaced, no longer race each other: the newest route is registered over one connection and an update the replacement made obsolete ends quietly. A route connection abandoned because the session itself shut down is still reported ([#3887](https://github.com/bastani-inc/atomic/issues/3887))
24
+
7
25
  ## [0.9.43-alpha.1] - 2026-10-24
8
26
 
9
27
  ### 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 `[]`
@@ -17,6 +17,7 @@ import type {
17
17
  WorkflowFutureStageRosterEntry,
18
18
  WorkflowPossibleStageAnnouncement,
19
19
  WorkflowRunParentAnnouncement,
20
+ PendingStageRouteRefusalCode,
20
21
  } from "../types.js";
21
22
  import { buildSendSignature, PendingSendRegistry, PendingSendTimeoutError } from "./pending-send-registry.js";
22
23
  import { readSubagentMessageSource } from "../source-ownership.js";
@@ -61,6 +62,11 @@ export interface IntercomClientOptions {
61
62
  readonly acknowledgesReceipt?: true;
62
63
  /** Registration answer bound for `connect()`; defaults to `BROKER_CONNECT_TIMEOUT_MS`. */
63
64
  readonly connectTimeoutMs?: number;
65
+ /**
66
+ * #3906: this connection carries the pending-stage routes of many runs, so it asks a broker that supports it to
67
+ * refuse one run's route on its own and keep the connection open for the others.
68
+ */
69
+ readonly multiplexesPendingStageRoutes?: true;
64
70
  }
65
71
 
66
72
  export interface SendResult {
@@ -168,6 +174,28 @@ function isWorkflowStageRosterEntries(value: unknown): value is WorkflowStageRos
168
174
  );
169
175
  }
170
176
 
177
+ /** The broker refused this pending-stage route registration itself, not the connection it was sent on (#3904). */
178
+ export class IntercomPendingStageRouteRefusedError extends Error {
179
+ /**
180
+ * #3906: the broker answered this refusal on its own and kept the connection, with the route of every other run on
181
+ * it. When false, the broker (an older one) ended the connection with the refusal.
182
+ */
183
+ readonly connectionKept: boolean;
184
+ readonly code: PendingStageRouteRefusalCode | undefined;
185
+ constructor(reason: string, refusal: { readonly connectionKept: boolean; readonly code?: PendingStageRouteRefusalCode } = { connectionKept: false }) {
186
+ super(reason);
187
+ this.name = "IntercomPendingStageRouteRefusedError";
188
+ this.connectionKept = refusal.connectionKept;
189
+ this.code = refusal.code;
190
+ }
191
+ }
192
+
193
+ const PENDING_STAGE_ROUTE_REFUSAL_CODES: ReadonlySet<string> = new Set<PendingStageRouteRefusalCode>([
194
+ "not_authorized",
195
+ "invalid_stage_roster",
196
+ "invalid_possible_stage_roster",
197
+ ]);
198
+
171
199
  export class IntercomClient extends EventEmitter {
172
200
  private readonly returnAddress: string;
173
201
  private readonly preferredSessionId: string | undefined;
@@ -177,6 +205,9 @@ export class IntercomClient extends EventEmitter {
177
205
  private brokerAcknowledgesReceipt = false;
178
206
  /** The broker of the current registration answers a pending-stage route registration that carries a `requestId`. */
179
207
  private brokerAcknowledgesPendingStageRoutes = false;
208
+ private readonly multiplexesPendingStageRoutes: boolean;
209
+ /** #3906: the broker of the current registration refuses a pending-stage route without ending the connection. */
210
+ private brokerRefusesPendingStageRoutesInPlace = false;
180
211
  private brokerAnswersStatus = false;
181
212
  private socket: net.Socket | null = null;
182
213
  private _sessionId: string | null = null;
@@ -251,6 +282,7 @@ export class IntercomClient extends EventEmitter {
251
282
  this.preferredSessionId = options.preferredSessionId;
252
283
  this.acknowledgesReceipt = options.acknowledgesReceipt === true;
253
284
  this.connectTimeoutMs = options.connectTimeoutMs ?? BROKER_CONNECT_TIMEOUT_MS;
285
+ this.multiplexesPendingStageRoutes = options.multiplexesPendingStageRoutes === true;
254
286
  }
255
287
 
256
288
  private failPending(error: Error): void {
@@ -376,6 +408,7 @@ export class IntercomClient extends EventEmitter {
376
408
  this._supervisorSessionId = null;
377
409
  this.brokerAcknowledgesReceipt = false;
378
410
  this.brokerAcknowledgesPendingStageRoutes = false;
411
+ this.brokerRefusesPendingStageRoutesInPlace = false;
379
412
  this.brokerAnswersStatus = false;
380
413
  this._messageSource = undefined;
381
414
  this.disconnectError = null;
@@ -486,6 +519,7 @@ export class IntercomClient extends EventEmitter {
486
519
  || (brokerMessage.supervisorSessionId !== undefined && typeof brokerMessage.supervisorSessionId !== "string")
487
520
  || (brokerMessage.acknowledgesReceipt !== undefined && brokerMessage.acknowledgesReceipt !== true)
488
521
  || (brokerMessage.acknowledgesPendingStageRoutes !== undefined && brokerMessage.acknowledgesPendingStageRoutes !== true)
522
+ || (brokerMessage.refusesPendingStageRoutesInPlace !== undefined && brokerMessage.refusesPendingStageRoutesInPlace !== true)
489
523
  || (brokerMessage.brokerStatus !== undefined && brokerMessage.brokerStatus !== true)) {
490
524
  throw new Error("Invalid registered message");
491
525
  }
@@ -500,6 +534,7 @@ export class IntercomClient extends EventEmitter {
500
534
  // #3761: a broker from before receipts never echoes the capability and drops a client that sends `received`.
501
535
  this.brokerAcknowledgesReceipt = this.acknowledgesReceipt && brokerMessage.acknowledgesReceipt === true;
502
536
  this.brokerAcknowledgesPendingStageRoutes = brokerMessage.acknowledgesPendingStageRoutes === true;
537
+ this.brokerRefusesPendingStageRoutesInPlace = brokerMessage.refusesPendingStageRoutesInPlace === true;
503
538
  this.brokerAnswersStatus = brokerMessage.brokerStatus === true;
504
539
  this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
505
540
  break;
@@ -528,6 +563,12 @@ export class IntercomClient extends EventEmitter {
528
563
  // disconnect cause, settle outstanding work with it, and destroy the socket
529
564
  // so no new work is accepted while we wait for the peer FIN. `onClose`
530
565
  // still owns session/socket teardown and the `disconnected` emission.
566
+ const refusedRouteId = typeof brokerMessage.requestId === "string" ? brokerMessage.requestId : "";
567
+ const refusedRoute = this.pendingStageRouteRegistrations.get(refusedRouteId);
568
+ if (refusedRoute !== undefined) {
569
+ this.pendingStageRouteRegistrations.delete(refusedRouteId);
570
+ refusedRoute.reject(new IntercomPendingStageRouteRefusedError(brokerMessage.reason));
571
+ }
531
572
  this.disconnectError ??= refusal;
532
573
  this.failPending(this.disconnectError);
533
574
  this.socket?.destroy();
@@ -679,6 +720,27 @@ export class IntercomClient extends EventEmitter {
679
720
  this.emit("pending_stage_route_revoked", { runId });
680
721
  break;
681
722
  }
723
+ case "pending_stage_route_refused": {
724
+ const { requestId, runId, code, reason } = brokerMessage;
725
+ if (
726
+ typeof requestId !== "string" ||
727
+ typeof runId !== "string" ||
728
+ typeof reason !== "string" ||
729
+ typeof code !== "string"
730
+ ) {
731
+ throw new Error("Invalid pending-stage route refusal");
732
+ }
733
+ const pending = this.pendingStageRouteRegistrations.get(requestId);
734
+ if (!pending) return;
735
+ this.pendingStageRouteRegistrations.delete(requestId);
736
+ pending.reject(
737
+ new IntercomPendingStageRouteRefusedError(reason, {
738
+ connectionKept: true,
739
+ ...(PENDING_STAGE_ROUTE_REFUSAL_CODES.has(code) ? { code: code as PendingStageRouteRefusalCode } : {}),
740
+ }),
741
+ );
742
+ break;
743
+ }
682
744
  case "pending_stage_route_registered": {
683
745
  const { requestId } = brokerMessage;
684
746
  if (typeof requestId !== "string") throw new Error("Invalid pending-stage route registration acknowledgement");
@@ -1197,6 +1259,9 @@ export class IntercomClient extends EventEmitter {
1197
1259
  possibleStages,
1198
1260
  ...(parent === undefined ? {} : { parent }),
1199
1261
  ...(dormant === true ? { dormant: true as const } : {}),
1262
+ ...(this.multiplexesPendingStageRoutes && this.brokerRefusesPendingStageRoutesInPlace
1263
+ ? { refusalKeepsConnection: true as const }
1264
+ : {}),
1200
1265
  });
1201
1266
  } catch (error) {
1202
1267
  clearTimeout(timeout);