@bastani/atomic 0.9.44-alpha.1 → 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 (40) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +10 -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/send-signature.ts +4 -1
  6. package/dist/builtin/intercom/index.bundle.mjs +82 -14
  7. package/dist/builtin/intercom/package.json +1 -1
  8. package/dist/builtin/intercom/skills/intercom/SKILL.md +1 -0
  9. package/dist/builtin/intercom/types.ts +8 -0
  10. package/dist/builtin/mcp/package.json +1 -1
  11. package/dist/builtin/subagents/package.json +1 -1
  12. package/dist/builtin/web-access/package.json +1 -1
  13. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  14. package/dist/builtin/workflows/package.json +1 -1
  15. package/dist/builtin/workflows/src/extension/index.bundle.mjs +94 -5
  16. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +94 -5
  17. package/dist/builtin/workflows/src/index.js +94 -5
  18. package/dist/builtin/workflows/src/local-worker-entry.mjs +94 -5
  19. package/dist/core/agent-session-custom-message-commit.d.ts.map +1 -1
  20. package/dist/core/agent-session-custom-message-commit.js +59 -0
  21. package/dist/core/agent-session-custom-message-commit.js.map +1 -1
  22. package/dist/core/agent-session-methods.d.ts +1 -0
  23. package/dist/core/agent-session-methods.d.ts.map +1 -1
  24. package/dist/core/agent-session-methods.js.map +1 -1
  25. package/dist/core/agent-session-prompt.d.ts +4 -0
  26. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  27. package/dist/core/agent-session-prompt.js +10 -2
  28. package/dist/core/agent-session-prompt.js.map +1 -1
  29. package/dist/core/agent-session.d.ts +2 -0
  30. package/dist/core/agent-session.d.ts.map +1 -1
  31. package/dist/core/agent-session.js +9 -0
  32. package/dist/core/agent-session.js.map +1 -1
  33. package/dist/core/builtin-packages.d.ts.map +1 -1
  34. package/dist/core/builtin-packages.js +13 -13
  35. package/dist/core/builtin-packages.js.map +1 -1
  36. package/docs/intercom/operations.md +1 -1
  37. package/docs/intercom.md +11 -1
  38. package/docs/subagents.md +9 -1
  39. package/npm-shrinkwrap.json +32 -32
  40. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
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
+
5
21
  ## [0.9.44-alpha.1] - 2026-10-24
6
22
 
7
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.
@@ -4,6 +4,16 @@ 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
+
7
17
  ## [0.9.44-alpha.1] - 2026-10-24
8
18
 
9
19
  ### 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 },
@@ -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))
@@ -689,7 +691,8 @@ function buildSendSignature(to, options) {
689
691
  attachments: canonicalizeAttachmentsForSendSignature(options.attachments) ?? [],
690
692
  replyTo: options.replyTo ?? null,
691
693
  expectsReply: options.expectsReply ?? false,
692
- replyError: options.replyError ?? null
694
+ replyError: options.replyError ?? null,
695
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }
693
696
  });
694
697
  }
695
698
 
@@ -1990,6 +1993,7 @@ var init_client = __esm(() => {
1990
1993
  timestamp: Date.now(),
1991
1994
  replyTo: options.replyTo,
1992
1995
  expectsReply: options.expectsReply,
1996
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery },
1993
1997
  replyError: options.replyError,
1994
1998
  source: this._messageSource,
1995
1999
  content: { text: options.text, attachments: options.attachments }
@@ -2790,6 +2794,7 @@ class ReplyTracker {
2790
2794
  pendingAsks = new Map;
2791
2795
  pendingTurnContexts = [];
2792
2796
  currentTurnContext = null;
2797
+ turnContextAdmitted = false;
2793
2798
  constructor(askTimeoutMs = ASK_REPLY_TIMEOUT_MS) {
2794
2799
  this.askTimeoutMs = askTimeoutMs;
2795
2800
  }
@@ -2815,15 +2820,27 @@ class ReplyTracker {
2815
2820
  }
2816
2821
  beginTurn(now = Date.now()) {
2817
2822
  this.pruneExpired(now);
2818
- this.currentTurnContext = this.pendingTurnContexts.shift() ?? null;
2823
+ this.turnContextAdmitted = false;
2824
+ }
2825
+ admitTurnMessage(messageId) {
2826
+ if (this.currentTurnContext?.message.id === messageId) {
2827
+ this.turnContextAdmitted = true;
2828
+ return;
2829
+ }
2830
+ const index = this.pendingTurnContexts.findIndex((context) => context.message.id === messageId);
2831
+ if (index < 0)
2832
+ return;
2833
+ const [admitted] = this.pendingTurnContexts.splice(index, 1);
2834
+ if (this.turnContextAdmitted)
2835
+ return;
2836
+ this.currentTurnContext = admitted;
2837
+ this.turnContextAdmitted = true;
2819
2838
  }
2820
2839
  endTurn() {
2821
2840
  this.currentTurnContext = null;
2822
2841
  }
2823
2842
  restoreTurnContext() {
2824
- if (this.currentTurnContext)
2825
- this.pendingTurnContexts.unshift(this.currentTurnContext);
2826
- this.currentTurnContext = null;
2843
+ this.turnContextAdmitted = false;
2827
2844
  }
2828
2845
  reset() {
2829
2846
  this.pendingAsks.clear();
@@ -2872,10 +2889,11 @@ class ReplyTracker {
2872
2889
  }
2873
2890
  throw new Error(`No pending ask from "${options.to}"`);
2874
2891
  }
2875
- if (pending.length === 1) {
2876
- return pending[0];
2892
+ const visible = pending.filter((context) => !this.pendingTurnContexts.some((queued) => queued.message.id === context.message.id));
2893
+ if (visible.length === 1) {
2894
+ return visible[0];
2877
2895
  }
2878
- if (pending.length === 0) {
2896
+ if (visible.length === 0) {
2879
2897
  throw new Error("No active intercom context to reply to");
2880
2898
  }
2881
2899
  throw new Error("Multiple pending asks — specify `to`");
@@ -4078,6 +4096,10 @@ A valid target outside the known set queues speculatively with a \`notInKnownSet
4078
4096
  undeliverable at terminal only if never delivered. Use \`ask\` only for a live, reply-capable target.
4079
4097
  Stage paths win over same-named owned subgroups for send/ask; \`list\` with \`group\` continues to
4080
4098
  select the group.
4099
+ Choose \`delivery\` per send/ask by urgency. The default "steer" arrives after the recipient's current
4100
+ tool calls finish and never cancels its work. Use "interrupt" when it must stop now (an urgent correction
4101
+ or a stop order); it cancels the current model call or tool call. Use "followUp" when the message can wait
4102
+ until the recipient finishes (low priority, "after you're done"). An idle recipient starts a turn.
4081
4103
  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
4104
 
4083
4105
  Usage:
@@ -4089,6 +4111,7 @@ Usage:
4089
4111
  intercom({ action: "leave" }) → Return to your resolved home group
4090
4112
  intercom({ action: "send", to: "session-name", message: "..." }) → Send message (shared group only)
4091
4113
  intercom({ action: "ask", to: "session-name", message: "..." }) → Ask and wait for reply
4114
+ intercom({ action: "send", to: "session-name", message: "...", delivery: "interrupt" }) → Cancel the recipient's current work
4092
4115
  intercom({ action: "reply", message: "..." }) → Reply to the active or exact pending ask
4093
4116
  intercom({ action: "pending" }) → List unresolved inbound asks
4094
4117
  intercom({ action: "status" }) → Show connection status and all your groups
@@ -4113,6 +4136,9 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4113
4136
  content: Type2.String(),
4114
4137
  language: Type2.Optional(Type2.String())
4115
4138
  }))),
4139
+ delivery: Type2.Optional(Type2.Union([Type2.Literal("steer"), Type2.Literal("followUp"), Type2.Literal("interrupt")], {
4140
+ 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)."
4141
+ })),
4116
4142
  replyTo: Type2.Optional(Type2.String({
4117
4143
  description: "Exact pending-ask message ID or unique 8-hex UUID prefix; disambiguates concurrent asks, including asks from one sender"
4118
4144
  })),
@@ -4130,6 +4156,7 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4130
4156
  }
4131
4157
  const toolSessionId = ctx.sessionManager.getSessionId();
4132
4158
  const { action, to, message, replyTo, group } = params;
4159
+ const deliveryOption = params.delivery === undefined || params.delivery === "steer" ? {} : { delivery: params.delivery };
4133
4160
  const attachments = params.attachments?.map((attachment) => ({ ...attachment }));
4134
4161
  const executeAttempt = async (retryToken, _signal) => {
4135
4162
  if (retryToken !== undefined) {
@@ -4440,7 +4467,8 @@ ${message}${attachmentText}`);
4440
4467
  logicalTarget: to,
4441
4468
  text: message,
4442
4469
  attachments,
4443
- replyTo
4470
+ replyTo,
4471
+ ...deliveryOption
4444
4472
  });
4445
4473
  if (result.queued === true) {
4446
4474
  retryIdentities.release(retryIdentity);
@@ -4672,6 +4700,7 @@ ${message}${attachmentText}`);
4672
4700
  attachments,
4673
4701
  replyTo,
4674
4702
  expectsReply: true,
4703
+ ...deliveryOption,
4675
4704
  onReplyTarget: wait.bindSender
4676
4705
  });
4677
4706
  if (sendResult.queued === true) {
@@ -5561,6 +5590,13 @@ function registerIntercomLifecycle(pi, deps) {
5561
5590
  deps.syncPresenceIdentity(ctx.sessionManager.getSessionId());
5562
5591
  activeReplyTracker().beginTurn();
5563
5592
  });
5593
+ pi.on("message_start", (event, ctx) => {
5594
+ if (!deps.getLiveContext(ctx))
5595
+ return;
5596
+ const id = admittedIntercomMessageId(event.message, ctx);
5597
+ if (id !== undefined)
5598
+ activeReplyTracker().admitTurnMessage(id);
5599
+ });
5564
5600
  pi.on("model_select", (event, ctx) => {
5565
5601
  if (!deps.getLiveContext(ctx))
5566
5602
  return;
@@ -5575,6 +5611,25 @@ function registerIntercomLifecycle(pi, deps) {
5575
5611
  }
5576
5612
  });
5577
5613
  }
5614
+ function intercomMessageId(customType, details) {
5615
+ if (customType !== "intercom_message" || details === null || typeof details !== "object")
5616
+ return;
5617
+ const id = details.message?.id;
5618
+ return typeof id === "string" ? id : undefined;
5619
+ }
5620
+ function admittedIntercomMessageId(message, ctx) {
5621
+ const custom = message;
5622
+ if (custom?.role !== "custom")
5623
+ return;
5624
+ if (custom.customType !== PROTECTED_RECONCILIATION_CUSTOM_TYPE)
5625
+ return intercomMessageId(custom.customType, custom.details);
5626
+ const intentEntryId = custom.details?.protectedReconciliationOf;
5627
+ if (typeof intentEntryId !== "string")
5628
+ return;
5629
+ const intent = ctx.sessionManager.getEntries().find((entry) => entry.id === intentEntryId);
5630
+ return intent?.type === "custom_message" ? intercomMessageId(intent.customType, intent.details) : undefined;
5631
+ }
5632
+ var PROTECTED_RECONCILIATION_CUSTOM_TYPE = "atomic:protected-streaming-reconciliation";
5578
5633
  var init_lifecycle = __esm(() => {
5579
5634
  init_diagnostics();
5580
5635
  init_intercom_utils();
@@ -6654,8 +6709,9 @@ function createIncomingMessageSender(input) {
6654
6709
  if (!input.canDeliver(generation)) {
6655
6710
  return Promise.reject(new Error("Intercom session retired before inbound delivery"));
6656
6711
  }
6657
- const delivery = (requested === "trigger" || requested === "interrupt") && !mayStartTurn(entry) ? "prelude" : requested;
6658
- if ((delivery === "trigger" || delivery === "interrupt") && trackReplyContext && maySupplyReplyContext(entry)) {
6712
+ const startsTurn = requested === "trigger" || requested === "triggerFollowUp" || requested === "interrupt";
6713
+ const delivery = startsTurn && !mayStartTurn(entry) ? "prelude" : requested;
6714
+ if ((delivery === "trigger" || delivery === "triggerFollowUp" || delivery === "interrupt") && trackReplyContext && maySupplyReplyContext(entry)) {
6659
6715
  input.queueTurnContext(turnContext ?? { from: entry.from, message: entry.message, receivedAt: Date.now() });
6660
6716
  }
6661
6717
  const baseOptions = {
@@ -6663,7 +6719,7 @@ function createIncomingMessageSender(input) {
6663
6719
  persistWhenStreaming: true,
6664
6720
  ...stageAdmissionBarrier ? { stageAdmissionBarrier } : {}
6665
6721
  };
6666
- const options = delivery === "interrupt" ? { ...baseOptions, triggerTurn: true, deliverAs: "interrupt" } : delivery === "trigger" ? { ...baseOptions, triggerTurn: true } : delivery === "followUp" ? { ...baseOptions, deliverAs: "followUp" } : baseOptions;
6722
+ 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
6723
  return Promise.resolve(input.pi.sendMessage(buildIncomingCustomMessage(entry), options));
6668
6724
  };
6669
6725
  }
@@ -7902,8 +7958,9 @@ function piIntercomExtension(pi, testOverrides = {}) {
7902
7958
  });
7903
7959
  const activeDelivery = admitActiveSessionInbound(liveContext, (admissionBarrier) => {
7904
7960
  replyTracker.queueTurnContext(replyContext);
7961
+ const delivery = message.delivery === "interrupt" ? "interrupt" : message.delivery === "followUp" ? "triggerFollowUp" : "trigger";
7905
7962
  return retryStableDelivery({
7906
- deliver: () => sendIncomingMessage(entry, "interrupt", messageGeneration, false, undefined, admissionBarrier),
7963
+ deliver: () => sendIncomingMessage(entry, delivery, messageGeneration, false, undefined, admissionBarrier),
7907
7964
  isCurrent: () => Boolean(getLiveContext(liveContext, messageGeneration)) && liveContext.subagentPolicy?.executionEnded?.aborted !== true && liveContext.subagentPolicy?.messageAdmission?.isOpen() !== false
7908
7965
  });
7909
7966
  }, () => foregroundDetachHandoff.claim(from, message, messageGeneration, () => Boolean(getLiveContext(liveContext, messageGeneration))), release);
@@ -8959,7 +9016,8 @@ function createForwardedHandlerMap() {
8959
9016
  agent_end: [],
8960
9017
  tool_execution_start: [],
8961
9018
  tool_execution_end: [],
8962
- model_select: []
9019
+ model_select: [],
9020
+ message_start: []
8963
9021
  };
8964
9022
  }
8965
9023
  function addHandler(captured, event, handler) {
@@ -8993,6 +9051,9 @@ function captureForwardedHandler(captured, event, handler) {
8993
9051
  return;
8994
9052
  case "model_select":
8995
9053
  addHandler(captured, event, handler);
9054
+ return;
9055
+ case "message_start":
9056
+ addHandler(captured, event, handler);
8996
9057
  }
8997
9058
  }
8998
9059
  function addEventHandler(captured, event, handler) {
@@ -9501,6 +9562,13 @@ function intercom(pi, options = {}) {
9501
9562
  if (heavy)
9502
9563
  await lifecycleForward.enqueue(() => dispatchHandlers(heavy, "agent_end", event, ctx));
9503
9564
  });
9565
+ pi.on("message_start", async (event, ctx) => {
9566
+ if (activeLease.retired)
9567
+ return;
9568
+ const heavy = loadedHeavy?.heavy;
9569
+ if (heavy)
9570
+ await lifecycleForward.enqueue(() => dispatchHandlers(heavy, "message_start", event, ctx));
9571
+ });
9504
9572
  pi.on("tool_execution_start", async (event, ctx) => {
9505
9573
  if (activeLease.retired)
9506
9574
  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.2",
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.2",
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.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.44-alpha.1",
3
+ "version": "0.9.44-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.44-alpha.2] - 2026-10-24
10
+
11
+ ### Fixed
12
+
13
+ - A sticky Intercom broadcast, such as `workflow:<rootRunId>/**`, that reaches many workflow stages no longer makes the workflow journal grow quadratically. Recording one more delivery used to rewrite every stage's deliveries in the run's queued-message event; each event now stores only the deliveries that changed since the run's previous event. In a 160-worker run this shrinks those events from about 160 MB to about 9 MB. Journals written by earlier versions read and resume unchanged ([#3920](https://github.com/bastani-inc/atomic/issues/3920))
14
+
9
15
  ## [0.9.44-alpha.1] - 2026-10-24
10
16
 
11
17
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.44-alpha.1",
3
+ "version": "0.9.44-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -6808,6 +6808,13 @@ function removeProcessExitCleanup(cleanup) {
6808
6808
  if (processExitCleanups.delete(cleanup) && processExitCleanups.size === 0)
6809
6809
  process.off("exit", runProcessExitCleanups);
6810
6810
  }
6811
+ function snapshotNeedsHistory(tail) {
6812
+ const event = tail.snapshot.event;
6813
+ if (event.type !== RUN_EVENT_TYPES.messageQueued)
6814
+ return false;
6815
+ const payload = event.payload;
6816
+ return hasPendingStageDeliveryDelta(payload.pendingStageMessages);
6817
+ }
6811
6818
  function* invoke(action) {
6812
6819
  try {
6813
6820
  const result = action();
@@ -6923,8 +6930,11 @@ class JournalDurableBackend {
6923
6930
  this.projection.applyWorkflowSnapshot(event.payload.handle);
6924
6931
  this.prompts.set(entry.runId, new PromptReservationState(event.payload.handle.pendingPrompts, event.payload.prompts));
6925
6932
  } else {
6926
- const handle = event.payload;
6927
6933
  const previous = this.projection.getWorkflow(entry.runId);
6934
+ const handle = event.type === RUN_EVENT_TYPES.messageQueued && hasPendingStageDeliveryDelta(event.payload.pendingStageMessages) ? {
6935
+ ...event.payload,
6936
+ pendingStageMessages: decodePendingStageDeliveries(previous?.pendingStageMessages ?? [], event.payload.pendingStageMessages ?? [])
6937
+ } : event.payload;
6928
6938
  this.projection.applyWorkflowSnapshot(handle);
6929
6939
  if (event.type === RUN_EVENT_TYPES.runStarted || previous?.pendingPrompts !== handle.pendingPrompts)
6930
6940
  this.prompts.delete(entry.runId);
@@ -7220,11 +7230,23 @@ class JournalDurableBackend {
7220
7230
  for (const message of messages)
7221
7231
  if (message.runId !== logicalRunId)
7222
7232
  throw new JournalMessageScopeError(logicalRunId, message.runId);
7223
- yield* this.snapshot({
7233
+ const next = {
7224
7234
  ...handle,
7225
7235
  pendingStageMessages: replacePendingStageMessagesForRun(handle.pendingStageMessages ?? [], logicalRunId, messages),
7226
7236
  updatedAt: Math.max(Date.now(), handle.updatedAt + 1)
7227
- }, RUN_EVENT_TYPES.messageQueued);
7237
+ };
7238
+ const encoded = encodePendingStageDeliveries(handle.pendingStageMessages ?? [], next.pendingStageMessages ?? []);
7239
+ const base = this.sequences.get(runId);
7240
+ if (encoded !== undefined && base !== undefined) {
7241
+ try {
7242
+ yield* this.append(runId, { type: RUN_EVENT_TYPES.messageQueued, payload: { ...next, pendingStageMessages: encoded } }, { expectSeq: base });
7243
+ return true;
7244
+ } catch (error) {
7245
+ if (!(error instanceof JournalCasConflictError))
7246
+ throw error;
7247
+ }
7248
+ }
7249
+ yield* this.snapshot(next, RUN_EVENT_TYPES.messageQueued);
7228
7250
  return true;
7229
7251
  }
7230
7252
  async archivePendingStageMessage(runId, message) {
@@ -7318,7 +7340,7 @@ class JournalDurableBackend {
7318
7340
  if (summarized !== undefined && summarized.seq === version.seq && summarized.at === version.at)
7319
7341
  return;
7320
7342
  const tail = yield* invoke(() => journal.snapshotTail(runId));
7321
- if (tail !== undefined)
7343
+ if (tail !== undefined && !snapshotNeedsHistory(tail))
7322
7344
  return this.summarize(runId, tail);
7323
7345
  if (summarized === undefined && version.seq === 0)
7324
7346
  return;
@@ -7544,6 +7566,69 @@ class JournalDurableBackend {
7544
7566
  }
7545
7567
  }
7546
7568
  }
7569
+ function identity(message) {
7570
+ return JSON.stringify([message.runId, message.id]);
7571
+ }
7572
+ function uniqueByIdentity(messages) {
7573
+ const result = new Map;
7574
+ const repeated = new Set;
7575
+ for (const message of messages) {
7576
+ const key = identity(message);
7577
+ if (result.has(key))
7578
+ repeated.add(key);
7579
+ result.set(key, message);
7580
+ }
7581
+ for (const key of repeated)
7582
+ result.delete(key);
7583
+ return result;
7584
+ }
7585
+ function isDeltaEncoded(message) {
7586
+ return message.deliveryDelta !== undefined;
7587
+ }
7588
+ function encodePendingStageDeliveries(previous, next) {
7589
+ const bases = uniqueByIdentity(previous);
7590
+ const nextIdentities = uniqueByIdentity(next);
7591
+ let encodedAny = false;
7592
+ const encoded = next.map((message) => {
7593
+ const base = bases.get(identity(message))?.deliveries;
7594
+ const deliveries = message.deliveries;
7595
+ if (base === undefined || deliveries === undefined || !nextIdentities.has(identity(message)))
7596
+ return message;
7597
+ const drop = [];
7598
+ let kept = 0;
7599
+ base.forEach((delivery, index) => {
7600
+ if (kept < deliveries.length && JSON.stringify(delivery) === JSON.stringify(deliveries[kept]))
7601
+ kept++;
7602
+ else
7603
+ drop.push(index);
7604
+ });
7605
+ const add = deliveries.slice(kept);
7606
+ if (drop.length + add.length >= deliveries.length)
7607
+ return message;
7608
+ encodedAny = true;
7609
+ const { deliveries: _full, ...rest } = message;
7610
+ return { ...rest, deliveryDelta: drop.length === 0 ? { add } : { drop, add } };
7611
+ });
7612
+ return encodedAny ? encoded : undefined;
7613
+ }
7614
+ function decodePendingStageDeliveries(previous, messages) {
7615
+ if (!messages.some(isDeltaEncoded))
7616
+ return messages;
7617
+ const bases = uniqueByIdentity(previous);
7618
+ return messages.map((message) => {
7619
+ if (!isDeltaEncoded(message))
7620
+ return message;
7621
+ const base = bases.get(identity(message))?.deliveries;
7622
+ if (base === undefined)
7623
+ throw new Error(`atomic-workflows: journal delivery change for ${identity(message)} has no base entry`);
7624
+ const { deliveryDelta, ...rest } = message;
7625
+ const dropped = new Set(deliveryDelta.drop ?? []);
7626
+ return { ...rest, deliveries: [...base.filter((_, index) => !dropped.has(index)), ...deliveryDelta.add] };
7627
+ });
7628
+ }
7629
+ function hasPendingStageDeliveryDelta(messages) {
7630
+ return messages?.some(isDeltaEncoded) === true;
7631
+ }
7547
7632
  var PROCESS_HOLDER_ID, processExitCleanups, JournalOwnershipError, JournalBackendClosedError, JournalOperationAbortedError, JournalExecutionRetiredError, JournalMessageScopeError;
7548
7633
  var init_journal_backend = __esm(() => {
7549
7634
  init_workflow_instance_ownership_error();
@@ -61647,7 +61732,8 @@ function buildSendSignature(to, options) {
61647
61732
  attachments: canonicalizeAttachmentsForSendSignature(options.attachments) ?? [],
61648
61733
  replyTo: options.replyTo ?? null,
61649
61734
  expectsReply: options.expectsReply ?? false,
61650
- replyError: options.replyError ?? null
61735
+ replyError: options.replyError ?? null,
61736
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery }
61651
61737
  });
61652
61738
  }
61653
61739
 
@@ -61833,6 +61919,8 @@ function isMessage(value2) {
61833
61919
  return false;
61834
61920
  if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean")
61835
61921
  return false;
61922
+ if (message.delivery !== undefined && message.delivery !== "steer" && message.delivery !== "followUp" && message.delivery !== "interrupt")
61923
+ return false;
61836
61924
  if (message.replyError !== undefined && typeof message.replyError !== "string")
61837
61925
  return false;
61838
61926
  if (message.source !== undefined && !isMessageSource(message.source))
@@ -63042,6 +63130,7 @@ var init_client = __esm(() => {
63042
63130
  timestamp: Date.now(),
63043
63131
  replyTo: options.replyTo,
63044
63132
  expectsReply: options.expectsReply,
63133
+ ...options.delivery === undefined || options.delivery === "steer" ? {} : { delivery: options.delivery },
63045
63134
  replyError: options.replyError,
63046
63135
  source: this._messageSource,
63047
63136
  content: { text: options.text, attachments: options.attachments }