@bastani/atomic 0.9.16-alpha.8 → 0.9.16

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 (212) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +4 -4
  3. package/dist/builtin/intercom/CHANGELOG.md +42 -0
  4. package/dist/builtin/intercom/README.md +18 -17
  5. package/dist/builtin/intercom/broker/broker.ts +43 -18
  6. package/dist/builtin/intercom/broker/client.ts +138 -7
  7. package/dist/builtin/intercom/broker/group-isolation.ts +3 -3
  8. package/dist/builtin/intercom/broker/group-membership.ts +55 -0
  9. package/dist/builtin/intercom/broker/presence-handler.ts +62 -50
  10. package/dist/builtin/intercom/broker/send-handler.ts +2 -3
  11. package/dist/builtin/intercom/group.ts +40 -0
  12. package/dist/builtin/intercom/index.bundle.mjs +556 -185
  13. package/dist/builtin/intercom/package.json +2 -2
  14. package/dist/builtin/intercom/skills/intercom/SKILL.md +16 -10
  15. package/dist/builtin/intercom/types.ts +24 -2
  16. package/dist/builtin/mcp/index.bundle.mjs +268 -54
  17. package/dist/builtin/mcp/package.json +2 -2
  18. package/dist/builtin/subagents/CHANGELOG.md +51 -0
  19. package/dist/builtin/subagents/README.md +3 -3
  20. package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
  21. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  22. package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
  23. package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
  24. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
  25. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  26. package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
  27. package/dist/builtin/subagents/agents/debugger.md +1 -1
  28. package/dist/builtin/subagents/agents/worker.md +1 -1
  29. package/dist/builtin/subagents/package.json +3 -3
  30. package/dist/builtin/subagents/src/extension/index.bundle.mjs +488 -142
  31. package/dist/builtin/web-access/index.bundle.mjs +268 -54
  32. package/dist/builtin/web-access/package.json +2 -2
  33. package/dist/builtin/workflows/CHANGELOG.md +70 -0
  34. package/dist/builtin/workflows/README.md +2 -2
  35. package/dist/builtin/workflows/builtin/{chunk-zf28603f.js → chunk-3y1hmzw6.js} +36 -6
  36. package/dist/builtin/workflows/builtin/{chunk-nqr34qp3.js → chunk-6rbajjf5.js} +7 -1
  37. package/dist/builtin/workflows/builtin/{chunk-v04c5qmd.js → chunk-bz1c9732.js} +15 -3
  38. package/dist/builtin/workflows/builtin/{chunk-cdtd3m3w.js → chunk-dattsgxg.js} +1 -1
  39. package/dist/builtin/workflows/builtin/{chunk-5n10x7j2.js → chunk-gdx0avpx.js} +421 -69
  40. package/dist/builtin/workflows/builtin/{chunk-hdpj1dkw.js → chunk-w9c7rkrk.js} +1 -1
  41. package/dist/builtin/workflows/builtin/goal.js +3 -3
  42. package/dist/builtin/workflows/builtin/index.js +5 -5
  43. package/dist/builtin/workflows/builtin/open-claude-design.js +2 -2
  44. package/dist/builtin/workflows/builtin/ralph.js +3 -3
  45. package/dist/builtin/workflows/package.json +2 -2
  46. package/dist/builtin/workflows/src/extension/index.bundle.mjs +597 -145
  47. package/dist/builtin/workflows/src/index.js +278 -74
  48. package/dist/cli/args.js +6 -6
  49. package/dist/cli/args.js.map +1 -1
  50. package/dist/cli/config-selector.d.ts.map +1 -1
  51. package/dist/cli/config-selector.js +2 -1
  52. package/dist/cli/config-selector.js.map +1 -1
  53. package/dist/cli/session-picker.d.ts +2 -1
  54. package/dist/cli/session-picker.d.ts.map +1 -1
  55. package/dist/cli/session-picker.js +3 -2
  56. package/dist/cli/session-picker.js.map +1 -1
  57. package/dist/cli/startup-ui.d.ts.map +1 -1
  58. package/dist/cli/startup-ui.js +2 -1
  59. package/dist/cli/startup-ui.js.map +1 -1
  60. package/dist/core/agent-session-methods.d.ts +1 -0
  61. package/dist/core/agent-session-methods.d.ts.map +1 -1
  62. package/dist/core/agent-session-methods.js.map +1 -1
  63. package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
  64. package/dist/core/agent-session-post-tool-compaction.js +3 -6
  65. package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
  66. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  67. package/dist/core/agent-session-prompt.js +3 -1
  68. package/dist/core/agent-session-prompt.js.map +1 -1
  69. package/dist/core/agent-session-services.d.ts.map +1 -1
  70. package/dist/core/agent-session-services.js +6 -3
  71. package/dist/core/agent-session-services.js.map +1 -1
  72. package/dist/core/agent-session-state.d.ts.map +1 -1
  73. package/dist/core/agent-session-state.js +7 -5
  74. package/dist/core/agent-session-state.js.map +1 -1
  75. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  76. package/dist/core/agent-session-tool-hooks.js +37 -23
  77. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  78. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  79. package/dist/core/agent-session-tool-registry.js +9 -2
  80. package/dist/core/agent-session-tool-registry.js.map +1 -1
  81. package/dist/core/agent-session.d.ts +2 -0
  82. package/dist/core/agent-session.d.ts.map +1 -1
  83. package/dist/core/agent-session.js +2 -0
  84. package/dist/core/agent-session.js.map +1 -1
  85. package/dist/core/builtin-packages.d.ts +4 -0
  86. package/dist/core/builtin-packages.d.ts.map +1 -1
  87. package/dist/core/builtin-packages.js +22 -1
  88. package/dist/core/builtin-packages.js.map +1 -1
  89. package/dist/core/callback-activity.d.ts.map +1 -1
  90. package/dist/core/callback-activity.js +6 -3
  91. package/dist/core/callback-activity.js.map +1 -1
  92. package/dist/core/extensions/agent-events.d.ts +15 -0
  93. package/dist/core/extensions/agent-events.d.ts.map +1 -1
  94. package/dist/core/extensions/agent-events.js.map +1 -1
  95. package/dist/core/extensions/api-types.d.ts +3 -1
  96. package/dist/core/extensions/api-types.d.ts.map +1 -1
  97. package/dist/core/extensions/api-types.js.map +1 -1
  98. package/dist/core/extensions/event-types.d.ts +2 -2
  99. package/dist/core/extensions/event-types.d.ts.map +1 -1
  100. package/dist/core/extensions/event-types.js.map +1 -1
  101. package/dist/core/extensions/index.d.ts +1 -1
  102. package/dist/core/extensions/index.d.ts.map +1 -1
  103. package/dist/core/extensions/index.js.map +1 -1
  104. package/dist/core/extensions/loader-core.d.ts +1 -0
  105. package/dist/core/extensions/loader-core.d.ts.map +1 -1
  106. package/dist/core/extensions/loader-core.js +11 -1
  107. package/dist/core/extensions/loader-core.js.map +1 -1
  108. package/dist/core/extensions/loader.d.ts +1 -1
  109. package/dist/core/extensions/loader.d.ts.map +1 -1
  110. package/dist/core/extensions/loader.js +1 -1
  111. package/dist/core/extensions/loader.js.map +1 -1
  112. package/dist/core/extensions/runner-registries.d.ts.map +1 -1
  113. package/dist/core/extensions/runner-registries.js +9 -4
  114. package/dist/core/extensions/runner-registries.js.map +1 -1
  115. package/dist/core/extensions/runner-shortcuts.d.ts.map +1 -1
  116. package/dist/core/extensions/runner-shortcuts.js +1 -0
  117. package/dist/core/extensions/runner-shortcuts.js.map +1 -1
  118. package/dist/core/extensions/runner.d.ts +6 -0
  119. package/dist/core/extensions/runner.d.ts.map +1 -1
  120. package/dist/core/extensions/runner.js +65 -1
  121. package/dist/core/extensions/runner.js.map +1 -1
  122. package/dist/core/keybindings.d.ts +5 -0
  123. package/dist/core/keybindings.d.ts.map +1 -1
  124. package/dist/core/keybindings.js +4 -0
  125. package/dist/core/keybindings.js.map +1 -1
  126. package/dist/core/mandatory-resource-loader.d.ts +6 -0
  127. package/dist/core/mandatory-resource-loader.d.ts.map +1 -0
  128. package/dist/core/mandatory-resource-loader.js +94 -0
  129. package/dist/core/mandatory-resource-loader.js.map +1 -0
  130. package/dist/core/mandatory-runtime-tools.d.ts +10 -0
  131. package/dist/core/mandatory-runtime-tools.d.ts.map +1 -0
  132. package/dist/core/mandatory-runtime-tools.js +31 -0
  133. package/dist/core/mandatory-runtime-tools.js.map +1 -0
  134. package/dist/core/model-resolver-defaults.js +1 -1
  135. package/dist/core/model-resolver-defaults.js.map +1 -1
  136. package/dist/core/resource-loader-reload.d.ts.map +1 -1
  137. package/dist/core/resource-loader-reload.js +24 -13
  138. package/dist/core/resource-loader-reload.js.map +1 -1
  139. package/dist/core/sdk.d.ts.map +1 -1
  140. package/dist/core/sdk.js +2 -0
  141. package/dist/core/sdk.js.map +1 -1
  142. package/dist/core/settings-manager-ui-accessors.d.ts +4 -1
  143. package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
  144. package/dist/core/settings-manager-ui-accessors.js +22 -0
  145. package/dist/core/settings-manager-ui-accessors.js.map +1 -1
  146. package/dist/core/settings-types.d.ts +4 -0
  147. package/dist/core/settings-types.d.ts.map +1 -1
  148. package/dist/core/settings-types.js.map +1 -1
  149. package/dist/core/slash-commands.js +1 -1
  150. package/dist/core/slash-commands.js.map +1 -1
  151. package/dist/index-extensions.d.ts +1 -1
  152. package/dist/index-extensions.d.ts.map +1 -1
  153. package/dist/index-extensions.js.map +1 -1
  154. package/dist/main-session.d.ts.map +1 -1
  155. package/dist/main-session.js +3 -2
  156. package/dist/main-session.js.map +1 -1
  157. package/dist/main.d.ts.map +1 -1
  158. package/dist/main.js +6 -0
  159. package/dist/main.js.map +1 -1
  160. package/dist/modes/interactive/components/fast-mode-selector.js +2 -2
  161. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
  164. package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
  165. package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/settings-selector-items.js +7 -0
  167. package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
  168. package/dist/modes/interactive/components/settings-selector-types.d.ts +2 -0
  169. package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
  171. package/dist/modes/interactive/interactive-agent-events.js +2 -1
  172. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  173. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  174. package/dist/modes/interactive/interactive-deferred-startup.js +1 -0
  175. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  176. package/dist/modes/interactive/interactive-extension-runtime.js +1 -1
  177. package/dist/modes/interactive/interactive-extension-runtime.js.map +1 -1
  178. package/dist/modes/interactive/interactive-input-handling.js +3 -0
  179. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  180. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  181. package/dist/modes/interactive/interactive-mode-base.js +15 -1
  182. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  183. package/dist/modes/interactive/interactive-mode-surface.d.ts +6 -1
  184. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  186. package/dist/modes/interactive/interactive-selectors.js +6 -1
  187. package/dist/modes/interactive/interactive-selectors.js.map +1 -1
  188. package/dist/modes/interactive/interactive-session-runtime.js +3 -0
  189. package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
  190. package/dist/modes/interactive/interactive-slash-commands.js +7 -12
  191. package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
  192. package/dist/modes/interactive/interactive-tui.d.ts +1 -0
  193. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  194. package/dist/modes/interactive/interactive-tui.js +16 -0
  195. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  196. package/dist/modes/rpc/rpc-session-binding.d.ts.map +1 -1
  197. package/dist/modes/rpc/rpc-session-binding.js +2 -0
  198. package/dist/modes/rpc/rpc-session-binding.js.map +1 -1
  199. package/docs/compaction.md +1 -1
  200. package/docs/environment-variables.md +2 -0
  201. package/docs/extensions.md +20 -1
  202. package/docs/intercom.md +29 -24
  203. package/docs/keybindings.md +5 -2
  204. package/docs/packages.md +1 -1
  205. package/docs/providers.md +2 -2
  206. package/docs/sdk.md +9 -7
  207. package/docs/settings.md +9 -3
  208. package/docs/subagents.md +2 -2
  209. package/docs/usage.md +7 -6
  210. package/docs/workflows.md +11 -7
  211. package/npm-shrinkwrap.json +59 -77
  212. package/package.json +7 -7
@@ -3,12 +3,14 @@ import net from "net";
3
3
  import { randomUUID } from "crypto";
4
4
  import { writeMessage, createMessageReader } from "./framing.js";
5
5
  import { getBrokerSocketPath } from "./paths.js";
6
- import type { SessionInfo, Message, Attachment, SupervisorRegistration } from "../types.js";
6
+ import type { SessionInfo, Message, Attachment, GroupSummary, SupervisorRegistration } from "../types.js";
7
7
  import { buildSendSignature, PendingSendRegistry } from "./pending-send-registry.js";
8
8
  import { readSubagentMessageSource } from "../source-ownership.js";
9
9
  import { isMessage, isSessionInfo } from "./client-message-validation.js";
10
+ import { normalizeGroups } from "../group.js";
10
11
 
11
12
  const BROKER_SOCKET = getBrokerSocketPath();
13
+ const GROUP_REQUEST_TIMEOUT_MS = 5000;
12
14
  const PRESENCE_ACK_TIMEOUT_MS = 5000;
13
15
 
14
16
 
@@ -58,6 +60,7 @@ export interface PresenceUpdates {
58
60
  name?: string;
59
61
  status?: string;
60
62
  model?: string;
63
+ groups?: string[];
61
64
  group?: string;
62
65
  }
63
66
 
@@ -79,8 +82,15 @@ export class IntercomClient extends EventEmitter {
79
82
  /** Source identity is captured once at connect, never read from env per message. */
80
83
  private _messageSource: Message["source"] | undefined;
81
84
  private pendingSends = new PendingSendRegistry();
85
+ private pendingGroupLists = new Map<string, { resolve: (groups: GroupSummary[]) => void; reject: (error: Error) => void }>();
82
86
  private pendingLists = new Map<string, { resolve: (sessions: SessionInfo[]) => void; reject: (e: Error) => void }>();
83
- private pendingPresence = new Map<string, { resolve: (group: string) => void; reject: (error: Error) => void }>();
87
+ private pendingPresence = new Map<string, {
88
+ resolve: (group: string) => void;
89
+ reject: (error: Error) => void;
90
+ groups?: string[];
91
+ }>();
92
+ private pendingMemberships = new Map<string, { resolve: (groups: string[]) => void; reject: (error: Error) => void }>();
93
+ private _groups = normalizeGroups();
84
94
  private pendingLiveWorkflowStageRoutes = new Map<string, { resolve(): void; reject(error: Error): void }>();
85
95
 
86
96
  private pendingSupervisorAuthorizations = new Map<string, {
@@ -98,13 +108,17 @@ export class IntercomClient extends EventEmitter {
98
108
 
99
109
  private failPending(error: Error): void {
100
110
  this.pendingSends.rejectAll(error);
111
+ for (const pending of this.pendingGroupLists.values()) pending.reject(error);
101
112
  for (const pending of this.pendingLists.values()) pending.reject(error);
102
113
  for (const pending of this.pendingSupervisorAuthorizations.values()) pending.reject(error);
103
114
  for (const pending of this.pendingPresence.values()) pending.reject(error);
115
+ for (const pending of this.pendingMemberships.values()) pending.reject(error);
104
116
  for (const pending of this.pendingLiveWorkflowStageRoutes.values()) pending.reject(error);
105
117
  this.pendingLists.clear();
118
+ this.pendingGroupLists.clear();
106
119
  this.pendingSupervisorAuthorizations.clear();
107
120
  this.pendingPresence.clear();
121
+ this.pendingMemberships.clear();
108
122
  this.pendingLiveWorkflowStageRoutes.clear();
109
123
  }
110
124
 
@@ -115,6 +129,10 @@ export class IntercomClient extends EventEmitter {
115
129
  get supervisorSessionId(): string | null {
116
130
  return this._supervisorSessionId;
117
131
  }
132
+ get groups(): string[] {
133
+ return [...this._groups];
134
+ }
135
+
118
136
 
119
137
 
120
138
  isConnected(): boolean {
@@ -149,6 +167,7 @@ export class IntercomClient extends EventEmitter {
149
167
  return Promise.reject(new Error("Already connected"));
150
168
  }
151
169
  this._messageSource = messageSource ?? readSubagentMessageSource();
170
+ this._groups = normalizeGroups(session.groups, session.group);
152
171
 
153
172
  return new Promise((resolve, reject) => {
154
173
  const socket = net.connect(BROKER_SOCKET);
@@ -323,6 +342,27 @@ export class IntercomClient extends EventEmitter {
323
342
  pending.resolve(sessions);
324
343
  break;
325
344
  }
345
+ case "groups": {
346
+ const { requestId, groups } = brokerMessage;
347
+ if (
348
+ typeof requestId !== "string" ||
349
+ !Array.isArray(groups) ||
350
+ !groups.every((summary) =>
351
+ typeof summary === "object" &&
352
+ summary !== null &&
353
+ typeof summary.group === "string" &&
354
+ typeof summary.sessionCount === "number" &&
355
+ typeof summary.member === "boolean"
356
+ )
357
+ ) {
358
+ throw new Error("Invalid groups message");
359
+ }
360
+ const pending = this.pendingGroupLists.get(requestId);
361
+ if (!pending) return;
362
+ this.pendingGroupLists.delete(requestId);
363
+ pending.resolve(groups as GroupSummary[]);
364
+ break;
365
+ }
326
366
  case "supervisor_authorized": {
327
367
  const { requestId, capability, supervisorSessionId, childName } = brokerMessage;
328
368
  if (typeof requestId !== "string" || typeof capability !== "string"
@@ -465,17 +505,39 @@ export class IntercomClient extends EventEmitter {
465
505
  const pending = this.pendingPresence.get(brokerMessage.requestId);
466
506
  if (!pending) return;
467
507
  this.pendingPresence.delete(brokerMessage.requestId);
508
+ this._groups = pending.groups === undefined
509
+ ? normalizeGroups(undefined, brokerMessage.group)
510
+ : normalizeGroups(pending.groups);
468
511
  pending.resolve(brokerMessage.group);
469
512
  break;
470
513
  }
514
+ case "membership_ack": {
515
+ const { requestId, groups } = brokerMessage;
516
+ if (typeof requestId !== "string" || !Array.isArray(groups) || !groups.every((group) => typeof group === "string")) {
517
+ throw new Error("Invalid membership_ack message");
518
+ }
519
+ const pending = this.pendingMemberships.get(requestId);
520
+ if (!pending) return;
521
+ this.pendingMemberships.delete(requestId);
522
+ const normalizedGroups = [...normalizeGroups(groups)];
523
+ this._groups = new Set(normalizedGroups);
524
+ pending.resolve(normalizedGroups);
525
+ break;
526
+ }
471
527
  case "presence_failed": {
472
528
  if (typeof brokerMessage.requestId !== "string" || typeof brokerMessage.reason !== "string") {
473
529
  throw new Error("Invalid presence_failed message");
474
530
  }
475
- const pending = this.pendingPresence.get(brokerMessage.requestId);
476
- if (!pending) return;
477
- this.pendingPresence.delete(brokerMessage.requestId);
478
- pending.reject(new Error(brokerMessage.reason));
531
+ const presence = this.pendingPresence.get(brokerMessage.requestId);
532
+ if (presence) {
533
+ this.pendingPresence.delete(brokerMessage.requestId);
534
+ presence.reject(new Error(brokerMessage.reason));
535
+ break;
536
+ }
537
+ const membership = this.pendingMemberships.get(brokerMessage.requestId);
538
+ if (!membership) return;
539
+ this.pendingMemberships.delete(brokerMessage.requestId);
540
+ membership.reject(new Error(brokerMessage.reason));
479
541
  break;
480
542
  }
481
543
  case "peer_disconnected": {
@@ -573,6 +635,69 @@ export class IntercomClient extends EventEmitter {
573
635
  }
574
636
  });
575
637
  }
638
+ listGroups(): Promise<GroupSummary[]> {
639
+ let socket: net.Socket;
640
+ try {
641
+ socket = this.requireActiveSocket();
642
+ } catch (error) {
643
+ return Promise.reject(toError(error));
644
+ }
645
+ const requestId = randomUUID();
646
+ return new Promise((resolve, reject) => {
647
+ const timeout = setTimeout(() => {
648
+ if (!this.pendingGroupLists.delete(requestId)) return;
649
+ reject(new Error("List groups timeout"));
650
+ }, GROUP_REQUEST_TIMEOUT_MS);
651
+ this.pendingGroupLists.set(requestId, {
652
+ resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
653
+ reject: (error) => { clearTimeout(timeout); reject(error); },
654
+ });
655
+ try {
656
+ writeMessage(socket, { type: "list_groups", requestId });
657
+ } catch (error) {
658
+ clearTimeout(timeout);
659
+ this.pendingGroupLists.delete(requestId);
660
+ reject(toError(error));
661
+ }
662
+ });
663
+ }
664
+
665
+ joinGroup(group: string): Promise<string[]> {
666
+ return this.updateMembership("join_group", group);
667
+ }
668
+
669
+ leaveGroup(group?: string): Promise<string[]> {
670
+ return this.updateMembership("leave_group", group);
671
+ }
672
+
673
+ private updateMembership(type: "join_group" | "leave_group", group?: string): Promise<string[]> {
674
+ let socket: net.Socket;
675
+ try {
676
+ socket = this.requireActiveSocket();
677
+ } catch (error) {
678
+ return Promise.reject(toError(error));
679
+ }
680
+ const requestId = randomUUID();
681
+ return new Promise((resolve, reject) => {
682
+ const timeout = setTimeout(() => {
683
+ if (!this.pendingMemberships.delete(requestId)) return;
684
+ reject(new Error("Membership update timeout"));
685
+ }, GROUP_REQUEST_TIMEOUT_MS);
686
+ this.pendingMemberships.set(requestId, {
687
+ resolve: (groups) => { clearTimeout(timeout); resolve(groups); },
688
+ reject: (error) => { clearTimeout(timeout); reject(error); },
689
+ });
690
+ try {
691
+ writeMessage(socket, group === undefined ? { type, requestId } : { type, requestId, group });
692
+ } catch (error) {
693
+ clearTimeout(timeout);
694
+ this.pendingMemberships.delete(requestId);
695
+ reject(toError(error));
696
+ }
697
+ });
698
+ }
699
+
700
+
576
701
  authorizeSupervisorChild(childName: string, capability?: string): Promise<SupervisorAuthorization> {
577
702
  let socket: net.Socket;
578
703
  try {
@@ -728,6 +853,8 @@ export class IntercomClient extends EventEmitter {
728
853
  return false;
729
854
  }
730
855
  writeMessage(socket, { type: "presence", ...updates });
856
+ if (updates.groups !== undefined) this._groups = normalizeGroups(updates.groups, updates.group);
857
+ else if (updates.group !== undefined) this._groups = normalizeGroups(undefined, updates.group);
731
858
  return true;
732
859
  }
733
860
 
@@ -752,7 +879,11 @@ export class IntercomClient extends EventEmitter {
752
879
  if (!this.pendingPresence.delete(requestId)) return;
753
880
  reject(new Error("Presence update timeout"));
754
881
  }, PRESENCE_ACK_TIMEOUT_MS);
755
- this.pendingPresence.set(requestId, { resolve: wrappedResolve, reject: wrappedReject });
882
+ this.pendingPresence.set(requestId, {
883
+ resolve: wrappedResolve,
884
+ reject: wrappedReject,
885
+ ...(updates.groups === undefined ? {} : { groups: [...updates.groups] }),
886
+ });
756
887
  try {
757
888
  writeMessage(socket, { type: "presence", ...updates, requestId });
758
889
  } catch (error) {
@@ -1,10 +1,10 @@
1
1
  import type { SessionInfo } from "../types.js";
2
- import { normalizeGroup } from "../group.js";
3
2
  import type { SupervisorChannelCache } from "./supervisor-channel.js";
3
+ import { sessionsShareGroup } from "./group-membership.js";
4
4
 
5
- /** Two sessions share a group when their normalized group ids are equal. */
5
+ /** Two sessions share a group when any normalized membership intersects. */
6
6
  export function sameGroup(a: SessionInfo, b: SessionInfo): boolean {
7
- return normalizeGroup(a.group) === normalizeGroup(b.group);
7
+ return sessionsShareGroup(a, b);
8
8
  }
9
9
 
10
10
  export interface VerticalBypassInput {
@@ -0,0 +1,55 @@
1
+ import { hasGroup, normalizeGroup, normalizeGroups } from "../group.js";
2
+ import type { GroupSummary, SessionInfo } from "../types.js";
3
+ import type { BrokerConnectedSession } from "./send-handler.js";
4
+
5
+ /** Return the normalized memberships carried by a wire session. */
6
+ export function sessionGroups(session: SessionInfo): Set<string> {
7
+ return normalizeGroups(session.groups, session.group);
8
+ }
9
+
10
+ /** Persist normalized memberships while retaining the legacy single-group field. */
11
+ export function setSessionGroups(session: SessionInfo, groups: ReadonlySet<string>, legacyGroup?: string): void {
12
+ const normalized = normalizeGroups(groups);
13
+ session.groups = [...normalized];
14
+ const firstGroup = normalized.values().next().value ?? normalizeGroup();
15
+ session.group = legacyGroup !== undefined && normalized.has(normalizeGroup(legacyGroup))
16
+ ? normalizeGroup(legacyGroup)
17
+ : firstGroup;
18
+ }
19
+
20
+ /** Sessions can route to each other when any normalized membership intersects. */
21
+ export function sessionsShareGroup(a: SessionInfo, b: SessionInfo): boolean {
22
+ const bGroups = sessionGroups(b);
23
+ return [...sessionGroups(a)].some((group) => bGroups.has(group));
24
+ }
25
+
26
+ /** Return each session visible through at least one of the requester's memberships. */
27
+ export function sessionsVisibleTo(
28
+ sessions: ReadonlyMap<string, BrokerConnectedSession>,
29
+ requester: SessionInfo,
30
+ ): SessionInfo[] {
31
+ return [...sessions.values()].map(({ info }) => info).filter((info) => sessionsShareGroup(requester, info));
32
+ }
33
+
34
+ /** Return sessions that belong to one explicit group. */
35
+ export function sessionsInGroup(
36
+ sessions: ReadonlyMap<string, BrokerConnectedSession>,
37
+ group: string | undefined,
38
+ ): SessionInfo[] {
39
+ return [...sessions.values()].map(({ info }) => info).filter((info) => hasGroup(sessionGroups(info), group));
40
+ }
41
+
42
+ /** Summarize every group currently represented by a connected session. */
43
+ export function knownGroupSummaries(
44
+ sessions: ReadonlyMap<string, BrokerConnectedSession>,
45
+ requester: SessionInfo,
46
+ ): GroupSummary[] {
47
+ const counts = new Map<string, number>();
48
+ for (const { info } of sessions.values()) {
49
+ for (const group of sessionGroups(info)) counts.set(group, (counts.get(group) ?? 0) + 1);
50
+ }
51
+ const requesterGroups = sessionGroups(requester);
52
+ return [...counts]
53
+ .sort(([left], [right]) => left.localeCompare(right))
54
+ .map(([group, sessionCount]) => ({ group, sessionCount, member: requesterGroups.has(group) }));
55
+ }
@@ -1,6 +1,7 @@
1
1
  import type net from "node:net";
2
+ import { addGroup, normalizeGroups, removeGroup, validateRuntimeGroup } from "../group.js";
2
3
  import type { BrokerMessage } from "../types.js";
3
- import { normalizeGroup, validateRuntimeGroup } from "../group.js";
4
+ import { sessionGroups, sessionsShareGroup, setSessionGroups } from "./group-membership.js";
4
5
  import type { BrokerConnectedSession } from "./send-handler.js";
5
6
 
6
7
  interface PresenceClientMessage extends Record<string, unknown> {
@@ -9,22 +10,28 @@ interface PresenceClientMessage extends Record<string, unknown> {
9
10
 
10
11
  type WriteMessage = (socket: net.Socket, message: BrokerMessage) => void;
11
12
 
12
- function broadcastToGroup(
13
+ function broadcastMembershipChange(
13
14
  sessions: Map<string, BrokerConnectedSession>,
14
15
  write: WriteMessage,
15
- message: BrokerMessage,
16
- group: string | undefined,
17
- exclude?: string,
16
+ session: BrokerConnectedSession,
17
+ previous: ReadonlySet<string>,
18
18
  ): void {
19
- const target = normalizeGroup(group);
20
- for (const [id, session] of sessions) {
21
- if (id !== exclude && normalizeGroup(session.info.group) === target) {
22
- write(session.socket, message);
19
+ const previousInfo = { ...session.info, groups: [...previous], group: previous.values().next().value };
20
+ for (const [id, peer] of sessions) {
21
+ if (id === session.info.id) continue;
22
+ const sharedBefore = sessionsShareGroup(previousInfo, peer.info);
23
+ const sharedAfter = sessionsShareGroup(session.info, peer.info);
24
+ if (!sharedBefore && sharedAfter) {
25
+ write(peer.socket, { type: "session_joined", session: session.info });
26
+ } else if (sharedBefore && !sharedAfter) {
27
+ write(peer.socket, { type: "session_left", sessionId: session.info.id });
28
+ } else if (sharedAfter) {
29
+ write(peer.socket, { type: "presence_update", session: session.info });
23
30
  }
24
31
  }
25
32
  }
26
33
 
27
- /** Apply one presence update and notify only the affected group members. */
34
+ /** Apply one presence or membership update and notify every affected group member. */
28
35
  export function handleBrokerPresence(
29
36
  socket: net.Socket,
30
37
  clientMessage: PresenceClientMessage,
@@ -37,67 +44,72 @@ export function handleBrokerPresence(
37
44
  throw new Error("Invalid presence requestId");
38
45
  }
39
46
  const requestId = typeof rawRequestId === "string" ? rawRequestId : undefined;
40
- if (currentId === null) {
41
- if (requestId !== undefined) {
42
- write(socket, { type: "presence_failed", requestId, reason: "Session not found" });
43
- }
44
- return;
45
- }
46
- const sessionId = currentId;
47
- const session = sessions.get(sessionId);
47
+ const session = currentId === null ? undefined : sessions.get(currentId);
48
48
  if (!session) {
49
- if (requestId !== undefined) {
50
- write(socket, { type: "presence_failed", requestId, reason: "Session not found" });
51
- }
49
+ if (requestId !== undefined) write(socket, { type: "presence_failed", requestId, reason: "Session not found" });
52
50
  return;
53
51
  }
54
52
 
55
- const fail = (reason: string): boolean => {
53
+ const fail = (reason: string): true => {
56
54
  if (requestId === undefined) throw new Error(reason);
57
55
  write(socket, { type: "presence_failed", requestId, reason });
58
56
  return true;
59
57
  };
58
+ const previousGroups = sessionGroups(session.info);
59
+ let nextGroups = new Set(previousGroups);
60
+ let legacyGroup = session.info.group;
60
61
 
61
- const previousGroup = normalizeGroup(session.info.group);
62
- let nextGroup = previousGroup;
63
- const rawGroup = clientMessage.group;
64
- if (rawGroup !== undefined) {
65
- if (typeof rawGroup !== "string") {
66
- if (fail("Invalid presence group")) return;
62
+ try {
63
+ if (clientMessage.type === "join_group") {
64
+ nextGroups = addGroup(previousGroups, clientMessage.group);
65
+ } else if (clientMessage.type === "leave_group") {
66
+ if (clientMessage.group === undefined) {
67
+ nextGroups = normalizeGroups(undefined, session.registrationGroup);
68
+ legacyGroup = session.registrationGroup;
69
+ } else {
70
+ if (typeof clientMessage.group !== "string") return void fail("Invalid presence group");
71
+ const group = validateRuntimeGroup(clientMessage.group);
72
+ nextGroups = removeGroup(previousGroups, group);
73
+ if (legacyGroup === group) legacyGroup = undefined;
74
+ }
67
75
  } else {
68
- try {
69
- nextGroup = validateRuntimeGroup(rawGroup);
70
- } catch (error) {
71
- const reason = error instanceof Error ? error.message : String(error);
72
- if (fail(`Invalid presence group: ${reason}`)) return;
76
+ const rawGroups = clientMessage.groups;
77
+ if (rawGroups !== undefined) {
78
+ if (!Array.isArray(rawGroups) || !rawGroups.every((group) => typeof group === "string")) {
79
+ return void fail("Invalid presence groups");
80
+ }
81
+ nextGroups = normalizeGroups(rawGroups.map(validateRuntimeGroup));
82
+ legacyGroup = nextGroups.values().next().value;
83
+ } else if (clientMessage.group !== undefined) {
84
+ if (typeof clientMessage.group !== "string") return void fail("Invalid presence group");
85
+ legacyGroup = validateRuntimeGroup(clientMessage.group);
86
+ nextGroups = new Set([legacyGroup]);
73
87
  }
74
88
  }
89
+ } catch (error) {
90
+ const reason = error instanceof Error ? error.message : String(error);
91
+ return void fail(`Invalid presence group: ${reason}`);
75
92
  }
93
+
76
94
  const name = clientMessage.name;
77
- if (name !== undefined && typeof name !== "string") {
78
- if (fail("Invalid presence name")) return;
79
- }
95
+ if (name !== undefined && typeof name !== "string") return void fail("Invalid presence name");
80
96
  const status = clientMessage.status;
81
- if (status !== undefined && typeof status !== "string") {
82
- if (fail("Invalid presence status")) return;
83
- }
97
+ if (status !== undefined && typeof status !== "string") return void fail("Invalid presence status");
84
98
  const model = clientMessage.model;
85
- if (model !== undefined && typeof model !== "string") {
86
- if (fail("Invalid presence model")) return;
87
- }
99
+ if (model !== undefined && typeof model !== "string") return void fail("Invalid presence model");
88
100
 
89
101
  if (typeof name === "string") session.info.name = name;
90
102
  if (typeof status === "string") session.info.status = status;
91
103
  if (typeof model === "string") session.info.model = model;
92
- session.info.group = nextGroup;
104
+ setSessionGroups(session.info, nextGroups, legacyGroup);
93
105
  session.info.lastActivity = Date.now();
94
- if (nextGroup !== previousGroup) {
95
- broadcastToGroup(sessions, write, { type: "session_left", sessionId }, previousGroup, sessionId);
96
- broadcastToGroup(sessions, write, { type: "session_joined", session: session.info }, nextGroup, sessionId);
97
- } else {
98
- broadcastToGroup(sessions, write, { type: "presence_update", session: session.info }, nextGroup, sessionId);
99
- }
106
+ broadcastMembershipChange(sessions, write, session, previousGroups);
107
+
100
108
  if (requestId !== undefined) {
101
- write(socket, { type: "presence_ack", requestId, group: nextGroup });
109
+ if (clientMessage.type === "join_group" || clientMessage.type === "leave_group") {
110
+ write(socket, { type: "membership_ack", requestId, groups: session.info.groups ?? [] });
111
+ } else {
112
+ write(socket, { type: "presence_ack", requestId, group: session.info.group ?? "default" });
113
+ }
102
114
  }
103
115
  }
@@ -6,7 +6,7 @@ import { DeliveredMessageCache } from "./delivered-message-cache.js";
6
6
  import { buildMessageSendSignature } from "./send-signature.js";
7
7
  import { SupervisorChannelCache } from "./supervisor-channel.js";
8
8
  import { isVerticalBypass, sameGroup } from "./group-isolation.js";
9
- import { normalizeGroup } from "../group.js";
9
+ import { sessionsShareGroup } from "./group-membership.js";
10
10
  import { PendingQuestionIndex } from "./pending-question-index.js";
11
11
 
12
12
  export interface BrokerConnectedSession {
@@ -139,12 +139,11 @@ export function handleBrokerSend(
139
139
  // supervisor frame or an exact recorded reply may resolve across groups.
140
140
  const liveWorkflowTarget = sessions.has(trimmedTo) ? undefined : resolveLiveWorkflowStage?.(trimmedTo);
141
141
  const exactIdTarget = sessions.get(trimmedTo) ?? liveWorkflowTarget;
142
- const senderGroup = normalizeGroup(fromSession.info.group);
143
142
  const reachableAcrossGroups = supervisorSend || Boolean(message.replyTo);
144
143
  const candidates = reachableAcrossGroups
145
144
  ? Array.from(sessions.values(), (session) => session.info)
146
145
  : Array.from(sessions.values(), (session) => session.info).filter(
147
- (info) => normalizeGroup(info.group) === senderGroup,
146
+ (info) => sessionsShareGroup(info, fromSession.info),
148
147
  );
149
148
  const resolution = exactIdTarget
150
149
  ? ({ kind: "resolved", session: exactIdTarget.info } as const)
@@ -20,6 +20,11 @@ export const DEFAULT_GROUP = "default";
20
20
 
21
21
  const RESERVED_RUNTIME_GROUPS = new Set(["true", "auto"]);
22
22
 
23
+ /** Runtime representation of a session's normalized group memberships. */
24
+ export type GroupMemberships = ReadonlySet<string>;
25
+
26
+ type GroupMembershipInput = readonly (string | null | undefined)[] | ReadonlySet<string | null | undefined>;
27
+
23
28
  /**
24
29
  * Normalize an intercom group id. Undefined, empty, or whitespace-only values
25
30
  * collapse to the shared {@link DEFAULT_GROUP} so ungrouped sessions all compare
@@ -31,6 +36,22 @@ export function normalizeGroup(value?: string | null): string {
31
36
  return trimmed.length > 0 ? trimmed : DEFAULT_GROUP;
32
37
  }
33
38
 
39
+ /**
40
+ * Normalize serialized group memberships into a non-empty set. Duplicate names
41
+ * collapse, and an absent or empty collection retains legacy default membership.
42
+ * A legacy `group` value is merged so single-group clients remain authoritative.
43
+ */
44
+ export function normalizeGroups(
45
+ values?: GroupMembershipInput | null,
46
+ legacyGroup?: string | null,
47
+ ): Set<string> {
48
+ const groups = new Set<string>();
49
+ for (const value of values ?? []) groups.add(normalizeGroup(value));
50
+ if (legacyGroup !== undefined) groups.add(normalizeGroup(legacyGroup));
51
+ if (groups.size === 0) groups.add(DEFAULT_GROUP);
52
+ return groups;
53
+ }
54
+
34
55
  /**
35
56
  * Validate a group requested by the runtime join action. `default` is a real,
36
57
  * explicit group name; `true` and `auto` remain reserved for subagent auto-grouping.
@@ -46,6 +67,25 @@ export function validateRuntimeGroup(value: unknown): string {
46
67
  return group;
47
68
  }
48
69
 
70
+ /** Add one validated runtime group to a normalized membership set. */
71
+ export function addGroup(groups: GroupMemberships, value: unknown): Set<string> {
72
+ const next = normalizeGroups(groups);
73
+ next.add(validateRuntimeGroup(value));
74
+ return next;
75
+ }
76
+
77
+ /** Remove one normalized group while retaining default membership when none remain. */
78
+ export function removeGroup(groups: GroupMemberships, value?: string | null): Set<string> {
79
+ const next = normalizeGroups(groups);
80
+ next.delete(normalizeGroup(value));
81
+ return normalizeGroups(next);
82
+ }
83
+
84
+ /** Test a normalized group membership. */
85
+ export function hasGroup(groups: GroupMemberships, value?: string | null): boolean {
86
+ return normalizeGroups(groups).has(normalizeGroup(value));
87
+ }
88
+
49
89
  interface HomeGroupContext {
50
90
  orchestrationContext?: { intercomGroup?: string } | undefined;
51
91
  subagentPolicy?: { intercomGroup?: string } | undefined;