@ctliz/agent-intercom-codex 0.11.0-connect.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.codex-plugin/plugin.json +39 -0
  2. package/.mcp.json +10 -0
  3. package/COPYRIGHT +6 -0
  4. package/LICENSE +661 -0
  5. package/LICENSE_TRANSITION.md +25 -0
  6. package/README.md +572 -0
  7. package/THIRD_PARTY_NOTICES.md +17 -0
  8. package/assets/logo-generated.png +0 -0
  9. package/assets/logo.svg +22 -0
  10. package/boss-control-outbox.ts +195 -0
  11. package/broker/access-credential.ts +76 -0
  12. package/broker/access-registry.ts +464 -0
  13. package/broker/audit.ts +60 -0
  14. package/broker/authorization.ts +54 -0
  15. package/broker/boss-adapter.ts +531 -0
  16. package/broker/boss-control-ledger.ts +306 -0
  17. package/broker/broker.ts +2327 -0
  18. package/broker/client.ts +1111 -0
  19. package/broker/framing.ts +87 -0
  20. package/broker/ownership.ts +61 -0
  21. package/broker/paths.ts +161 -0
  22. package/broker/spawn.ts +453 -0
  23. package/codex/app-server-client.ts +562 -0
  24. package/codex/boss-client.ts +34 -0
  25. package/codex/bridge-config.ts +282 -0
  26. package/codex/bridge-daemon.ts +913 -0
  27. package/codex/clipboard.ts +142 -0
  28. package/codex/coi.ts +802 -0
  29. package/codex/contact.ts +54 -0
  30. package/codex/mcp-protocol.ts +248 -0
  31. package/codex/runtime.ts +488 -0
  32. package/codex/server.ts +60 -0
  33. package/codex/team.ts +249 -0
  34. package/codex/tui-input.ts +174 -0
  35. package/config.ts +195 -0
  36. package/dist/bridge-daemon.mjs +3781 -0
  37. package/dist/broker.mjs +3012 -0
  38. package/dist/build-info.json +12 -0
  39. package/dist/codex-server.mjs +2890 -0
  40. package/dist/coi.mjs +4686 -0
  41. package/durable-json.ts +25 -0
  42. package/licenses/MIT-pi-intercom.txt +21 -0
  43. package/outbound-outbox.ts +116 -0
  44. package/package.json +89 -0
  45. package/protocol-v4/contract.ts +27 -0
  46. package/provider/protected-service.ts +162 -0
  47. package/provider/provider.mjs +34 -0
  48. package/scripts/build-info.mjs +58 -0
  49. package/scripts/build.mjs +55 -0
  50. package/skills/codex-intercom/SKILL.md +104 -0
  51. package/tsconfig.json +16 -0
  52. package/types.ts +225 -0
@@ -0,0 +1,1111 @@
1
+ import { EventEmitter } from "events";
2
+ import net from "net";
3
+ import { randomUUID } from "crypto";
4
+ import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
5
+ import {
6
+ BOSS_RUN_FEATURE,
7
+ parseBrokerCapabilityAdvertisement,
8
+ type BossControlEnvelope,
9
+ } from "@ctliz/agent-intercom-core/boss";
10
+ import { writeMessage, createMessageReader } from "./framing.ts";
11
+ import { intercomScopeIdFromEnv, parseIntercomScopeId } from "../protocol-v4/contract.ts";
12
+ import { PersistentOutboundOutbox } from "../outbound-outbox.ts";
13
+ import { PersistentBossControlOutbox } from "../boss-control-outbox.ts";
14
+ import { loadRemoteAccessCredential, writeRemoteSessionCredential, type LoadedRemoteAccessCredential } from "./access-credential.ts";
15
+ import { parseBossControlAck, parseBossControlResult } from "./boss-control-ledger.ts";
16
+ import {
17
+ parseBossParticipantBindingMetadata,
18
+ parseBossParticipantRegistrationMetadata,
19
+ bossControlKind,
20
+ parseExactRegisteredFrame,
21
+ parseExactRegistrationFrame,
22
+ } from "./boss-adapter.ts";
23
+ import { types as nodeUtilTypes } from "node:util";
24
+ import {
25
+ getBrokerConnectTarget,
26
+ INTERCOM_PROTOCOL_NAME,
27
+ INTERCOM_PROTOCOL_VERSION,
28
+ type BrokerConnectTarget,
29
+ } from "./paths.ts";
30
+ import type {
31
+ AskCancellationReason,
32
+ DeliveryFailureCode,
33
+ SessionInfo,
34
+ Message,
35
+ Attachment,
36
+ SessionRegistration,
37
+ BossParticipantBindingMetadata,
38
+ BossParticipantRegistrationMetadata,
39
+ BossControlFailureCode,
40
+ } from "../types.ts";
41
+
42
+ export interface SendOptions {
43
+ text: string;
44
+ attachments?: Attachment[];
45
+ replyTo?: string;
46
+ expectsReply?: boolean;
47
+ messageId?: string;
48
+ }
49
+
50
+ export interface SendResult {
51
+ id: string;
52
+ accepted: boolean;
53
+ delivered: boolean;
54
+ deliveryId?: string;
55
+ code?: DeliveryFailureCode;
56
+ reason?: string;
57
+ }
58
+
59
+ export type BossControlSendResult = {
60
+ requestId: string;
61
+ messageId: string;
62
+ idempotencyKey: string;
63
+ status: "delivered";
64
+ delivered: true;
65
+ deliveryId: string;
66
+ } | {
67
+ requestId: string;
68
+ messageId: string;
69
+ idempotencyKey: string;
70
+ status: "rejected";
71
+ delivered: false;
72
+ deliveryId?: string;
73
+ code: BossControlFailureCode;
74
+ reason: string;
75
+ };
76
+
77
+ function toError(error: unknown): Error {
78
+ return error instanceof Error ? error : new Error(String(error));
79
+ }
80
+
81
+ function connectToBrokerTarget(target: BrokerConnectTarget): net.Socket {
82
+ return typeof target === "string"
83
+ ? net.connect(target)
84
+ : net.connect({ host: target.host, port: target.port });
85
+ }
86
+
87
+ function isAttachment(value: unknown): value is Attachment {
88
+ if (typeof value !== "object" || value === null) {
89
+ return false;
90
+ }
91
+
92
+ const attachment = value as Record<string, unknown>;
93
+
94
+ if (
95
+ attachment.type !== "file"
96
+ && attachment.type !== "snippet"
97
+ && attachment.type !== "context"
98
+ ) {
99
+ return false;
100
+ }
101
+
102
+ if (typeof attachment.name !== "string" || typeof attachment.content !== "string") {
103
+ return false;
104
+ }
105
+
106
+ return attachment.language === undefined || typeof attachment.language === "string";
107
+ }
108
+
109
+ function isMessage(value: unknown): value is Message {
110
+ if (typeof value !== "object" || value === null) {
111
+ return false;
112
+ }
113
+
114
+ const message = value as Record<string, unknown>;
115
+
116
+ if (typeof message.id !== "string" || typeof message.timestamp !== "number") {
117
+ return false;
118
+ }
119
+
120
+ if (message.replyTo !== undefined && typeof message.replyTo !== "string") {
121
+ return false;
122
+ }
123
+
124
+ if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean") {
125
+ return false;
126
+ }
127
+
128
+ if (typeof message.content !== "object" || message.content === null) {
129
+ return false;
130
+ }
131
+
132
+ const content = message.content as Record<string, unknown>;
133
+ if (typeof content.text !== "string") {
134
+ return false;
135
+ }
136
+
137
+ return content.attachments === undefined
138
+ || (Array.isArray(content.attachments) && content.attachments.every(isAttachment));
139
+ }
140
+
141
+ function isSessionInfo(value: unknown): value is SessionInfo {
142
+ if (typeof value !== "object" || value === null) {
143
+ return false;
144
+ }
145
+
146
+ const session = value as Record<string, unknown>;
147
+
148
+ if (
149
+ typeof session.id !== "string"
150
+ || typeof session.cwd !== "string"
151
+ || typeof session.model !== "string"
152
+ || typeof session.pid !== "number"
153
+ || typeof session.startedAt !== "number"
154
+ || typeof session.lastActivity !== "number"
155
+ ) {
156
+ return false;
157
+ }
158
+
159
+ if (session.name !== undefined && typeof session.name !== "string") {
160
+ return false;
161
+ }
162
+
163
+ if (session.status !== undefined && typeof session.status !== "string") {
164
+ return false;
165
+ }
166
+
167
+ if (session.peerUid !== undefined && typeof session.peerUid !== "number") {
168
+ return false;
169
+ }
170
+
171
+ if (session.trustedLocal !== undefined && typeof session.trustedLocal !== "boolean") return false;
172
+ if (session.origin !== undefined && session.origin !== "local" && session.origin !== "remote") return false;
173
+ if (session.remoteHostId !== undefined && typeof session.remoteHostId !== "string") return false;
174
+ if (session.parentSessionId !== undefined && typeof session.parentSessionId !== "string") return false;
175
+ if (session.rootSessionId !== undefined && typeof session.rootSessionId !== "string") return false;
176
+ if (session.generation !== undefined && (typeof session.generation !== "number" || !Number.isSafeInteger(session.generation))) return false;
177
+ if (session.canDelegate !== undefined && typeof session.canDelegate !== "boolean") return false;
178
+ for (const field of ["depth", "maxDepth", "maxChildren"] as const) {
179
+ if (session[field] !== undefined && (typeof session[field] !== "number" || !Number.isSafeInteger(session[field]))) return false;
180
+ }
181
+ if (session.boss !== undefined) {
182
+ try {
183
+ parseBossParticipantBindingMetadata(session.boss, session.id);
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+
191
+ function isRemoteAccessMetadata(value: unknown): value is import("../types.ts").RemoteAccessMetadata {
192
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
193
+ const access = value as Record<string, unknown>;
194
+ return access.origin === "remote"
195
+ && typeof access.remoteHostId === "string"
196
+ && typeof access.parentSessionId === "string"
197
+ && typeof access.rootSessionId === "string"
198
+ && typeof access.generation === "number"
199
+ && Number.isSafeInteger(access.generation)
200
+ && access.generation > 0
201
+ && typeof access.canDelegate === "boolean"
202
+ && typeof access.depth === "number"
203
+ && Number.isSafeInteger(access.depth)
204
+ && typeof access.maxDepth === "number"
205
+ && Number.isSafeInteger(access.maxDepth)
206
+ && typeof access.maxChildren === "number"
207
+ && Number.isSafeInteger(access.maxChildren)
208
+ && (access.sessionCredential === undefined || typeof access.sessionCredential === "string");
209
+ }
210
+
211
+ export interface IntercomClientOptions {
212
+ /** Exact private registration scope captured for this client lifecycle. */
213
+ scopeId?: string;
214
+ /** Environment used only when scopeId is not explicitly supplied. */
215
+ env?: NodeJS.ProcessEnv;
216
+ }
217
+
218
+ export class IntercomClient extends EventEmitter {
219
+ private socket: net.Socket | null = null;
220
+ private readonly scopeId: string | undefined;
221
+ private _sessionId: string | null = null;
222
+ private pendingSends = new Map<string, {
223
+ accepted: boolean;
224
+ deliveryId?: string;
225
+ resolve: (r: SendResult) => void;
226
+ reject: (e: Error) => void;
227
+ }>();
228
+ private pendingLists = new Map<string, { resolve: (sessions: SessionInfo[]) => void; reject: (e: Error) => void }>();
229
+ private pendingAskControls = new Map<string, { resolve: (applied: boolean) => void; timeout: NodeJS.Timeout }>();
230
+ private pendingBossControls = new Map<string, {
231
+ messageId: string;
232
+ idempotencyKey: string;
233
+ deliveryId?: string;
234
+ resolve: (result: BossControlSendResult) => void;
235
+ reject: (error: Error) => void;
236
+ timeout: NodeJS.Timeout;
237
+ }>();
238
+ private outbox: PersistentOutboundOutbox | null = null;
239
+ private bossControlOutbox: PersistentBossControlOutbox | null = null;
240
+ private remoteAccessCredential: LoadedRemoteAccessCredential | undefined;
241
+ private requestedBossRegistration: BossParticipantRegistrationMetadata | undefined;
242
+ private _bossBinding: BossParticipantBindingMetadata | undefined;
243
+ private disconnecting = false;
244
+ private disconnectError: Error | null = null;
245
+
246
+ constructor(options: IntercomClientOptions = {}) {
247
+ super();
248
+ this.scopeId = options.scopeId === undefined
249
+ ? intercomScopeIdFromEnv(options.env ?? process.env)
250
+ : parseIntercomScopeId(options.scopeId, "scopeId");
251
+ }
252
+
253
+ private failPending(error: Error): void {
254
+ for (const pending of this.pendingSends.values()) {
255
+ pending.reject(error);
256
+ }
257
+ this.pendingSends.clear();
258
+ for (const pending of this.pendingLists.values()) {
259
+ pending.reject(error);
260
+ }
261
+ this.pendingLists.clear();
262
+ for (const pending of this.pendingAskControls.values()) {
263
+ clearTimeout(pending.timeout);
264
+ pending.resolve(false);
265
+ }
266
+ this.pendingAskControls.clear();
267
+ for (const pending of this.pendingBossControls.values()) {
268
+ clearTimeout(pending.timeout);
269
+ pending.reject(error);
270
+ }
271
+ this.pendingBossControls.clear();
272
+ }
273
+
274
+ get sessionId(): string | null {
275
+ return this._sessionId;
276
+ }
277
+
278
+ get outboxSize(): number {
279
+ return this.outbox?.list().length ?? 0;
280
+ }
281
+
282
+ get bossBinding(): BossParticipantBindingMetadata | undefined {
283
+ return this._bossBinding;
284
+ }
285
+
286
+ get bossControlOutboxSize(): number {
287
+ return this.bossControlOutbox?.list().length ?? 0;
288
+ }
289
+
290
+ isConnected(): boolean {
291
+ const socket = this.socket;
292
+ return Boolean(socket && this._sessionId && !this.disconnecting && !socket.destroyed && !socket.writableEnded && socket.writable);
293
+ }
294
+
295
+ private requireActiveSocket(): net.Socket {
296
+ if (this.disconnecting) {
297
+ throw new Error("Client disconnecting");
298
+ }
299
+
300
+ const socket = this.socket;
301
+ if (!socket || !this._sessionId) {
302
+ throw new Error("Not connected");
303
+ }
304
+
305
+ if (socket.destroyed || socket.writableEnded || !socket.writable) {
306
+ throw new Error("Client disconnected");
307
+ }
308
+
309
+ return socket;
310
+ }
311
+
312
+ connect(session: SessionRegistration, sessionId?: string): Promise<void> {
313
+ if (this.socket) {
314
+ return Promise.reject(new Error("Already connected"));
315
+ }
316
+
317
+ try {
318
+ const canonicalSession = parseExactRegistrationFrame({
319
+ type: "register",
320
+ ...(typeof session === "object" && session !== null && !nodeUtilTypes.isProxy(session)
321
+ && Object.getOwnPropertyDescriptor(session, "boss") !== undefined
322
+ ? { registrationKind: "boss" as const }
323
+ : {}),
324
+ protocol: INTERCOM_PROTOCOL_NAME,
325
+ version: INTERCOM_PROTOCOL_VERSION,
326
+ session,
327
+ }).session;
328
+ this.requestedBossRegistration = session.boss === undefined
329
+ ? undefined
330
+ : parseBossParticipantRegistrationMetadata(session.boss);
331
+ if (canonicalSession !== session) throw new Error("Registration session identity changed during validation");
332
+ } catch (error) {
333
+ return Promise.reject(toError(error));
334
+ }
335
+
336
+ return new Promise((resolve, reject) => {
337
+ let socket: net.Socket;
338
+ let target: BrokerConnectTarget;
339
+ try {
340
+ target = getBrokerConnectTarget();
341
+ this.remoteAccessCredential = loadRemoteAccessCredential();
342
+ socket = connectToBrokerTarget(target);
343
+ } catch (error) {
344
+ reject(toError(error));
345
+ return;
346
+ }
347
+ this.socket = socket;
348
+ this.disconnectError = null;
349
+ let settled = false;
350
+ const timeout = setTimeout(() => {
351
+ if (!this._sessionId) {
352
+ cleanupConnectionAttempt();
353
+ cleanupSocketListeners();
354
+ if (this.socket === socket) {
355
+ this.socket = null;
356
+ }
357
+ socket.destroy();
358
+ reject(new Error("Connection timeout"));
359
+ }
360
+ }, 10000);
361
+
362
+ let connectionEstablished = false;
363
+
364
+ const onRegistered = () => {
365
+ settled = true;
366
+ connectionEstablished = true;
367
+ cleanupConnectionAttempt();
368
+ resolve();
369
+ };
370
+
371
+ const onError = (err: Error) => {
372
+ settled = true;
373
+ cleanupConnectionAttempt();
374
+ cleanupSocketListeners();
375
+ if (this.socket === socket) {
376
+ this.socket = null;
377
+ }
378
+ socket.destroy();
379
+ reject(err);
380
+ };
381
+
382
+ const onClose = () => {
383
+ const wasConnecting = !settled && !this._sessionId;
384
+ const wasDisconnecting = this.disconnecting;
385
+ const disconnectError = this.disconnectError ?? new Error("Client disconnected");
386
+ this.disconnecting = false;
387
+ cleanupConnectionAttempt();
388
+ cleanupSocketListeners();
389
+ this.failPending(disconnectError);
390
+ if (this.socket === socket) {
391
+ this.socket = null;
392
+ }
393
+ this._sessionId = null;
394
+ this._bossBinding = undefined;
395
+ this.requestedBossRegistration = undefined;
396
+ this.disconnectError = null;
397
+ if (connectionEstablished && !wasDisconnecting) {
398
+ this.emit("disconnected", disconnectError);
399
+ }
400
+ if (wasConnecting) {
401
+ reject(new Error("Connection closed before registration"));
402
+ }
403
+ };
404
+
405
+ const onSocketError = (err: Error) => {
406
+ if (connectionEstablished) {
407
+ this.disconnectError = err;
408
+ this.emit("error", err);
409
+ }
410
+ };
411
+
412
+ const onReaderError = (error: Error) => {
413
+ const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
414
+ if (!connectionEstablished) {
415
+ onError(protocolError);
416
+ return;
417
+ }
418
+ this.disconnectError = protocolError;
419
+ this.emit("error", protocolError);
420
+ socket.destroy();
421
+ };
422
+
423
+ const reader = createMessageReader((msg) => {
424
+ this.handleBrokerMessage(msg);
425
+ }, onReaderError);
426
+
427
+ const cleanupConnectionAttempt = () => {
428
+ this.off("_registered", onRegistered);
429
+ socket.off("error", onError);
430
+ clearTimeout(timeout);
431
+ };
432
+
433
+ const cleanupSocketListeners = () => {
434
+ socket.off("data", reader);
435
+ socket.off("error", onSocketError);
436
+ socket.off("close", onClose);
437
+ };
438
+
439
+ socket.on("data", reader);
440
+ socket.on("error", onError);
441
+ socket.on("close", onClose);
442
+
443
+ socket.on("error", onSocketError);
444
+ this.once("_registered", onRegistered);
445
+
446
+ try {
447
+ writeMessage(socket, {
448
+ type: "register",
449
+ ...(session.boss === undefined ? {} : { registrationKind: "boss" as const }),
450
+ protocol: INTERCOM_PROTOCOL_NAME,
451
+ version: INTERCOM_PROTOCOL_VERSION,
452
+ session,
453
+ ...(!this.remoteAccessCredential && sessionId ? { sessionId } : {}),
454
+ ...(this.remoteAccessCredential ? { access: this.remoteAccessCredential.access } : {}),
455
+ ...(this.scopeId ? { scopeId: this.scopeId } : {}),
456
+ ...(typeof target === "string" ? {} : { stateId: target.stateId }),
457
+ });
458
+ } catch (error) {
459
+ cleanupConnectionAttempt();
460
+ cleanupSocketListeners();
461
+ if (this.socket === socket) {
462
+ this.socket = null;
463
+ }
464
+ socket.destroy();
465
+ reject(toError(error));
466
+ }
467
+ });
468
+ }
469
+
470
+ private handleBrokerMessage(msg: unknown): void {
471
+ if (typeof msg !== "object" || msg === null || nodeUtilTypes.isProxy(msg)) {
472
+ throw new Error("Invalid broker message");
473
+ }
474
+ const typeDescriptor = Object.getOwnPropertyDescriptor(msg, "type");
475
+ if (
476
+ typeDescriptor === undefined
477
+ || !typeDescriptor.enumerable
478
+ || !Object.hasOwn(typeDescriptor, "value")
479
+ || typeof typeDescriptor.value !== "string"
480
+ ) throw new Error("Invalid broker message");
481
+
482
+ const brokerMessage = msg as { type: string } & Record<string, unknown>;
483
+
484
+ if (this._sessionId === null && brokerMessage.type !== "registered" && brokerMessage.type !== "error") {
485
+ throw new Error(`Received ${brokerMessage.type} before registered`);
486
+ }
487
+
488
+ switch (brokerMessage.type) {
489
+ case "registered": {
490
+ parseExactRegisteredFrame(
491
+ brokerMessage,
492
+ this.requestedBossRegistration === undefined
493
+ ? this.remoteAccessCredential === undefined ? "ordinary-local" : "ordinary-remote"
494
+ : "boss",
495
+ );
496
+ if (
497
+ typeof brokerMessage.sessionId !== "string"
498
+ || brokerMessage.protocol !== INTERCOM_PROTOCOL_NAME
499
+ || brokerMessage.version !== INTERCOM_PROTOCOL_VERSION
500
+ ) {
501
+ throw new Error("Invalid registered message");
502
+ }
503
+
504
+ if (this._sessionId !== null) {
505
+ throw new Error("Received duplicate registered message");
506
+ }
507
+
508
+ if (this.remoteAccessCredential) {
509
+ const contract = brokerMessage.remoteAccess;
510
+ const contractFields = typeof contract === "object" && contract !== null
511
+ ? contract as Record<string, unknown>
512
+ : undefined;
513
+ if (
514
+ !contractFields
515
+ || contractFields.feature !== "remote-access-v1"
516
+ || contractFields.policySemanticsVersion !== POLICY_SEMANTICS_VERSION
517
+ || contractFields.policySemanticsHash !== POLICY_SEMANTICS_HASH
518
+ ) {
519
+ throw new Error("Remote Intercom policy contract is absent or incompatible");
520
+ }
521
+ if (!isRemoteAccessMetadata(brokerMessage.access)) {
522
+ throw new Error("Remote Intercom registration omitted broker-owned provenance");
523
+ }
524
+ if (this.remoteAccessCredential.enrollment) {
525
+ writeRemoteSessionCredential(this.remoteAccessCredential.path, brokerMessage.sessionId, brokerMessage.access);
526
+ } else {
527
+ const reconnect = this.remoteAccessCredential.access;
528
+ if (!("sessionId" in reconnect) || reconnect.sessionId !== brokerMessage.sessionId || reconnect.generation !== brokerMessage.access.generation) {
529
+ throw new Error("Remote Intercom reconnect identity or generation changed unexpectedly");
530
+ }
531
+ }
532
+ }
533
+
534
+
535
+ if (this.requestedBossRegistration !== undefined) {
536
+ if (brokerMessage.remoteAccess !== undefined || brokerMessage.access !== undefined) {
537
+ throw new Error("Boss registration returned folded remote-access metadata");
538
+ }
539
+ const advertisement = parseBrokerCapabilityAdvertisement(brokerMessage.capabilities);
540
+ if (!advertisement.features.some((feature) => feature.feature === BOSS_RUN_FEATURE)) {
541
+ throw new Error("Broker did not echo the required boss-run-v1 feature contract");
542
+ }
543
+ const binding = parseBossParticipantBindingMetadata(brokerMessage.boss, brokerMessage.sessionId);
544
+ const credential = this.requestedBossRegistration.credential;
545
+ if (
546
+ binding.featureContract.feature !== this.requestedBossRegistration.featureContract.feature
547
+ || binding.binding.bossRunId !== credential.bossRunId
548
+ || binding.binding.participantId !== credential.participantId
549
+ || binding.binding.role !== credential.role
550
+ || binding.binding.communicationProfile !== credential.communicationProfile
551
+ || binding.binding.bindingEpoch !== credential.bindingEpoch
552
+ ) {
553
+ throw new Error("Broker returned a Boss binding that does not match the authenticated registration request");
554
+ }
555
+ this._bossBinding = binding;
556
+ } else if (brokerMessage.boss !== undefined) {
557
+ throw new Error("Broker attached unsolicited Boss binding metadata to an ordinary registration");
558
+ }
559
+
560
+ this._sessionId = brokerMessage.sessionId;
561
+ this.outbox = new PersistentOutboundOutbox(brokerMessage.sessionId);
562
+ this.bossControlOutbox = this._bossBinding === undefined
563
+ ? null
564
+ : new PersistentBossControlOutbox(brokerMessage.sessionId);
565
+ this.replayOutbox();
566
+ this.replayBossControlOutbox();
567
+ this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
568
+ break;
569
+ }
570
+
571
+ case "sessions": {
572
+ const { requestId, sessions } = brokerMessage;
573
+ if (typeof requestId !== "string" || !Array.isArray(sessions) || !sessions.every(isSessionInfo)) {
574
+ throw new Error("Invalid sessions message");
575
+ }
576
+
577
+ const pending = this.pendingLists.get(requestId);
578
+ if (!pending) {
579
+ // Late list responses can still arrive after the caller has already timed out.
580
+ return;
581
+ }
582
+
583
+ this.pendingLists.delete(requestId);
584
+ pending.resolve(sessions);
585
+ break;
586
+ }
587
+
588
+ case "message": {
589
+ const { deliveryId, from, message } = brokerMessage;
590
+ if (typeof deliveryId !== "string" || !isSessionInfo(from) || !isMessage(message)) {
591
+ throw new Error("Invalid message event");
592
+ }
593
+
594
+ this.emit("message", from, message, deliveryId);
595
+ break;
596
+ }
597
+
598
+ case "boss_control": {
599
+ const { deliveryId, from } = brokerMessage;
600
+ if (typeof deliveryId !== "string" || !isSessionInfo(from)) {
601
+ throw new Error("Invalid boss_control event");
602
+ }
603
+ // Keep every inbound envelope behind the adapter's recursive,
604
+ // proxy-zero-trap preflight before Core parses or reflects on it.
605
+ const envelope = bossControlKind(brokerMessage.envelope).envelope;
606
+ const source = from.boss === undefined
607
+ ? undefined
608
+ : parseBossParticipantBindingMetadata(from.boss, from.id).binding;
609
+ if (
610
+ source === undefined
611
+ || source.state !== "active"
612
+ || source.bossRunId !== envelope.bossRunId
613
+ || source.participantId !== envelope.participantId
614
+ || source.bindingEpoch !== envelope.bindingEpoch
615
+ ) throw new Error("Boss control event sender does not match its broker-owned binding");
616
+ this.emit("boss_control", from, envelope, deliveryId);
617
+ break;
618
+ }
619
+
620
+ case "boss_control_result": {
621
+ const result = parseBossControlResult(brokerMessage);
622
+ const { requestId, messageId, idempotencyKey, deliveryId } = result;
623
+ const stored = this.bossControlOutbox?.find(idempotencyKey);
624
+ if (!stored || stored.envelope.messageId !== requestId) throw new Error("Boss control result does not match the durable outbox binding");
625
+ const pending = this.pendingBossControls.get(requestId);
626
+ if (pending && (pending.messageId !== messageId || pending.idempotencyKey !== idempotencyKey)) {
627
+ throw new Error("Boss control result correlation does not match the pending request");
628
+ }
629
+ this.bossControlOutbox!.removeCorrelated(idempotencyKey, messageId, deliveryId);
630
+ if (pending) {
631
+ clearTimeout(pending.timeout);
632
+ this.pendingBossControls.delete(requestId);
633
+ pending.resolve(result);
634
+ }
635
+ break;
636
+ }
637
+
638
+ case "boss_control_ack": {
639
+ const { requestId, messageId, idempotencyKey, deliveryId } = parseBossControlAck(brokerMessage);
640
+ const pending = this.pendingBossControls.get(requestId);
641
+ if (pending && (pending.messageId !== messageId || pending.idempotencyKey !== idempotencyKey)) {
642
+ throw new Error("Boss control acknowledgement correlation does not match the pending request");
643
+ }
644
+ const transition = this.bossControlOutbox?.markAccepted(idempotencyKey, messageId, deliveryId);
645
+ if (transition === undefined) throw new Error("Boss control acknowledgement has no durable outbox");
646
+ // A terminal replay is deliberately ACK-first. The exact ACK is
647
+ // idempotent when this durable accepted binding survived reconnect;
648
+ // markAccepted still rejects any changed correlation or deliveryId.
649
+ if (pending?.deliveryId !== undefined && pending.deliveryId !== deliveryId) {
650
+ throw new Error("Boss control acknowledgement changed the pending deliveryId");
651
+ }
652
+ if (pending) pending.deliveryId = deliveryId;
653
+ break;
654
+ }
655
+
656
+ case "delivery_accepted": {
657
+ const { deliveryId, messageId } = brokerMessage;
658
+ if (typeof deliveryId !== "string" || typeof messageId !== "string") {
659
+ throw new Error("Invalid delivery_accepted message");
660
+ }
661
+
662
+ const pending = this.pendingSends.get(messageId);
663
+ if (!pending) {
664
+ return;
665
+ }
666
+ pending.accepted = true;
667
+ pending.deliveryId = deliveryId;
668
+ this.emit("delivery_accepted", messageId, deliveryId);
669
+ break;
670
+ }
671
+
672
+ case "delivered": {
673
+ const { deliveryId, messageId } = brokerMessage;
674
+ if (typeof deliveryId !== "string" || typeof messageId !== "string") {
675
+ throw new Error("Invalid delivered message");
676
+ }
677
+
678
+ this.outbox?.remove(messageId);
679
+ const pending = this.pendingSends.get(messageId);
680
+ if (!pending) {
681
+ this.emit("outbox_delivered", messageId, deliveryId);
682
+ return;
683
+ }
684
+
685
+ this.pendingSends.delete(messageId);
686
+ pending.resolve({ id: messageId, accepted: true, delivered: true, deliveryId });
687
+ break;
688
+ }
689
+
690
+ case "delivery_failed": {
691
+ const { accepted, code, messageId, reason } = brokerMessage;
692
+ if (
693
+ typeof accepted !== "boolean"
694
+ || typeof code !== "string"
695
+ || typeof messageId !== "string"
696
+ || typeof reason !== "string"
697
+ ) {
698
+ throw new Error("Invalid delivery_failed message");
699
+ }
700
+
701
+ this.outbox?.remove(messageId);
702
+ const pending = this.pendingSends.get(messageId);
703
+ if (!pending) {
704
+ this.emit("outbox_failed", messageId, code, reason);
705
+ return;
706
+ }
707
+
708
+ this.pendingSends.delete(messageId);
709
+ pending.resolve({
710
+ id: messageId,
711
+ accepted,
712
+ delivered: false,
713
+ code: code as DeliveryFailureCode,
714
+ reason,
715
+ ...(pending.deliveryId ? { deliveryId: pending.deliveryId } : {}),
716
+ });
717
+ break;
718
+ }
719
+
720
+ case "ask_deferred": {
721
+ const { fromSessionId, messageId } = brokerMessage;
722
+ if (typeof fromSessionId !== "string" || typeof messageId !== "string") {
723
+ throw new Error("Invalid ask_deferred message");
724
+ }
725
+ this.emit("ask_deferred", messageId, fromSessionId);
726
+ break;
727
+ }
728
+
729
+ case "ask_cancelled": {
730
+ const { fromSessionId, messageId, reason } = brokerMessage;
731
+ if (typeof fromSessionId !== "string" || typeof messageId !== "string" || typeof reason !== "string") {
732
+ throw new Error("Invalid ask_cancelled message");
733
+ }
734
+ this.emit("ask_cancelled", messageId, fromSessionId, reason as AskCancellationReason);
735
+ break;
736
+ }
737
+
738
+ case "ask_control_result": {
739
+ const { action, applied, messageId, requestId } = brokerMessage;
740
+ if (
741
+ (action !== "defer" && action !== "cancel")
742
+ || typeof applied !== "boolean"
743
+ || typeof messageId !== "string"
744
+ || typeof requestId !== "string"
745
+ ) {
746
+ throw new Error("Invalid ask_control_result message");
747
+ }
748
+ const pending = this.pendingAskControls.get(requestId);
749
+ if (!pending) return;
750
+ clearTimeout(pending.timeout);
751
+ this.pendingAskControls.delete(requestId);
752
+ pending.resolve(applied);
753
+ break;
754
+ }
755
+
756
+ case "session_joined": {
757
+ if (!isSessionInfo(brokerMessage.session)) {
758
+ throw new Error("Invalid session_joined message");
759
+ }
760
+
761
+ this.emit("session_joined", brokerMessage.session);
762
+ break;
763
+ }
764
+
765
+ case "session_left": {
766
+ if (typeof brokerMessage.sessionId !== "string") {
767
+ throw new Error("Invalid session_left message");
768
+ }
769
+
770
+ this.emit("session_left", brokerMessage.sessionId);
771
+ break;
772
+ }
773
+
774
+ case "presence_update": {
775
+ if (!isSessionInfo(brokerMessage.session)) {
776
+ throw new Error("Invalid presence_update message");
777
+ }
778
+
779
+ this.emit("presence_update", brokerMessage.session);
780
+ break;
781
+ }
782
+
783
+ case "error": {
784
+ if (typeof brokerMessage.code !== "string" || typeof brokerMessage.error !== "string") {
785
+ throw new Error("Invalid error message");
786
+ }
787
+
788
+ if (this._sessionId === null) {
789
+ const error = new Error(brokerMessage.error) as Error & { code?: string };
790
+ error.code = brokerMessage.code;
791
+ throw error;
792
+ }
793
+ const error = new Error(brokerMessage.error) as Error & { code?: string };
794
+ error.code = brokerMessage.code;
795
+ this.emit("error", error);
796
+ break;
797
+ }
798
+
799
+ default:
800
+ throw new Error(`Unknown broker message type: ${brokerMessage.type}`);
801
+ }
802
+ }
803
+
804
+ async disconnect(preserveAsks = false): Promise<void> {
805
+ const socket = this.socket;
806
+ if (!socket) {
807
+ return;
808
+ }
809
+
810
+ this.disconnecting = true;
811
+ this.disconnectError = null;
812
+ this.failPending(new Error("Client disconnected"));
813
+ if (!preserveAsks) this.outbox?.clear();
814
+
815
+ await new Promise<void>((resolve) => {
816
+ let settled = false;
817
+ const finish = () => {
818
+ if (settled) {
819
+ return;
820
+ }
821
+ settled = true;
822
+ clearTimeout(timeout);
823
+ socket.off("close", onClose);
824
+ socket.off("error", onError);
825
+ resolve();
826
+ };
827
+ const onClose = () => finish();
828
+ const onError = () => {
829
+ socket.destroy();
830
+ };
831
+ const timeout = setTimeout(() => {
832
+ socket.destroy();
833
+ }, 2000);
834
+
835
+ socket.once("close", onClose);
836
+ socket.once("error", onError);
837
+
838
+ try {
839
+ writeMessage(socket, { type: "unregister", ...(preserveAsks ? { preserveAsks: true } : {}) });
840
+ socket.end();
841
+ } catch {
842
+ // Disconnect should still finish even if the unregister write fails.
843
+ socket.destroy();
844
+ }
845
+ });
846
+ }
847
+
848
+ listSessions(): Promise<SessionInfo[]> {
849
+ let socket: net.Socket;
850
+ try {
851
+ socket = this.requireActiveSocket();
852
+ } catch (error) {
853
+ return Promise.reject(toError(error));
854
+ }
855
+
856
+ return new Promise((resolve, reject) => {
857
+ const requestId = randomUUID();
858
+ const wrappedResolve = (sessions: SessionInfo[]) => {
859
+ clearTimeout(timeout);
860
+ resolve(sessions);
861
+ };
862
+ const wrappedReject = (error: Error) => {
863
+ clearTimeout(timeout);
864
+ reject(error);
865
+ };
866
+ const timeout = setTimeout(() => {
867
+ if (this.pendingLists.has(requestId)) {
868
+ this.pendingLists.delete(requestId);
869
+ wrappedReject(new Error("List sessions timeout"));
870
+ }
871
+ }, 5000);
872
+ this.pendingLists.set(requestId, { resolve: wrappedResolve, reject: wrappedReject });
873
+ try {
874
+ writeMessage(socket, { type: "list", requestId });
875
+ } catch (error) {
876
+ clearTimeout(timeout);
877
+ this.pendingLists.delete(requestId);
878
+ reject(toError(error));
879
+ }
880
+ });
881
+ }
882
+
883
+ send(to: string, options: SendOptions): Promise<SendResult> {
884
+ let socket: net.Socket;
885
+ try {
886
+ socket = this.requireActiveSocket();
887
+ } catch (error) {
888
+ return Promise.reject(toError(error));
889
+ }
890
+
891
+ const messageId = options.messageId ?? randomUUID();
892
+ if (this.pendingSends.has(messageId)) {
893
+ return Promise.resolve({
894
+ id: messageId,
895
+ accepted: false,
896
+ delivered: false,
897
+ code: "DUPLICATE_MESSAGE_ID",
898
+ reason: `Message ID ${messageId} is already pending`,
899
+ });
900
+ }
901
+ const message: Message = {
902
+ id: messageId,
903
+ timestamp: Date.now(),
904
+ replyTo: options.replyTo,
905
+ expectsReply: options.expectsReply,
906
+ content: {
907
+ text: options.text,
908
+ attachments: options.attachments,
909
+ },
910
+ };
911
+
912
+ try {
913
+ this.outbox?.enqueue(to, message);
914
+ } catch (error) {
915
+ return Promise.reject(toError(error));
916
+ }
917
+
918
+ return new Promise((resolve, reject) => {
919
+ const wrappedResolve = (result: SendResult) => {
920
+ clearTimeout(timeout);
921
+ resolve(result);
922
+ };
923
+ const wrappedReject = (error: Error) => {
924
+ clearTimeout(timeout);
925
+ reject(error);
926
+ };
927
+ const timeout = setTimeout(() => {
928
+ if (this.pendingSends.has(messageId)) {
929
+ this.pendingSends.delete(messageId);
930
+ wrappedReject(new Error("Send timeout"));
931
+ }
932
+ }, 10000);
933
+ this.pendingSends.set(messageId, {
934
+ accepted: false,
935
+ resolve: wrappedResolve,
936
+ reject: wrappedReject,
937
+ });
938
+
939
+ try {
940
+ writeMessage(socket, { type: "send", to, message });
941
+ } catch (error) {
942
+ clearTimeout(timeout);
943
+ this.pendingSends.delete(messageId);
944
+ reject(toError(error));
945
+ }
946
+ });
947
+ }
948
+
949
+ sendBossControl(to: string, envelopeValue: unknown): Promise<BossControlSendResult> {
950
+ let socket: net.Socket;
951
+ try {
952
+ socket = this.requireActiveSocket();
953
+ } catch (error) {
954
+ return Promise.reject(toError(error));
955
+ }
956
+ let envelope: BossControlEnvelope;
957
+ try {
958
+ // Caller objects receive the same adapter-owned zero-trap boundary as
959
+ // broker-delivered envelopes; Core never sees a Proxy at any depth.
960
+ envelope = bossControlKind(envelopeValue).envelope;
961
+ const binding = this._bossBinding?.binding;
962
+ if (
963
+ binding === undefined
964
+ || binding.state !== "active"
965
+ || envelope.bossRunId !== binding.bossRunId
966
+ || envelope.participantId !== binding.participantId
967
+ || envelope.bindingEpoch !== binding.bindingEpoch
968
+ ) throw new Error("Boss control envelope does not match this client's active participant binding");
969
+ } catch (error) {
970
+ return Promise.reject(toError(error));
971
+ }
972
+ const requestId = envelope.messageId;
973
+ if (this.pendingBossControls.has(requestId)) {
974
+ return Promise.resolve({
975
+ requestId,
976
+ messageId: envelope.messageId,
977
+ idempotencyKey: envelope.idempotencyKey,
978
+ status: "rejected",
979
+ delivered: false,
980
+ code: "INVALID_CONTROL",
981
+ reason: "Boss requestId is already pending",
982
+ });
983
+ }
984
+ try {
985
+ if (!this.bossControlOutbox) throw new Error("Durable Boss control outbox is unavailable");
986
+ this.bossControlOutbox.enqueue(to, envelope);
987
+ } catch (error) {
988
+ return Promise.reject(toError(error));
989
+ }
990
+ return new Promise((resolve, reject) => {
991
+ const timeout = setTimeout(() => {
992
+ if (!this.pendingBossControls.delete(requestId)) return;
993
+ reject(new Error("Boss control delivery timeout"));
994
+ }, 10000);
995
+ timeout.unref?.();
996
+ this.pendingBossControls.set(requestId, {
997
+ messageId: envelope.messageId,
998
+ idempotencyKey: envelope.idempotencyKey,
999
+ resolve,
1000
+ reject,
1001
+ timeout,
1002
+ });
1003
+ try {
1004
+ writeMessage(socket, { type: "boss_control", requestId, to, envelope });
1005
+ } catch (error) {
1006
+ clearTimeout(timeout);
1007
+ this.pendingBossControls.delete(requestId);
1008
+ reject(toError(error));
1009
+ }
1010
+ });
1011
+ }
1012
+
1013
+ acknowledgeBossControl(deliveryId: string, messageId: string, idempotencyKey: string): boolean {
1014
+ return this.writeControlMessage({ type: "boss_control_received", deliveryId, messageId, idempotencyKey });
1015
+ }
1016
+
1017
+ acknowledgeMessage(deliveryId: string): boolean {
1018
+ return this.writeControlMessage({ type: "message_received", deliveryId });
1019
+ }
1020
+
1021
+ rejectMessage(deliveryId: string, reason: string): boolean {
1022
+ return this.writeControlMessage({ type: "message_rejected", deliveryId, code: "CONFLICTING_MESSAGE_ID", reason });
1023
+ }
1024
+
1025
+ deferAsk(messageId: string): Promise<boolean> {
1026
+ return this.sendAskControl("defer", messageId);
1027
+ }
1028
+
1029
+ cancelAsk(messageId: string): Promise<boolean> {
1030
+ return this.sendAskControl("cancel", messageId);
1031
+ }
1032
+
1033
+ private sendAskControl(action: "defer" | "cancel", messageId: string): Promise<boolean> {
1034
+ const requestId = randomUUID();
1035
+ return new Promise((resolve) => {
1036
+ const timeout = setTimeout(() => {
1037
+ this.pendingAskControls.delete(requestId);
1038
+ resolve(false);
1039
+ }, 2000);
1040
+ timeout.unref?.();
1041
+ this.pendingAskControls.set(requestId, { resolve, timeout });
1042
+ if (!this.writeControlMessage({ type: action === "defer" ? "defer_ask" : "cancel_ask", requestId, messageId })) {
1043
+ clearTimeout(timeout);
1044
+ this.pendingAskControls.delete(requestId);
1045
+ resolve(false);
1046
+ }
1047
+ });
1048
+ }
1049
+
1050
+ private writeControlMessage(message: Record<string, unknown>): boolean {
1051
+ if (this.disconnecting) {
1052
+ return false;
1053
+ }
1054
+
1055
+ const socket = this.socket;
1056
+ if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
1057
+ return false;
1058
+ }
1059
+
1060
+ try {
1061
+ writeMessage(socket, message);
1062
+ return true;
1063
+ } catch {
1064
+ // Control messages are best-effort; local cleanup must still proceed.
1065
+ return false;
1066
+ }
1067
+ }
1068
+
1069
+ private replayOutbox(): void {
1070
+ const socket = this.socket;
1071
+ if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) return;
1072
+ for (const entry of this.outbox?.list() ?? []) {
1073
+ if (this.pendingSends.has(entry.message.id)) continue;
1074
+ try {
1075
+ writeMessage(socket, { type: "send", to: entry.to, message: entry.message });
1076
+ } catch {
1077
+ return;
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ private replayBossControlOutbox(): void {
1083
+ const socket = this.socket;
1084
+ if (!socket || socket.destroyed || !this._sessionId || !this.bossControlOutbox) return;
1085
+ for (const entry of this.bossControlOutbox.list()) {
1086
+ try {
1087
+ writeMessage(socket, {
1088
+ type: "boss_control",
1089
+ requestId: entry.envelope.messageId,
1090
+ to: entry.to,
1091
+ envelope: entry.envelope,
1092
+ });
1093
+ } catch {
1094
+ return;
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ updatePresence(updates: { name?: string; status?: string; model?: string }): void {
1100
+ if (this.disconnecting) {
1101
+ return;
1102
+ }
1103
+
1104
+ const socket = this.socket;
1105
+ if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
1106
+ return;
1107
+ }
1108
+
1109
+ writeMessage(socket, { type: "presence", ...updates });
1110
+ }
1111
+ }