@bastani/atomic 0.9.16-alpha.9 → 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.
- package/CHANGELOG.md +120 -0
- package/README.md +4 -4
- package/dist/builtin/intercom/CHANGELOG.md +42 -0
- package/dist/builtin/intercom/README.md +18 -17
- package/dist/builtin/intercom/broker/broker.ts +43 -18
- package/dist/builtin/intercom/broker/client.ts +138 -7
- package/dist/builtin/intercom/broker/group-isolation.ts +3 -3
- package/dist/builtin/intercom/broker/group-membership.ts +55 -0
- package/dist/builtin/intercom/broker/presence-handler.ts +62 -50
- package/dist/builtin/intercom/broker/send-handler.ts +2 -3
- package/dist/builtin/intercom/group.ts +40 -0
- package/dist/builtin/intercom/index.bundle.mjs +490 -183
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/intercom/skills/intercom/SKILL.md +16 -10
- package/dist/builtin/intercom/types.ts +24 -2
- package/dist/builtin/mcp/index.bundle.mjs +202 -52
- package/dist/builtin/mcp/package.json +2 -2
- package/dist/builtin/subagents/CHANGELOG.md +51 -0
- package/dist/builtin/subagents/README.md +3 -3
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/agents/worker.md +1 -1
- package/dist/builtin/subagents/package.json +3 -3
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +422 -140
- package/dist/builtin/web-access/index.bundle.mjs +202 -52
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +70 -0
- package/dist/builtin/workflows/README.md +2 -2
- package/dist/builtin/workflows/builtin/{chunk-h5e65g7g.js → chunk-3y1hmzw6.js} +36 -6
- package/dist/builtin/workflows/builtin/{chunk-nqr34qp3.js → chunk-6rbajjf5.js} +7 -1
- package/dist/builtin/workflows/builtin/{chunk-qe6bfpbd.js → chunk-bz1c9732.js} +15 -3
- package/dist/builtin/workflows/builtin/{chunk-8v8wmb7z.js → chunk-dattsgxg.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-17zk6ffd.js → chunk-gdx0avpx.js} +355 -67
- package/dist/builtin/workflows/builtin/{chunk-rbnj9621.js → chunk-w9c7rkrk.js} +1 -1
- package/dist/builtin/workflows/builtin/goal.js +3 -3
- package/dist/builtin/workflows/builtin/index.js +5 -5
- package/dist/builtin/workflows/builtin/open-claude-design.js +2 -2
- package/dist/builtin/workflows/builtin/ralph.js +3 -3
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +531 -143
- package/dist/builtin/workflows/src/index.js +212 -72
- package/dist/cli/args.js +6 -6
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +2 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/session-picker.d.ts +2 -1
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +3 -2
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +1 -0
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-post-tool-compaction.js +3 -6
- package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +3 -1
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +6 -3
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-state.d.ts.map +1 -1
- package/dist/core/agent-session-state.js +7 -5
- package/dist/core/agent-session-state.js.map +1 -1
- package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
- package/dist/core/agent-session-tool-hooks.js +37 -23
- package/dist/core/agent-session-tool-hooks.js.map +1 -1
- package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
- package/dist/core/agent-session-tool-registry.js +9 -2
- package/dist/core/agent-session-tool-registry.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/builtin-packages.d.ts +4 -0
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +22 -1
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/callback-activity.d.ts.map +1 -1
- package/dist/core/callback-activity.js +6 -3
- package/dist/core/callback-activity.js.map +1 -1
- package/dist/core/extensions/loader-core.d.ts +1 -0
- package/dist/core/extensions/loader-core.d.ts.map +1 -1
- package/dist/core/extensions/loader-core.js +11 -1
- package/dist/core/extensions/loader-core.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +1 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner-registries.d.ts.map +1 -1
- package/dist/core/extensions/runner-registries.js +9 -4
- package/dist/core/extensions/runner-registries.js.map +1 -1
- package/dist/core/extensions/runner-shortcuts.d.ts.map +1 -1
- package/dist/core/extensions/runner-shortcuts.js +1 -0
- package/dist/core/extensions/runner-shortcuts.js.map +1 -1
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mandatory-resource-loader.d.ts +6 -0
- package/dist/core/mandatory-resource-loader.d.ts.map +1 -0
- package/dist/core/mandatory-resource-loader.js +94 -0
- package/dist/core/mandatory-resource-loader.js.map +1 -0
- package/dist/core/mandatory-runtime-tools.d.ts +10 -0
- package/dist/core/mandatory-runtime-tools.d.ts.map +1 -0
- package/dist/core/mandatory-runtime-tools.js +31 -0
- package/dist/core/mandatory-runtime-tools.js.map +1 -0
- package/dist/core/model-resolver-defaults.js +1 -1
- package/dist/core/model-resolver-defaults.js.map +1 -1
- package/dist/core/resource-loader-reload.d.ts.map +1 -1
- package/dist/core/resource-loader-reload.js +24 -13
- package/dist/core/resource-loader-reload.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager-ui-accessors.d.ts +4 -1
- package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-ui-accessors.js +22 -0
- package/dist/core/settings-manager-ui-accessors.js.map +1 -1
- package/dist/core/settings-types.d.ts +4 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/main-session.d.ts.map +1 -1
- package/dist/main-session.js +3 -2
- package/dist/main-session.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.js +2 -2
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
- package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.js +7 -0
- package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +2 -1
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.js +1 -0
- package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-input-handling.js +3 -0
- package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +15 -1
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +6 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +6 -1
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +3 -0
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-slash-commands.js +7 -12
- package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts +1 -0
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +16 -0
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/rpc/rpc-session-binding.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-session-binding.js +2 -0
- package/dist/modes/rpc/rpc-session-binding.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/environment-variables.md +2 -0
- package/docs/extensions.md +1 -1
- package/docs/intercom.md +29 -24
- package/docs/keybindings.md +5 -2
- package/docs/packages.md +1 -1
- package/docs/providers.md +2 -2
- package/docs/sdk.md +9 -7
- package/docs/settings.md +9 -3
- package/docs/subagents.md +2 -2
- package/docs/usage.md +7 -6
- package/docs/workflows.md +11 -7
- package/npm-shrinkwrap.json +59 -77
- 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, {
|
|
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
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
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, {
|
|
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
|
|
5
|
+
/** Two sessions share a group when any normalized membership intersects. */
|
|
6
6
|
export function sameGroup(a: SessionInfo, b: SessionInfo): boolean {
|
|
7
|
-
|
|
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 {
|
|
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
|
|
13
|
+
function broadcastMembershipChange(
|
|
13
14
|
sessions: Map<string, BrokerConnectedSession>,
|
|
14
15
|
write: WriteMessage,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
exclude?: string,
|
|
16
|
+
session: BrokerConnectedSession,
|
|
17
|
+
previous: ReadonlySet<string>,
|
|
18
18
|
): void {
|
|
19
|
-
const
|
|
20
|
-
for (const [id,
|
|
21
|
-
if (id
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
104
|
+
setSessionGroups(session.info, nextGroups, legacyGroup);
|
|
93
105
|
session.info.lastActivity = Date.now();
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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 {
|
|
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) =>
|
|
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;
|