@bastani/atomic 0.9.44-alpha.1 → 0.9.44-alpha.3

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 (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +17 -0
  3. package/dist/builtin/intercom/broker/client-message-validation.ts +6 -0
  4. package/dist/builtin/intercom/broker/client.ts +4 -0
  5. package/dist/builtin/intercom/broker/framing.ts +18 -1
  6. package/dist/builtin/intercom/broker/send-signature.ts +4 -1
  7. package/dist/builtin/intercom/index.bundle.mjs +101 -18
  8. package/dist/builtin/intercom/package.json +1 -1
  9. package/dist/builtin/intercom/skills/intercom/SKILL.md +1 -0
  10. package/dist/builtin/intercom/types.ts +8 -0
  11. package/dist/builtin/mcp/package.json +1 -1
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/web-access/index.bundle.mjs +2510 -1308
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/workflows/CHANGELOG.md +13 -0
  16. package/dist/builtin/workflows/package.json +1 -1
  17. package/dist/builtin/workflows/src/extension/index.bundle.mjs +363 -37
  18. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +363 -37
  19. package/dist/builtin/workflows/src/index.js +363 -37
  20. package/dist/builtin/workflows/src/local-worker-entry.mjs +363 -37
  21. package/dist/core/agent-session-custom-message-commit.d.ts.map +1 -1
  22. package/dist/core/agent-session-custom-message-commit.js +59 -0
  23. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  24. package/dist/core/agent-session-methods.d.ts +1 -0
  25. package/dist/core/agent-session-methods.d.ts.map +1 -1
  26. package/dist/core/agent-session-methods.js.map +1 -1
  27. package/dist/core/agent-session-prompt.d.ts +4 -0
  28. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  29. package/dist/core/agent-session-prompt.js +10 -2
  30. package/dist/core/agent-session-prompt.js.map +1 -1
  31. package/dist/core/agent-session.d.ts +2 -0
  32. package/dist/core/agent-session.d.ts.map +1 -1
  33. package/dist/core/agent-session.js +9 -0
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/builtin-packages.d.ts.map +1 -1
  36. package/dist/core/builtin-packages.js +13 -13
  37. package/dist/core/builtin-packages.js.map +1 -1
  38. package/docs/intercom/operations.md +1 -1
  39. package/docs/intercom.md +11 -1
  40. package/docs/subagents.md +9 -1
  41. package/examples/extensions/gondolin/package-lock.json +3 -3
  42. package/npm-shrinkwrap.json +67 -51
  43. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.44-alpha.3] - 2026-10-25
6
+
7
+ Chatty workflow swarms stay fast and their journals stay small, and Atomic picks up a round of dependency security fixes.
8
+
9
+ - **Pattern sends at fan-out scale:** stages that message each other through `workflow:<rootRunId>/...` patterns no longer saturate the workflow host or time out their sends, so every send in a 150-child fan-out is delivered ([#3931](https://github.com/bastani-inc/atomic/issues/3931), [#3932](https://github.com/bastani-inc/atomic/pull/3932))
10
+ - **Journals that grow with the work, not the message count:** journal events record only what changed in a run's pending messages instead of the whole list ([#3929](https://github.com/bastani-inc/atomic/issues/3929), [#3930](https://github.com/bastani-inc/atomic/pull/3930))
11
+ - **Security updates:** `undici` 8.12.3, `zod` 4.6.7, `fast-uri` 3.1.9, `body-parser` 2.4.0 and `tokio` 1.54.1 close the published advisories against them ([#3935](https://github.com/bastani-inc/atomic/pull/3935))
12
+
13
+ ### Fixed
14
+
15
+ - Updated `undici`, which Atomic uses for HTTP requests, to 8.12.3. A server can no longer crash Atomic by sending more than five `Content-Encoding` values ([GHSA-xjj6-f33q-929c](https://github.com/advisories/GHSA-xjj6-f33q-929c)), Server-Sent Events streams can no longer grow parser memory without bound ([GHSA-4qv8-2p9j-2685](https://github.com/advisories/GHSA-4qv8-2p9j-2685)), and Subresource Integrity checks no longer skip uppercase algorithm names ([GHSA-f72v-6w45-q472](https://github.com/advisories/GHSA-f72v-6w45-q472)).
16
+ - Updated the MCP SDK dependencies in the published lockfile to `zod` 4.6.7 ([GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5)), `fast-uri` 3.1.9 ([GHSA-w74x-v6f9-j3xg](https://github.com/advisories/GHSA-w74x-v6f9-j3xg), [GHSA-wjr9-j795-jxcr](https://github.com/advisories/GHSA-wjr9-j795-jxcr), [GHSA-xhq7-xmqj-769h](https://github.com/advisories/GHSA-xhq7-xmqj-769h), [GHSA-333x-w4r6-6crp](https://github.com/advisories/GHSA-333x-w4r6-6crp)), and `body-parser` 2.4.0 ([GHSA-27r3-jxmh-p828](https://github.com/advisories/GHSA-27r3-jxmh-p828)).
17
+ - Updated the native module's `tokio` crate to 1.54.1, replacing the yanked 1.54.0 release and its `Interval` race ([GHSA-5wjr-9356-4wc8](https://github.com/advisories/GHSA-5wjr-9356-4wc8)).
18
+ - Updated the gondolin example extension's lockfile to `undici` 6.29.1, resolving [GHSA-xjj6-f33q-929c](https://github.com/advisories/GHSA-xjj6-f33q-929c) and [GHSA-4qv8-2p9j-2685](https://github.com/advisories/GHSA-4qv8-2p9j-2685) in its `@earendil-works/gondolin` dependency.
19
+
20
+ ## [0.9.44-alpha.2] - 2026-10-24
21
+
22
+ Workflow swarms now message each other without cancelling each other's work, and stages, replies and session exits behave reliably under fan-out.
23
+
24
+ - **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))
25
+ - **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))
26
+ - **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))
27
+ - **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))
28
+ - **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))
29
+
30
+ ### Fixed
31
+
32
+ - 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))
33
+ - 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))
34
+ - 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))
35
+
5
36
  ## [0.9.44-alpha.1] - 2026-10-24
6
37
 
7
38
  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.
@@ -4,6 +4,23 @@ 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.3] - 2026-10-25
8
+
9
+ ### Fixed
10
+
11
+ - Intercom `send` and `ask` to a workflow-stage target (`workflow:<rootRunId>/...`, including patterns) no longer fetch the whole session list before sending. The broker resolves those targets itself, so the extra request only added load, and when many stages sent at once some sends failed with `List sessions timeout` ([#3931](https://github.com/bastani-inc/atomic/issues/3931))
12
+ - Receiving a large Intercom message (for example a big workflow roster) no longer copies its data again for every chunk that arrives, which made large frames cost quadratic time ([#3931](https://github.com/bastani-inc/atomic/issues/3931))
13
+
14
+ ## [0.9.44-alpha.2] - 2026-10-24
15
+
16
+ ### Changed
17
+
18
+ - 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))
19
+
20
+ ### Fixed
21
+
22
+ - 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))
23
+
7
24
  ## [0.9.44-alpha.1] - 2026-10-24
8
25
 
9
26
  ### Changed
@@ -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;
@@ -18,6 +18,7 @@ import type {
18
18
  WorkflowPossibleStageAnnouncement,
19
19
  WorkflowRunParentAnnouncement,
20
20
  PendingStageRouteRefusalCode,
21
+ IntercomDeliveryMode,
21
22
  } from "../types.js";
22
23
  import { buildSendSignature, PendingSendRegistry, PendingSendTimeoutError } from "./pending-send-registry.js";
23
24
  import { readSubagentMessageSource } from "../source-ownership.js";
@@ -43,6 +44,8 @@ export interface SendOptions {
43
44
  attachments?: Attachment[];
44
45
  replyTo?: string;
45
46
  expectsReply?: boolean;
47
+ /** How a busy recipient receives the message; absent means `steer`. */
48
+ delivery?: IntercomDeliveryMode;
46
49
  replyError?: string;
47
50
  messageId?: string;
48
51
  /** Internal caller-issued target identity; transport still uses the `to` argument. */
@@ -1410,6 +1413,7 @@ export class IntercomClient extends EventEmitter {
1410
1413
  timestamp: Date.now(),
1411
1414
  replyTo: options.replyTo,
1412
1415
  expectsReply: options.expectsReply,
1416
+ ...(options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }),
1413
1417
  replyError: options.replyError,
1414
1418
  source: this._messageSource,
1415
1419
  content: { text: options.text, attachments: options.attachments },
@@ -27,14 +27,31 @@ export function createMessageReader(
27
27
  onError: (error: Error) => void,
28
28
  ) {
29
29
  let buffer = Buffer.alloc(0);
30
+ // Chunks of a frame still arriving: joined once the frame is complete, not on every
31
+ // chunk, so a large frame costs linear rather than quadratic copying.
32
+ let pending: Buffer[] = [];
33
+ let pendingLength = 0;
34
+ let awaitedLength = 0;
30
35
 
31
36
  return (data: Buffer) => {
32
- buffer = Buffer.concat([buffer, data]);
37
+ if (pendingLength + data.length < awaitedLength) {
38
+ pending.push(data);
39
+ pendingLength += data.length;
40
+ return;
41
+ }
42
+ buffer = pending.length === 0 ? Buffer.concat([buffer, data]) : Buffer.concat([...pending, data]);
43
+ pending = [];
44
+ pendingLength = 0;
45
+ awaitedLength = 0;
33
46
 
34
47
  while (buffer.length >= 4) {
35
48
  const length = buffer.readUInt32BE(0);
36
49
 
37
50
  if (buffer.length < 4 + length) {
51
+ pending = [buffer];
52
+ pendingLength = buffer.length;
53
+ awaitedLength = 4 + length;
54
+ buffer = Buffer.alloc(0);
38
55
  break;
39
56
  }
40
57
 
@@ -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;
@@ -364,6 +364,8 @@ function isMessage(value) {
364
364
  return false;
365
365
  if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean")
366
366
  return false;
367
+ if (message.delivery !== undefined && message.delivery !== "steer" && message.delivery !== "followUp" && message.delivery !== "interrupt")
368
+ return false;
367
369
  if (message.replyError !== undefined && typeof message.replyError !== "string")
368
370
  return false;
369
371
  if (message.source !== undefined && !isMessageSource(message.source))
@@ -563,11 +565,26 @@ function writeMessage(socket, msg, callback) {
563
565
  }
564
566
  function createMessageReader(onMessage, onError) {
565
567
  let buffer = Buffer.alloc(0);
568
+ let pending = [];
569
+ let pendingLength = 0;
570
+ let awaitedLength = 0;
566
571
  return (data) => {
567
- buffer = Buffer.concat([buffer, data]);
572
+ if (pendingLength + data.length < awaitedLength) {
573
+ pending.push(data);
574
+ pendingLength += data.length;
575
+ return;
576
+ }
577
+ buffer = pending.length === 0 ? Buffer.concat([buffer, data]) : Buffer.concat([...pending, data]);
578
+ pending = [];
579
+ pendingLength = 0;
580
+ awaitedLength = 0;
568
581
  while (buffer.length >= 4) {
569
582
  const length = buffer.readUInt32BE(0);
570
583
  if (buffer.length < 4 + length) {
584
+ pending = [buffer];
585
+ pendingLength = buffer.length;
586
+ awaitedLength = 4 + length;
587
+ buffer = Buffer.alloc(0);
571
588
  break;
572
589
  }
573
590
  const payload = buffer.subarray(4, 4 + length);
@@ -689,7 +706,8 @@ function buildSendSignature(to, options) {
689
706
  attachments: canonicalizeAttachmentsForSendSignature(options.attachments) ?? [],
690
707
  replyTo: options.replyTo ?? null,
691
708
  expectsReply: options.expectsReply ?? false,
692
- replyError: options.replyError ?? null
709
+ replyError: options.replyError ?? null,
710
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }
693
711
  });
694
712
  }
695
713
 
@@ -1990,6 +2008,7 @@ var init_client = __esm(() => {
1990
2008
  timestamp: Date.now(),
1991
2009
  replyTo: options.replyTo,
1992
2010
  expectsReply: options.expectsReply,
2011
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery },
1993
2012
  replyError: options.replyError,
1994
2013
  source: this._messageSource,
1995
2014
  content: { text: options.text, attachments: options.attachments }
@@ -2790,6 +2809,7 @@ class ReplyTracker {
2790
2809
  pendingAsks = new Map;
2791
2810
  pendingTurnContexts = [];
2792
2811
  currentTurnContext = null;
2812
+ turnContextAdmitted = false;
2793
2813
  constructor(askTimeoutMs = ASK_REPLY_TIMEOUT_MS) {
2794
2814
  this.askTimeoutMs = askTimeoutMs;
2795
2815
  }
@@ -2815,15 +2835,27 @@ class ReplyTracker {
2815
2835
  }
2816
2836
  beginTurn(now = Date.now()) {
2817
2837
  this.pruneExpired(now);
2818
- this.currentTurnContext = this.pendingTurnContexts.shift() ?? null;
2838
+ this.turnContextAdmitted = false;
2839
+ }
2840
+ admitTurnMessage(messageId) {
2841
+ if (this.currentTurnContext?.message.id === messageId) {
2842
+ this.turnContextAdmitted = true;
2843
+ return;
2844
+ }
2845
+ const index = this.pendingTurnContexts.findIndex((context) => context.message.id === messageId);
2846
+ if (index < 0)
2847
+ return;
2848
+ const [admitted] = this.pendingTurnContexts.splice(index, 1);
2849
+ if (this.turnContextAdmitted)
2850
+ return;
2851
+ this.currentTurnContext = admitted;
2852
+ this.turnContextAdmitted = true;
2819
2853
  }
2820
2854
  endTurn() {
2821
2855
  this.currentTurnContext = null;
2822
2856
  }
2823
2857
  restoreTurnContext() {
2824
- if (this.currentTurnContext)
2825
- this.pendingTurnContexts.unshift(this.currentTurnContext);
2826
- this.currentTurnContext = null;
2858
+ this.turnContextAdmitted = false;
2827
2859
  }
2828
2860
  reset() {
2829
2861
  this.pendingAsks.clear();
@@ -2872,10 +2904,11 @@ class ReplyTracker {
2872
2904
  }
2873
2905
  throw new Error(`No pending ask from "${options.to}"`);
2874
2906
  }
2875
- if (pending.length === 1) {
2876
- return pending[0];
2907
+ const visible = pending.filter((context) => !this.pendingTurnContexts.some((queued) => queued.message.id === context.message.id));
2908
+ if (visible.length === 1) {
2909
+ return visible[0];
2877
2910
  }
2878
- if (pending.length === 0) {
2911
+ if (visible.length === 0) {
2879
2912
  throw new Error("No active intercom context to reply to");
2880
2913
  }
2881
2914
  throw new Error("Multiple pending asks — specify `to`");
@@ -4055,6 +4088,7 @@ function registerIntercomTool(pi, deps) {
4055
4088
  const retryIdentities = new RetryIdentityReservations({ maxEntries: deps.retryIdentityMaxEntries });
4056
4089
  const { childOrchestratorMetadata, ensureConnected, syncPresenceIdentity, beginReplyWait } = deps;
4057
4090
  const resolveTarget = deps.resolveSessionTarget ?? resolveSessionTargetId;
4091
+ const resolveRecipient = async (client, target) => parseWorkflowStageTarget(target) === undefined ? await resolveTarget(client, target) ?? target : target;
4058
4092
  const getMetadata = typeof childOrchestratorMetadata === "function" ? childOrchestratorMetadata : () => childOrchestratorMetadata ?? null;
4059
4093
  const activeReplyTracker = () => typeof deps.replyTracker === "function" ? deps.replyTracker() : deps.replyTracker;
4060
4094
  pi.registerTool({
@@ -4078,6 +4112,10 @@ A valid target outside the known set queues speculatively with a \`notInKnownSet
4078
4112
  undeliverable at terminal only if never delivered. Use \`ask\` only for a live, reply-capable target.
4079
4113
  Stage paths win over same-named owned subgroups for send/ask; \`list\` with \`group\` continues to
4080
4114
  select the group.
4115
+ Choose \`delivery\` per send/ask by urgency. The default "steer" arrives after the recipient's current
4116
+ tool calls finish and never cancels its work. Use "interrupt" when it must stop now (an urgent correction
4117
+ or a stop order); it cancels the current model call or tool call. Use "followUp" when the message can wait
4118
+ until the recipient finishes (low priority, "after you're done"). An idle recipient starts a turn.
4081
4119
  For send/ask/reply, Intercom retries recoverable disconnects internally up to three times with the same operation identity. Each new tool call is a new operation. If recovery ends with an unknown delivery outcome, do not repeat it automatically.
4082
4120
 
4083
4121
  Usage:
@@ -4089,6 +4127,7 @@ Usage:
4089
4127
  intercom({ action: "leave" }) → Return to your resolved home group
4090
4128
  intercom({ action: "send", to: "session-name", message: "..." }) → Send message (shared group only)
4091
4129
  intercom({ action: "ask", to: "session-name", message: "..." }) → Ask and wait for reply
4130
+ intercom({ action: "send", to: "session-name", message: "...", delivery: "interrupt" }) → Cancel the recipient's current work
4092
4131
  intercom({ action: "reply", message: "..." }) → Reply to the active or exact pending ask
4093
4132
  intercom({ action: "pending" }) → List unresolved inbound asks
4094
4133
  intercom({ action: "status" }) → Show connection status and all your groups
@@ -4113,6 +4152,9 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4113
4152
  content: Type2.String(),
4114
4153
  language: Type2.Optional(Type2.String())
4115
4154
  }))),
4155
+ delivery: Type2.Optional(Type2.Union([Type2.Literal("steer"), Type2.Literal("followUp"), Type2.Literal("interrupt")], {
4156
+ description: "For 'send'/'ask'; choose per message by urgency. 'steer' (default): arrives after the recipient's current tool calls, never cancelling work. 'interrupt': the recipient must stop now (urgent correction, stop order); cancels its current model call or tool call. 'followUp': can wait until the recipient finishes (low priority, after you're done)."
4157
+ })),
4116
4158
  replyTo: Type2.Optional(Type2.String({
4117
4159
  description: "Exact pending-ask message ID or unique 8-hex UUID prefix; disambiguates concurrent asks, including asks from one sender"
4118
4160
  })),
@@ -4130,6 +4172,7 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4130
4172
  }
4131
4173
  const toolSessionId = ctx.sessionManager.getSessionId();
4132
4174
  const { action, to, message, replyTo, group } = params;
4175
+ const deliveryOption = params.delivery === undefined || params.delivery === "steer" ? {} : { delivery: params.delivery };
4133
4176
  const attachments = params.attachments?.map((attachment) => ({ ...attachment }));
4134
4177
  const executeAttempt = async (retryToken, _signal) => {
4135
4178
  if (retryToken !== undefined) {
@@ -4413,7 +4456,7 @@ ${message}${attachmentText}`);
4413
4456
  details: { error: true, delivered: false, reason: goneChild }
4414
4457
  };
4415
4458
  }
4416
- const sendTo = await resolveTarget(connectedClient, to) ?? to;
4459
+ const sendTo = await resolveRecipient(connectedClient, to);
4417
4460
  if (_signal?.aborted) {
4418
4461
  const retained = retainInconclusiveRetry(retryIdentities, retryIdentity);
4419
4462
  retryIdentity = undefined;
@@ -4440,7 +4483,8 @@ ${message}${attachmentText}`);
4440
4483
  logicalTarget: to,
4441
4484
  text: message,
4442
4485
  attachments,
4443
- replyTo
4486
+ replyTo,
4487
+ ...deliveryOption
4444
4488
  });
4445
4489
  if (result.queued === true) {
4446
4490
  retryIdentities.release(retryIdentity);
@@ -4586,8 +4630,7 @@ ${message}${attachmentText}`);
4586
4630
  details: { error: true, delivered: false, reason: goneChild }
4587
4631
  };
4588
4632
  }
4589
- const listedTarget = await resolveTarget(connectedClient, to);
4590
- const sendTo = listedTarget ?? to;
4633
+ const sendTo = await resolveRecipient(connectedClient, to);
4591
4634
  if (_signal?.aborted) {
4592
4635
  const retained = retryIdentity === undefined ? undefined : retainInconclusiveRetry(retryIdentities, retryIdentity);
4593
4636
  retryIdentity = undefined;
@@ -4672,6 +4715,7 @@ ${message}${attachmentText}`);
4672
4715
  attachments,
4673
4716
  replyTo,
4674
4717
  expectsReply: true,
4718
+ ...deliveryOption,
4675
4719
  onReplyTarget: wait.bindSender
4676
4720
  });
4677
4721
  if (sendResult.queued === true) {
@@ -5561,6 +5605,13 @@ function registerIntercomLifecycle(pi, deps) {
5561
5605
  deps.syncPresenceIdentity(ctx.sessionManager.getSessionId());
5562
5606
  activeReplyTracker().beginTurn();
5563
5607
  });
5608
+ pi.on("message_start", (event, ctx) => {
5609
+ if (!deps.getLiveContext(ctx))
5610
+ return;
5611
+ const id = admittedIntercomMessageId(event.message, ctx);
5612
+ if (id !== undefined)
5613
+ activeReplyTracker().admitTurnMessage(id);
5614
+ });
5564
5615
  pi.on("model_select", (event, ctx) => {
5565
5616
  if (!deps.getLiveContext(ctx))
5566
5617
  return;
@@ -5575,6 +5626,25 @@ function registerIntercomLifecycle(pi, deps) {
5575
5626
  }
5576
5627
  });
5577
5628
  }
5629
+ function intercomMessageId(customType, details) {
5630
+ if (customType !== "intercom_message" || details === null || typeof details !== "object")
5631
+ return;
5632
+ const id = details.message?.id;
5633
+ return typeof id === "string" ? id : undefined;
5634
+ }
5635
+ function admittedIntercomMessageId(message, ctx) {
5636
+ const custom = message;
5637
+ if (custom?.role !== "custom")
5638
+ return;
5639
+ if (custom.customType !== PROTECTED_RECONCILIATION_CUSTOM_TYPE)
5640
+ return intercomMessageId(custom.customType, custom.details);
5641
+ const intentEntryId = custom.details?.protectedReconciliationOf;
5642
+ if (typeof intentEntryId !== "string")
5643
+ return;
5644
+ const intent = ctx.sessionManager.getEntries().find((entry) => entry.id === intentEntryId);
5645
+ return intent?.type === "custom_message" ? intercomMessageId(intent.customType, intent.details) : undefined;
5646
+ }
5647
+ var PROTECTED_RECONCILIATION_CUSTOM_TYPE = "atomic:protected-streaming-reconciliation";
5578
5648
  var init_lifecycle = __esm(() => {
5579
5649
  init_diagnostics();
5580
5650
  init_intercom_utils();
@@ -6654,8 +6724,9 @@ function createIncomingMessageSender(input) {
6654
6724
  if (!input.canDeliver(generation)) {
6655
6725
  return Promise.reject(new Error("Intercom session retired before inbound delivery"));
6656
6726
  }
6657
- const delivery = (requested === "trigger" || requested === "interrupt") && !mayStartTurn(entry) ? "prelude" : requested;
6658
- if ((delivery === "trigger" || delivery === "interrupt") && trackReplyContext && maySupplyReplyContext(entry)) {
6727
+ const startsTurn = requested === "trigger" || requested === "triggerFollowUp" || requested === "interrupt";
6728
+ const delivery = startsTurn && !mayStartTurn(entry) ? "prelude" : requested;
6729
+ if ((delivery === "trigger" || delivery === "triggerFollowUp" || delivery === "interrupt") && trackReplyContext && maySupplyReplyContext(entry)) {
6659
6730
  input.queueTurnContext(turnContext ?? { from: entry.from, message: entry.message, receivedAt: Date.now() });
6660
6731
  }
6661
6732
  const baseOptions = {
@@ -6663,7 +6734,7 @@ function createIncomingMessageSender(input) {
6663
6734
  persistWhenStreaming: true,
6664
6735
  ...stageAdmissionBarrier ? { stageAdmissionBarrier } : {}
6665
6736
  };
6666
- const options = delivery === "interrupt" ? { ...baseOptions, triggerTurn: true, deliverAs: "interrupt" } : delivery === "trigger" ? { ...baseOptions, triggerTurn: true } : delivery === "followUp" ? { ...baseOptions, deliverAs: "followUp" } : baseOptions;
6737
+ const options = delivery === "interrupt" ? { ...baseOptions, triggerTurn: true, deliverAs: "interrupt" } : delivery === "trigger" ? { ...baseOptions, triggerTurn: true } : delivery === "triggerFollowUp" ? { ...baseOptions, triggerTurn: true, deliverAs: "followUp" } : delivery === "followUp" ? { ...baseOptions, deliverAs: "followUp" } : baseOptions;
6667
6738
  return Promise.resolve(input.pi.sendMessage(buildIncomingCustomMessage(entry), options));
6668
6739
  };
6669
6740
  }
@@ -7902,8 +7973,9 @@ function piIntercomExtension(pi, testOverrides = {}) {
7902
7973
  });
7903
7974
  const activeDelivery = admitActiveSessionInbound(liveContext, (admissionBarrier) => {
7904
7975
  replyTracker.queueTurnContext(replyContext);
7976
+ const delivery = message.delivery === "interrupt" ? "interrupt" : message.delivery === "followUp" ? "triggerFollowUp" : "trigger";
7905
7977
  return retryStableDelivery({
7906
- deliver: () => sendIncomingMessage(entry, "interrupt", messageGeneration, false, undefined, admissionBarrier),
7978
+ deliver: () => sendIncomingMessage(entry, delivery, messageGeneration, false, undefined, admissionBarrier),
7907
7979
  isCurrent: () => Boolean(getLiveContext(liveContext, messageGeneration)) && liveContext.subagentPolicy?.executionEnded?.aborted !== true && liveContext.subagentPolicy?.messageAdmission?.isOpen() !== false
7908
7980
  });
7909
7981
  }, () => foregroundDetachHandoff.claim(from, message, messageGeneration, () => Boolean(getLiveContext(liveContext, messageGeneration))), release);
@@ -8959,7 +9031,8 @@ function createForwardedHandlerMap() {
8959
9031
  agent_end: [],
8960
9032
  tool_execution_start: [],
8961
9033
  tool_execution_end: [],
8962
- model_select: []
9034
+ model_select: [],
9035
+ message_start: []
8963
9036
  };
8964
9037
  }
8965
9038
  function addHandler(captured, event, handler) {
@@ -8993,6 +9066,9 @@ function captureForwardedHandler(captured, event, handler) {
8993
9066
  return;
8994
9067
  case "model_select":
8995
9068
  addHandler(captured, event, handler);
9069
+ return;
9070
+ case "message_start":
9071
+ addHandler(captured, event, handler);
8996
9072
  }
8997
9073
  }
8998
9074
  function addEventHandler(captured, event, handler) {
@@ -9501,6 +9577,13 @@ function intercom(pi, options = {}) {
9501
9577
  if (heavy)
9502
9578
  await lifecycleForward.enqueue(() => dispatchHandlers(heavy, "agent_end", event, ctx));
9503
9579
  });
9580
+ pi.on("message_start", async (event, ctx) => {
9581
+ if (activeLease.retired)
9582
+ return;
9583
+ const heavy = loadedHeavy?.heavy;
9584
+ if (heavy)
9585
+ await lifecycleForward.enqueue(() => dispatchHandlers(heavy, "message_start", event, ctx));
9586
+ });
9504
9587
  pi.on("tool_execution_start", async (event, ctx) => {
9505
9588
  if (activeLease.retired)
9506
9589
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.44-alpha.1",
3
+ "version": "0.9.44-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -276,6 +276,7 @@ How a swarm works:
276
276
  - Decide on evidence, not deference. A peer's approval, rejection, or claim is input to inspect, not a verdict to copy, and each agent still returns its own result.
277
277
  - `contact_supervisor` is for supervisor decisions; explicit user approval gates and authorization for destructive or external actions still apply.
278
278
  - Inside a workflow, `send` to a known pending sibling queues until it starts, and `ask` to a completed sibling that retains a valid conversation reopens it for a post-mortem turn. Named stage subgroups isolate their members from sibling subgroups.
279
+ - Choose `delivery` per `send`/`ask` by urgency. The default `"steer"` arrives after the recipient's current tool calls finish and never cancels its work. Use `"interrupt"` when it must stop now (an urgent correction or a stop order), and `"followUp"` when the message can wait until it finishes (low priority, "after you're done").
279
280
  - Peer asks and one blocking supervisor request may wait concurrently in the same child; mutual asks work when both sides process inbound work.
280
281
  - Scale with the task. For very complex work or requested heavy parallelism, hundreds or thousands of agents self-organize into teams (named groups, per-stage subagent sets, child workflows) and coordinate peer-to-peer like an organization. Groups have no member limit; team members reach their lead with `contact_supervisor`, and leads talk to each other in their shared group.
281
282
 
@@ -115,11 +115,19 @@ export interface BrokerStatusReport {
115
115
  multiNode: BrokerMultiNodeStatus;
116
116
  }
117
117
 
118
+ export type IntercomDeliveryMode = "steer" | "followUp" | "interrupt";
119
+
118
120
  export interface Message {
119
121
  id: string;
120
122
  timestamp: number;
121
123
  replyTo?: string;
122
124
  expectsReply?: boolean;
125
+ /**
126
+ * How a busy recipient receives this message. `steer` (default when absent) queues it until the current turn's tool
127
+ * calls finish, never cancelling in-flight work. `followUp` waits until the recipient's agent finishes and stops.
128
+ * `interrupt` cancels the current model call or cancellable tool call and handles the message next.
129
+ */
130
+ delivery?: IntercomDeliveryMode;
123
131
  /** Correlated delivery refusal: settles a waiting ask, otherwise surfaces as send feedback. */
124
132
  replyError?: string;
125
133
  source?: {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.44-alpha.1",
3
+ "version": "0.9.44-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic's pi-compatible MCP integration for server connections, OAuth, tools, resources, and terminal management.",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.44-alpha.1",
3
+ "version": "0.9.44-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [