@bastani/atomic 0.9.9-alpha.2 → 0.9.9-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 (68) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +7 -0
  5. package/dist/builtin/intercom/README.md +10 -3
  6. package/dist/builtin/intercom/broker/client.ts +2 -0
  7. package/dist/builtin/intercom/broker/send-handler.ts +22 -20
  8. package/dist/builtin/intercom/foreground-detach-handoff.ts +3 -1
  9. package/dist/builtin/intercom/inbound-idle-queue.ts +82 -0
  10. package/dist/builtin/intercom/index-heavy.ts +47 -37
  11. package/dist/builtin/intercom/intercom-tool.ts +6 -4
  12. package/dist/builtin/intercom/intercom-utils.ts +30 -0
  13. package/dist/builtin/intercom/lifecycle.ts +8 -5
  14. package/dist/builtin/intercom/package.json +1 -1
  15. package/dist/builtin/intercom/reply-tracker.ts +19 -19
  16. package/dist/builtin/intercom/session-target.ts +68 -0
  17. package/dist/builtin/intercom/skills/intercom/SKILL.md +7 -4
  18. package/dist/builtin/intercom/source-ownership.ts +34 -0
  19. package/dist/builtin/intercom/subagent-relay.ts +45 -20
  20. package/dist/builtin/intercom/terminal-ordering-barrier.ts +111 -0
  21. package/dist/builtin/intercom/types.ts +5 -0
  22. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  23. package/dist/builtin/mcp/package.json +1 -1
  24. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  25. package/dist/builtin/subagents/package.json +1 -1
  26. package/dist/builtin/subagents/src/runs/background/notify.ts +53 -9
  27. package/dist/builtin/subagents/src/shared/types-config.ts +1 -0
  28. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  29. package/dist/builtin/web-access/package.json +1 -1
  30. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  31. package/dist/builtin/workflows/package.json +1 -1
  32. package/dist/builtin/workflows/src/extension/workflow-tool.ts +18 -2
  33. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  34. package/dist/core/agent-session-extension-bindings.js +9 -0
  35. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  36. package/dist/core/agent-session-message-queue.d.ts +4 -1
  37. package/dist/core/agent-session-message-queue.d.ts.map +1 -1
  38. package/dist/core/agent-session-message-queue.js +35 -0
  39. package/dist/core/agent-session-message-queue.js.map +1 -1
  40. package/dist/core/agent-session-methods.d.ts +2 -1
  41. package/dist/core/agent-session-methods.d.ts.map +1 -1
  42. package/dist/core/agent-session-methods.js.map +1 -1
  43. package/dist/core/extensions/api-types.d.ts +3 -1
  44. package/dist/core/extensions/api-types.d.ts.map +1 -1
  45. package/dist/core/extensions/api-types.js.map +1 -1
  46. package/dist/core/extensions/index.d.ts +1 -1
  47. package/dist/core/extensions/index.d.ts.map +1 -1
  48. package/dist/core/extensions/index.js.map +1 -1
  49. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  50. package/dist/core/extensions/loader-api.js +4 -0
  51. package/dist/core/extensions/loader-api.js.map +1 -1
  52. package/dist/core/extensions/loader-runtime.d.ts.map +1 -1
  53. package/dist/core/extensions/loader-runtime.js +1 -0
  54. package/dist/core/extensions/loader-runtime.js.map +1 -1
  55. package/dist/core/extensions/message-types.d.ts +3 -0
  56. package/dist/core/extensions/message-types.d.ts.map +1 -1
  57. package/dist/core/extensions/message-types.js.map +1 -1
  58. package/dist/core/extensions/runner.d.ts.map +1 -1
  59. package/dist/core/extensions/runner.js +1 -0
  60. package/dist/core/extensions/runner.js.map +1 -1
  61. package/dist/core/extensions/runtime-types.d.ts +3 -1
  62. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  63. package/dist/core/extensions/runtime-types.js.map +1 -1
  64. package/docs/extensions.md +13 -0
  65. package/docs/subagents.md +2 -0
  66. package/docs/workflows.md +2 -2
  67. package/npm-shrinkwrap.json +23 -23
  68. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import type { Message, SessionInfo } from "./types.ts";
2
+ import { resolveSessionTarget, sessionTargetFailureReason } from "./session-target.js";
2
3
 
3
4
  export interface IntercomContext {
4
5
  from: SessionInfo;
@@ -6,14 +7,6 @@ export interface IntercomContext {
6
7
  receivedAt: number;
7
8
  }
8
9
 
9
- function matchesPendingSender(context: IntercomContext, to: string): boolean {
10
- if (context.from.id === to) {
11
- return true;
12
- }
13
-
14
- return context.from.name?.toLowerCase() === to.toLowerCase();
15
- }
16
-
17
10
  export class ReplyTracker {
18
11
  private readonly pendingAsks = new Map<string, IntercomContext>();
19
12
  private readonly pendingTurnContexts: IntercomContext[] = [];
@@ -56,21 +49,28 @@ export class ReplyTracker {
56
49
  }
57
50
 
58
51
  const pending = Array.from(this.pendingAsks.values());
59
- if (pending.length === 1) {
60
- return pending[0]!;
61
- }
62
-
63
52
  if (options.to) {
64
- const matches = pending.filter((context) => matchesPendingSender(context, options.to!));
65
- if (matches.length === 1) {
66
- return matches[0]!;
53
+ const senders = [...new Map(
54
+ pending.map((context) => [context.from.id, context.from] as const),
55
+ ).values()];
56
+ const resolution = resolveSessionTarget(senders, options.to);
57
+ if (resolution.kind !== "resolved") {
58
+ if (resolution.kind === "not_found") {
59
+ throw new Error(`No pending ask from "${options.to}"`);
60
+ }
61
+ throw new Error(sessionTargetFailureReason(options.to, resolution));
67
62
  }
63
+ const matches = pending.filter(
64
+ (context) => context.from.id === resolution.session.id,
65
+ );
66
+ if (matches.length === 1) return matches[0]!;
68
67
  if (matches.length > 1) {
69
- throw new Error(`Multiple pending asks from \"${options.to}\" — use the sender session ID instead.`);
70
- }
71
- if (pending.length > 1) {
72
- throw new Error(`No pending ask from \"${options.to}\"`);
68
+ throw new Error(`Multiple pending asks from "${options.to}"`);
73
69
  }
70
+ throw new Error(`No pending ask from "${options.to}"`);
71
+ }
72
+ if (pending.length === 1) {
73
+ return pending[0]!;
74
74
  }
75
75
 
76
76
  if (pending.length === 0) {
@@ -0,0 +1,68 @@
1
+ import type { SessionInfo } from "./types.js";
2
+
3
+ export type SessionTargetResolution =
4
+ | { kind: "resolved"; session: SessionInfo }
5
+ | { kind: "ambiguous_name"; matches: readonly SessionInfo[] }
6
+ | { kind: "ambiguous_prefix"; matches: readonly SessionInfo[] }
7
+ | { kind: "not_found" };
8
+
9
+ /** Resolve an Intercom session by exact ID, exact case-insensitive name, or unique ID prefix. */
10
+ export function resolveSessionTarget(
11
+ sessions: readonly SessionInfo[],
12
+ nameOrId: string,
13
+ ): SessionTargetResolution {
14
+ const target = nameOrId.trim();
15
+ const exactId = sessions.find((session) => session.id === target);
16
+ if (exactId !== undefined) return { kind: "resolved", session: exactId };
17
+
18
+ const lowerTarget = target.toLowerCase();
19
+ const exactNames = sessions.filter(
20
+ (session) => session.name?.toLowerCase() === lowerTarget,
21
+ );
22
+ if (exactNames.length === 1) {
23
+ return { kind: "resolved", session: exactNames[0]! };
24
+ }
25
+ if (exactNames.length > 1) {
26
+ return { kind: "ambiguous_name", matches: exactNames };
27
+ }
28
+
29
+ const prefixedIds = sessions.filter((session) => session.id.startsWith(target));
30
+ if (prefixedIds.length === 1) {
31
+ return { kind: "resolved", session: prefixedIds[0]! };
32
+ }
33
+ if (prefixedIds.length > 1) {
34
+ return { kind: "ambiguous_prefix", matches: prefixedIds };
35
+ }
36
+ return { kind: "not_found" };
37
+ }
38
+
39
+ export function sessionTargetFailureReason(
40
+ target: string,
41
+ resolution: Exclude<SessionTargetResolution, { kind: "resolved" }>,
42
+ ): string {
43
+ if (resolution.kind === "ambiguous_name") {
44
+ return `Multiple sessions named "${target}" are connected. Use the session ID instead.`;
45
+ }
46
+ if (resolution.kind === "ambiguous_prefix") {
47
+ const labels = resolution.matches
48
+ .map((session) => `${session.name ?? "Unnamed session"} (${session.id})`)
49
+ .join(", ");
50
+ return `Ambiguous session ID prefix "${target}" matches: ${labels}. Use a longer ID or an exact session name.`;
51
+ }
52
+ return "Session not found";
53
+ }
54
+
55
+ export interface SessionListingClient {
56
+ listSessions(): Promise<SessionInfo[]>;
57
+ }
58
+
59
+ /** Resolve a target through the broker's current session list. */
60
+ export async function resolveSessionTargetId(
61
+ client: SessionListingClient,
62
+ nameOrId: string,
63
+ ): Promise<string | null> {
64
+ const resolution = resolveSessionTarget(await client.listSessions(), nameOrId);
65
+ if (resolution.kind === "resolved") return resolution.session.id;
66
+ if (resolution.kind === "not_found") return null;
67
+ throw new Error(sessionTargetFailureReason(nameOrId, resolution));
68
+ }
@@ -67,13 +67,16 @@ intercom({
67
67
 
68
68
  ### Pattern 2: Quick Status Check
69
69
 
70
- Before sending, verify who's connected:
70
+ Before sending, verify who's connected. The short ID printed by `list` is directly usable by `send`, `ask`, and targeted `reply`:
71
71
 
72
72
  ```typescript
73
73
  intercom({ action: "list" })
74
- // → Shows all connected sessions with names, cwd, models, and live status (`idle`, `thinking`, `tool:<name>`)
74
+ // → planner (6332faab) /workspace (model) [idle]
75
+ intercom({ action: "ask", to: "6332faab", message: "Which option should I use?" })
75
76
  ```
76
77
 
78
+ Intercom resolves exact full IDs first, exact case-insensitive names second, and unique ID prefixes last. A colliding prefix returns an ambiguity error; use a longer displayed ID or an exact name rather than guessing.
79
+
77
80
  ### Pattern 3: Reply Naturally
78
81
 
79
82
  When responding to an inbound ask, prefer `reply` instead of reconstructing raw IDs:
@@ -199,9 +202,9 @@ message, treat it as a `contact_supervisor` escalation.
199
202
  |--------|----------|----------|
200
203
  | `send` | Fire-and-forget | You don't need a response |
201
204
  | `ask` | Blocks until reply (10 min timeout) | You need an answer to continue |
202
- | `reply` | Responds to the active or pending inbound ask | You were asked something and need to answer naturally |
205
+ | `reply` | Responds to the active or pending inbound ask; `to` accepts a displayed short ID | You were asked something and need to answer naturally |
203
206
  | `pending` | Lists unresolved inbound asks | You need to see who is waiting before replying |
204
- | `list` | Returns all sessions with live status | You need to discover targets or choose an idle peer |
207
+ | `list` | Returns all sessions with actionable short IDs and live status | You need to discover targets or choose an idle peer |
205
208
  | `status` | Returns your connection state | Troubleshooting |
206
209
 
207
210
  ## Optional: Visible Peer Sessions via cmux, tmux, or psmux (Windows rewrite of tmux that has fully parity with tmux)
@@ -0,0 +1,34 @@
1
+ import { APP_NAME, getEnvValue } from "@bastani/atomic";
2
+ import type { Message } from "./types.js";
3
+
4
+ const ENV_PREFIX = APP_NAME.toUpperCase();
5
+ const SUBAGENT_RUN_ID_ENV = `${ENV_PREFIX}_SUBAGENT_RUN_ID`;
6
+ const SUBAGENT_CHILD_AGENT_ENV = `${ENV_PREFIX}_SUBAGENT_CHILD_AGENT`;
7
+ const SUBAGENT_CHILD_INDEX_ENV = `${ENV_PREFIX}_SUBAGENT_CHILD_INDEX`;
8
+
9
+ export function buildSubagentMessageSource(
10
+ runIdValue: string | undefined,
11
+ agentValue: string | undefined,
12
+ indexValue: string | undefined,
13
+ ): Message["source"] | undefined {
14
+ const subagentRunId = runIdValue?.trim();
15
+ if (!subagentRunId) return undefined;
16
+ const subagentAgent = agentValue?.trim();
17
+ const rawIndex = indexValue?.trim();
18
+ const parsedIndex = rawIndex === undefined ? undefined : Number(rawIndex);
19
+ const subagentIndex = parsedIndex !== undefined && Number.isInteger(parsedIndex) && parsedIndex >= 0
20
+ ? parsedIndex : undefined;
21
+ return {
22
+ subagentRunId,
23
+ ...(subagentAgent ? { subagentAgent } : {}),
24
+ ...(subagentIndex !== undefined ? { subagentIndex } : {}),
25
+ };
26
+ }
27
+
28
+ export function readSubagentMessageSource(): Message["source"] | undefined {
29
+ return buildSubagentMessageSource(
30
+ getEnvValue(SUBAGENT_RUN_ID_ENV),
31
+ getEnvValue(SUBAGENT_CHILD_AGENT_ENV),
32
+ getEnvValue(SUBAGENT_CHILD_INDEX_ENV),
33
+ );
34
+ }
@@ -6,11 +6,14 @@ import {
6
6
  SUBAGENT_CONTROL_INTERCOM_EVENT,
7
7
  SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
8
8
  SUBAGENT_RESULT_INTERCOM_EVENT,
9
+ SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT,
9
10
  getErrorMessage,
10
11
  parseSubagentIntercomPayload,
12
+ parseSubagentResultBarrier,
11
13
  } from "./intercom-utils.js";
12
14
  import { DeliveredMessageCache } from "./broker/delivered-message-cache.js";
13
15
  import { buildSendSignature } from "./broker/send-signature.js";
16
+ import { emitGlobalTerminalOrderingBarrier } from "./terminal-ordering-barrier.js";
14
17
 
15
18
  interface SubagentRelayDeps {
16
19
  runtimeGeneration(): number;
@@ -26,26 +29,47 @@ interface SubagentRelayDeps {
26
29
  export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps): void {
27
30
  const { getLiveContext, currentSessionTargetMatches, sendIncomingMessage, ensureConnected, resolveSessionTarget } = deps;
28
31
  const localDeliveries = new DeliveredMessageCache();
29
- function deliverLocalSubagentRelayMessage(sender: "subagent-control" | "subagent-result", status: string, messageText: string): void {
32
+ function deliverLocalSubagentRelayMessage(
33
+ sender: "subagent-control" | "subagent-result",
34
+ status: string,
35
+ messageText: string,
36
+ terminalBarrier?: { runId: string; terminalId?: string; sourceSessionTargets: string[] },
37
+ ): void {
30
38
  const now = Date.now();
31
- sendIncomingMessage({
39
+ const entry = {
32
40
  from: {
33
- id: sender,
34
- name: sender,
35
- cwd: deps.runtimeContext()?.cwd ?? process.cwd(),
36
- model: sender,
37
- pid: process.pid,
38
- startedAt: now,
39
- lastActivity: now,
40
- status,
41
- },
42
- message: {
43
- id: randomUUID(),
44
- timestamp: now,
45
- content: { text: messageText },
41
+ id: sender, name: sender, cwd: deps.runtimeContext()?.cwd ?? process.cwd(),
42
+ model: sender, pid: process.pid, startedAt: now, lastActivity: now, status,
46
43
  },
44
+ message: { id: randomUUID(), timestamp: now, content: { text: messageText } },
47
45
  bodyText: messageText,
48
- }, "trigger");
46
+ };
47
+ let dispatched = false;
48
+ if (terminalBarrier) {
49
+ const payload = {
50
+ ...terminalBarrier,
51
+ terminalAt: now,
52
+ source: "result-relay" as const,
53
+ dispatch: (prefix: Array<{ customType: string; content: string; display: boolean; details?: unknown }>) => {
54
+ const terminalMessage = {
55
+ customType: "intercom_message",
56
+ content: `**📨 From ${sender}** (${entry.from.cwd})\n\n${messageText}`,
57
+ display: true,
58
+ details: entry,
59
+ };
60
+ if (typeof pi.sendMessages === "function") {
61
+ pi.sendMessages([...prefix, terminalMessage], { triggerTurn: true });
62
+ } else {
63
+ for (const message of prefix) pi.sendMessage(message, { deliverAs: "steer" });
64
+ pi.sendMessage(terminalMessage, { triggerTurn: true });
65
+ }
66
+ dispatched = true;
67
+ },
68
+ };
69
+ pi.events.emit(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, payload);
70
+ emitGlobalTerminalOrderingBarrier(payload);
71
+ }
72
+ if (!dispatched) sendIncomingMessage(entry, "trigger");
49
73
  }
50
74
  function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void {
51
75
  pi.appendEntry(entryType, {
@@ -68,7 +92,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
68
92
  }
69
93
  function deliverLocal(
70
94
  parsed: ReturnType<typeof parseSubagentIntercomPayload> & {},
71
- options: { sender: "subagent-control" | "subagent-result"; status: string; errorEntryType: string; acknowledge?: boolean },
95
+ options: { sender: "subagent-control" | "subagent-result"; status: string; errorEntryType: string; acknowledge?: boolean; terminalBarrier?: { runId: string; terminalId?: string; sourceSessionTargets: string[] } },
72
96
  ): void {
73
97
  try {
74
98
  const signature = buildSendSignature(parsed.to, { text: parsed.message });
@@ -77,7 +101,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
77
101
  throw new Error(`Intercom message ID '${parsed.requestId}' was already delivered with a different target or payload`);
78
102
  }
79
103
  if (match === "miss") {
80
- deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message);
104
+ deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message, options.terminalBarrier);
81
105
  if (parsed.requestId) localDeliveries.record(parsed.requestId, signature);
82
106
  }
83
107
  acknowledgeResult(options, parsed.requestId, true);
@@ -102,6 +126,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
102
126
  }): void {
103
127
  const parsed = parseSubagentIntercomPayload(payload);
104
128
  if (!parsed) return;
129
+ const terminalBarrier = options.sender === "subagent-result" ? parseSubagentResultBarrier(payload) ?? undefined : undefined;
105
130
 
106
131
  const relayGeneration = deps.runtimeGeneration();
107
132
  void (async () => {
@@ -112,7 +137,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
112
137
  return;
113
138
  }
114
139
  if (currentSessionTargetMatches(parsed.to)) {
115
- deliverLocal(parsed, options);
140
+ deliverLocal(parsed, { ...options, terminalBarrier });
116
141
  return;
117
142
  }
118
143
  if (!deps.runtimeStarted()) {
@@ -145,7 +170,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
145
170
  return;
146
171
  }
147
172
  if (currentSessionTargetMatches(parsed.to, target, activeClient)) {
148
- deliverLocal(parsed, options);
173
+ deliverLocal(parsed, { ...options, terminalBarrier });
149
174
  return;
150
175
  }
151
176
 
@@ -0,0 +1,111 @@
1
+ import type { ExtensionAPI } from "@bastani/atomic";
2
+ import { SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, type InboundMessageEntry } from "./intercom-utils.js";
3
+ import type { InboundIdleQueue } from "./inbound-idle-queue.js";
4
+
5
+ export { SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT } from "./intercom-utils.js";
6
+
7
+ const GLOBAL_BARRIER_HANDLER = "__atomicTerminalOrderingBarrierHandler";
8
+
9
+ export interface OrderedTerminalPreludeMessage {
10
+ customType: "intercom_message";
11
+ content: string;
12
+ display: true;
13
+ details: InboundMessageEntry;
14
+ }
15
+
16
+ export interface TerminalOrderingBarrier {
17
+ runId: string;
18
+ terminalId: string;
19
+ terminalAt: number;
20
+ source: "background-notify" | "result-relay";
21
+ sourceSessionTargets: string[];
22
+ dispatch?(prefix: OrderedTerminalPreludeMessage[]): void;
23
+ }
24
+
25
+ interface TerminalOrderingBarrierOptions {
26
+ queue: InboundIdleQueue;
27
+ toMessage?(entry: InboundMessageEntry): OrderedTerminalPreludeMessage;
28
+ deliver(entry: InboundMessageEntry, mode: "prelude"): void;
29
+ onDrain?(): void;
30
+ }
31
+
32
+ function parseBarrier(value: unknown): TerminalOrderingBarrier | null {
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
34
+ const record = value as Record<string, unknown>;
35
+ if (typeof record.runId !== "string" || !record.runId) return null;
36
+ if (typeof record.terminalAt !== "number" || !Number.isFinite(record.terminalAt)) return null;
37
+ if (record.source !== "background-notify" && record.source !== "result-relay") return null;
38
+ if (!Array.isArray(record.sourceSessionTargets)) return null;
39
+ const sourceSessionTargets = record.sourceSessionTargets
40
+ .filter((target): target is string => typeof target === "string")
41
+ .map((target) => target.trim())
42
+ .filter(Boolean);
43
+ if (sourceSessionTargets.length === 0) return null;
44
+ const terminalId = typeof record.terminalId === "string" && record.terminalId
45
+ ? record.terminalId : `${record.source}:${record.terminalAt}`;
46
+ return {
47
+ runId: record.runId,
48
+ terminalId,
49
+ terminalAt: record.terminalAt,
50
+ source: record.source,
51
+ sourceSessionTargets,
52
+ ...(typeof record.dispatch === "function"
53
+ ? { dispatch: record.dispatch as (prefix: OrderedTerminalPreludeMessage[]) => void }
54
+ : {}),
55
+ };
56
+ }
57
+
58
+ export function emitGlobalTerminalOrderingBarrier(value: unknown): void {
59
+ const handler = (globalThis as Record<string, unknown>)[GLOBAL_BARRIER_HANDLER];
60
+ if (typeof handler === "function") (handler as (payload: unknown) => void)(value);
61
+ }
62
+
63
+ /** Claims accepted same-child messages before terminal delivery. */
64
+ export function registerTerminalOrderingBarrier(
65
+ pi: Pick<ExtensionAPI, "events">,
66
+ options: TerminalOrderingBarrierOptions,
67
+ ): () => void {
68
+ const drainedTargetsByTerminal = new Map<string, Set<string>>();
69
+ const handle = (value: unknown): void => {
70
+ const barrier = parseBarrier(value);
71
+ if (!barrier) return;
72
+ const terminalKey = `${barrier.runId}\0${barrier.terminalId}`;
73
+ const drainedTargets = drainedTargetsByTerminal.get(terminalKey) ?? new Set<string>();
74
+ const pendingTargets = barrier.sourceSessionTargets.filter((target) => !drainedTargets.has(target));
75
+ if (pendingTargets.length === 0) return;
76
+ const claim = options.queue.claimOrdinarySourceTargets(barrier.runId, pendingTargets, barrier.terminalAt);
77
+ if (barrier.dispatch && options.toMessage) {
78
+ try {
79
+ barrier.dispatch(claim.entries.map(options.toMessage));
80
+ } catch (error) {
81
+ claim.rollbackFrom(0);
82
+ throw error;
83
+ }
84
+ } else {
85
+ for (let index = 0; index < claim.entries.length; index++) {
86
+ try {
87
+ options.deliver(claim.entries[index]!, "prelude");
88
+ } catch (error) {
89
+ claim.rollbackFrom(index);
90
+ throw error;
91
+ }
92
+ }
93
+ }
94
+ if ((barrier.dispatch && options.toMessage) || claim.entries.length > 0) {
95
+ for (const target of pendingTargets) drainedTargets.add(target);
96
+ drainedTargetsByTerminal.set(terminalKey, drainedTargets);
97
+ }
98
+ if (drainedTargetsByTerminal.size > 1_000) {
99
+ const oldestKey = drainedTargetsByTerminal.keys().next().value;
100
+ if (oldestKey !== undefined) drainedTargetsByTerminal.delete(oldestKey);
101
+ }
102
+ if (claim.entries.length > 0) options.onDrain?.();
103
+ };
104
+ (globalThis as Record<string, unknown>)[GLOBAL_BARRIER_HANDLER] = handle;
105
+ const unsubscribe = pi.events.on(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, handle);
106
+ return () => {
107
+ unsubscribe();
108
+ const globals = globalThis as Record<string, unknown>;
109
+ if (globals[GLOBAL_BARRIER_HANDLER] === handle) delete globals[GLOBAL_BARRIER_HANDLER];
110
+ };
111
+ }
@@ -14,6 +14,11 @@ export interface Message {
14
14
  timestamp: number;
15
15
  replyTo?: string;
16
16
  expectsReply?: boolean;
17
+ source?: {
18
+ subagentRunId: string;
19
+ subagentAgent?: string;
20
+ subagentIndex?: number;
21
+ };
17
22
  content: {
18
23
  text: string;
19
24
  attachments?: Attachment[];
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.9-alpha.3] - 2026-07-14
11
+
12
+ ### Changed
13
+
14
+ - Published a synchronized Atomic 0.9.9-alpha.3 prerelease for the MCP extension; no functional MCP changes were made after 0.9.9-alpha.2.
15
+
10
16
  ## [0.9.9-alpha.2] - 2026-07-14
11
17
 
12
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.9-alpha.2",
3
+ "version": "0.9.9-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.9-alpha.3] - 2026-07-14
6
+
7
+ ### Fixed
8
+
9
+ - Preserved background async-child completion chronology with Intercom by emitting a terminal-ordering barrier and atomically batching claimed same-child messages before the completion notice, with a compatibility fallback for hosts without batched message admission.
10
+
5
11
  ## [0.9.9-alpha.2] - 2026-07-14
6
12
 
7
13
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.9-alpha.2",
3
+ "version": "0.9.9-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and background runs. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -3,14 +3,17 @@
3
3
  */
4
4
 
5
5
  import type { ExtensionAPI } from "@bastani/atomic";
6
+ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
6
7
  import { buildCompletionKey, hasSeenWithTtl, recordSeen } from "./completion-dedupe.ts";
7
8
  import type { CompletionNotificationEnvelope } from "./completion-notification.ts";
8
- import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
9
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT } from "../../shared/types.ts";
9
10
 
10
11
  interface ChainStepResult {
11
12
  agent: string;
12
13
  output: string;
13
14
  success: boolean;
15
+ intercomTarget?: string;
16
+ index?: number;
14
17
  }
15
18
 
16
19
  export interface SubagentNotifyDetails {
@@ -25,6 +28,8 @@ export interface SubagentNotifyDetails {
25
28
 
26
29
  interface SubagentResult {
27
30
  id: string | null;
31
+ runId?: string;
32
+ notificationId?: string;
28
33
  agent: string | null;
29
34
  success: boolean;
30
35
  summary: string;
@@ -41,6 +46,13 @@ interface SubagentResult {
41
46
  totalTasks?: number;
42
47
  }
43
48
 
49
+ interface TerminalPreludeMessage {
50
+ customType: string;
51
+ content: string;
52
+ display: boolean;
53
+ details?: unknown;
54
+ }
55
+
44
56
  interface NotifyRegistration {
45
57
  unsubscribe: () => void;
46
58
  }
@@ -87,6 +99,30 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
87
99
  const seen = getNotifySeen(pi);
88
100
  const ttlMs = 10 * 60 * 1000;
89
101
 
102
+ const emitTerminalOrderingBarrier = (result: SubagentResult, dispatch?: (prefix: TerminalPreludeMessage[]) => void): void => {
103
+ const runId = result.runId ?? result.id;
104
+ if (!runId) return;
105
+ const resultTargets = result.results?.map((child, arrayIndex) =>
106
+ child.intercomTarget?.trim() || resolveSubagentIntercomTarget(runId, child.agent, child.index ?? arrayIndex)) ?? [];
107
+ const sourceSessionTargets = resultTargets.length > 0
108
+ ? resultTargets
109
+ : result.agent ? [resolveSubagentIntercomTarget(runId, result.agent, 0)] : [];
110
+ if (sourceSessionTargets.length === 0) return;
111
+ const terminalId = result.notificationId?.startsWith("completion-notify-")
112
+ ? result.notificationId.slice("completion-notify-".length)
113
+ : result.notificationId;
114
+ const payload = {
115
+ runId,
116
+ ...(terminalId ? { terminalId } : {}),
117
+ terminalAt: Number.isFinite(result.timestamp) ? result.timestamp : Date.now(),
118
+ source: "background-notify" as const,
119
+ sourceSessionTargets,
120
+ ...(dispatch ? { dispatch } : {}),
121
+ };
122
+ pi.events.emit(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, payload);
123
+ const globalHandler = (globalThis as Record<string, unknown>).__atomicTerminalOrderingBarrierHandler;
124
+ if (typeof globalHandler === "function") (globalHandler as (value: unknown) => void)(payload);
125
+ };
90
126
  let registration: NotifyRegistration;
91
127
  const handleComplete = (data: unknown) => {
92
128
  if (registry.get(pi) !== registration) return;
@@ -130,15 +166,23 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
130
166
  .filter((line) => line !== undefined)
131
167
  .join("\n");
132
168
 
169
+ const terminalMessage = {
170
+ customType: "subagent-notify",
171
+ content,
172
+ display: true,
173
+ };
133
174
  try {
134
- pi.sendMessage(
135
- {
136
- customType: "subagent-notify",
137
- content,
138
- display: true,
139
- },
140
- { triggerTurn: true },
141
- );
175
+ let dispatched = false;
176
+ emitTerminalOrderingBarrier(result, (prefix) => {
177
+ if (typeof pi.sendMessages === "function") {
178
+ pi.sendMessages([...prefix, terminalMessage], { triggerTurn: true });
179
+ } else {
180
+ for (const message of prefix) pi.sendMessage(message, { deliverAs: "steer" });
181
+ pi.sendMessage(terminalMessage, { triggerTurn: true });
182
+ }
183
+ dispatched = true;
184
+ });
185
+ if (!dispatched) pi.sendMessage(terminalMessage, { triggerTurn: true });
142
186
  recordSeen(seen, key, Date.now());
143
187
  result.acknowledge?.(true);
144
188
  } catch (error) {
@@ -48,6 +48,7 @@ export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
48
48
  export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
49
49
  export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
50
50
  export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
51
+ export const SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT = "subagent:terminal-ordering-barrier";
51
52
  export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
52
53
 
53
54
  // ============================================================================
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.9-alpha.3] - 2026-07-14
8
+
9
+ ### Changed
10
+
11
+ - Published a synchronized Atomic 0.9.9-alpha.3 prerelease for the web-access extension; no functional web-access changes were made after 0.9.9-alpha.2.
12
+
7
13
  ## [0.9.9-alpha.2] - 2026-07-14
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.9-alpha.2",
3
+ "version": "0.9.9-alpha.3",
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.9-alpha.3] - 2026-07-14
10
+
11
+ ### Fixed
12
+
13
+ - Made `workflow({ action: "status", runId })` apply the same exact-or-unique-prefix contract as the other run actions. A prefix shared by multiple retained runs now returns the standard ambiguity diagnostic instead of silently inspecting the first match, so abbreviated run IDs printed by status surfaces remain safe and actionable.
14
+
9
15
  ## [0.9.9-alpha.2] - 2026-07-14
10
16
 
11
17
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.9-alpha.2",
3
+ "version": "0.9.9-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [