@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.
- package/.codex-plugin/plugin.json +39 -0
- package/.mcp.json +10 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +572 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/assets/logo-generated.png +0 -0
- package/assets/logo.svg +22 -0
- package/boss-control-outbox.ts +195 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +464 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +54 -0
- package/broker/boss-adapter.ts +531 -0
- package/broker/boss-control-ledger.ts +306 -0
- package/broker/broker.ts +2327 -0
- package/broker/client.ts +1111 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +453 -0
- package/codex/app-server-client.ts +562 -0
- package/codex/boss-client.ts +34 -0
- package/codex/bridge-config.ts +282 -0
- package/codex/bridge-daemon.ts +913 -0
- package/codex/clipboard.ts +142 -0
- package/codex/coi.ts +802 -0
- package/codex/contact.ts +54 -0
- package/codex/mcp-protocol.ts +248 -0
- package/codex/runtime.ts +488 -0
- package/codex/server.ts +60 -0
- package/codex/team.ts +249 -0
- package/codex/tui-input.ts +174 -0
- package/config.ts +195 -0
- package/dist/bridge-daemon.mjs +3781 -0
- package/dist/broker.mjs +3012 -0
- package/dist/build-info.json +12 -0
- package/dist/codex-server.mjs +2890 -0
- package/dist/coi.mjs +4686 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +89 -0
- package/protocol-v4/contract.ts +27 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build-info.mjs +58 -0
- package/scripts/build.mjs +55 -0
- package/skills/codex-intercom/SKILL.md +104 -0
- package/tsconfig.json +16 -0
- package/types.ts +225 -0
|
@@ -0,0 +1,2890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.11.0-connect.2 target=codex-server sourceSha256=3f81ffd19c83ad2f4db13939ce8dba9eae17d8c4b0c419e405941e0352f9c102\n");
|
|
3
|
+
|
|
4
|
+
// codex/server.ts
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { stdin, stdout } from "node:process";
|
|
7
|
+
|
|
8
|
+
// codex/runtime.ts
|
|
9
|
+
import { randomUUID as randomUUID4, createHash as createHash3 } from "crypto";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { basename } from "path";
|
|
12
|
+
import { cwd as processCwd } from "process";
|
|
13
|
+
|
|
14
|
+
// broker/client.ts
|
|
15
|
+
import { EventEmitter } from "events";
|
|
16
|
+
import net from "net";
|
|
17
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
18
|
+
import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
|
|
19
|
+
import {
|
|
20
|
+
BOSS_RUN_FEATURE as BOSS_RUN_FEATURE2,
|
|
21
|
+
parseBrokerCapabilityAdvertisement as parseBrokerCapabilityAdvertisement2
|
|
22
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
23
|
+
|
|
24
|
+
// broker/framing.ts
|
|
25
|
+
var MAX_FRAME_BYTES = 1024 * 1024;
|
|
26
|
+
function writeMessage(socket, msg) {
|
|
27
|
+
const json = JSON.stringify(msg);
|
|
28
|
+
const payload = Buffer.from(json, "utf-8");
|
|
29
|
+
const header = Buffer.alloc(4);
|
|
30
|
+
header.writeUInt32BE(payload.length, 0);
|
|
31
|
+
socket.write(Buffer.concat([header, payload]));
|
|
32
|
+
}
|
|
33
|
+
function createMessageReader(onMessage, onError, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
34
|
+
let buffer = Buffer.alloc(0);
|
|
35
|
+
function reportMessage(payload) {
|
|
36
|
+
let msg;
|
|
37
|
+
try {
|
|
38
|
+
msg = JSON.parse(payload.toString("utf-8"));
|
|
39
|
+
} catch (error2) {
|
|
40
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
41
|
+
onError(new Error(`Failed to parse intercom message: ${message}`, { cause: error2 }));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
onMessage(msg);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error2) {
|
|
48
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
49
|
+
onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error2 }));
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return (data) => {
|
|
54
|
+
let remaining = data;
|
|
55
|
+
while (remaining.length > 0) {
|
|
56
|
+
if (buffer.length < 4) {
|
|
57
|
+
const headerBytes = Math.min(4 - buffer.length, remaining.length);
|
|
58
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, headerBytes)]);
|
|
59
|
+
remaining = remaining.subarray(headerBytes);
|
|
60
|
+
if (buffer.length < 4) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const length = buffer.readUInt32BE(0);
|
|
65
|
+
if (length > maxFrameBytes) {
|
|
66
|
+
buffer = Buffer.alloc(0);
|
|
67
|
+
onError(new Error(`Intercom frame length ${length} exceeds maximum ${maxFrameBytes} bytes`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const missingPayloadBytes = length - Math.max(0, buffer.length - 4);
|
|
71
|
+
const payloadBytes = Math.min(missingPayloadBytes, remaining.length);
|
|
72
|
+
if (payloadBytes > 0) {
|
|
73
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, payloadBytes)]);
|
|
74
|
+
remaining = remaining.subarray(payloadBytes);
|
|
75
|
+
}
|
|
76
|
+
if (buffer.length < 4 + length) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const payload = buffer.subarray(4, 4 + length);
|
|
80
|
+
buffer = Buffer.alloc(0);
|
|
81
|
+
if (!reportMessage(payload)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// protocol-v4/contract.ts
|
|
89
|
+
import {
|
|
90
|
+
INTERCOM_PROTOCOL_NAME,
|
|
91
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
92
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
93
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
94
|
+
INTERCOM_PROTOCOL_VERSION,
|
|
95
|
+
INTERCOM_SCOPE_ENV,
|
|
96
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
97
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
98
|
+
intercomScopeIdFromEnv,
|
|
99
|
+
parseIntercomScopeId,
|
|
100
|
+
sameIntercomScope
|
|
101
|
+
} from "@ctliz/agent-intercom-core/protocol-v4";
|
|
102
|
+
|
|
103
|
+
// outbound-outbox.ts
|
|
104
|
+
import { createHash } from "crypto";
|
|
105
|
+
import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2 } from "fs";
|
|
106
|
+
import { join as join2 } from "path";
|
|
107
|
+
|
|
108
|
+
// broker/paths.ts
|
|
109
|
+
import { chmodSync, mkdirSync, readFileSync } from "fs";
|
|
110
|
+
import { isAbsolute, join, resolve } from "path";
|
|
111
|
+
import { homedir } from "os";
|
|
112
|
+
var INTERCOM_DIR_MODE = 448;
|
|
113
|
+
var INTERCOM_RUNTIME_FILE_MODE = 384;
|
|
114
|
+
var INTERCOM_TCP_HOST = "127.0.0.1";
|
|
115
|
+
var INTERCOM_PROTOCOL_NAME2 = "pi-intercom";
|
|
116
|
+
var INTERCOM_PROTOCOL_VERSION2 = 4;
|
|
117
|
+
function sanitizePipeSegment(value) {
|
|
118
|
+
return value.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "default";
|
|
119
|
+
}
|
|
120
|
+
function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
|
|
121
|
+
const configured = env.PI_CODING_AGENT_DIR?.trim();
|
|
122
|
+
if (!configured) {
|
|
123
|
+
return join(homeDir, ".pi/agent");
|
|
124
|
+
}
|
|
125
|
+
return isAbsolute(configured) ? configured : resolve(cwd, configured);
|
|
126
|
+
}
|
|
127
|
+
function getIntercomDirPath(agentDir = getAgentDirPath()) {
|
|
128
|
+
return join(agentDir, "intercom");
|
|
129
|
+
}
|
|
130
|
+
function shouldUseWindowsTcpTransport(platform = process.platform, env = process.env) {
|
|
131
|
+
if (platform !== "win32") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const transport = env.PI_INTERCOM_TRANSPORT?.trim().toLowerCase();
|
|
135
|
+
if (transport === "tcp") {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
const legacyOptIn = env.PI_INTERCOM_TCP?.trim().toLowerCase();
|
|
139
|
+
return legacyOptIn === "1" || legacyOptIn === "true";
|
|
140
|
+
}
|
|
141
|
+
function getBrokerPortFilePath(intercomDir = getIntercomDirPath()) {
|
|
142
|
+
return join(intercomDir, "broker.port.json");
|
|
143
|
+
}
|
|
144
|
+
function getBrokerSocketPath(platform = process.platform, agentDir = getAgentDirPath()) {
|
|
145
|
+
if (platform === "win32") {
|
|
146
|
+
return `\\\\.\\pipe\\pi-intercom-${sanitizePipeSegment(agentDir)}`;
|
|
147
|
+
}
|
|
148
|
+
return join(getIntercomDirPath(agentDir), "broker.sock");
|
|
149
|
+
}
|
|
150
|
+
function getBrokerConnectTarget(platform = process.platform, env = process.env, intercomDir = getIntercomDirPath(getAgentDirPath(env))) {
|
|
151
|
+
if (shouldUseWindowsTcpTransport(platform, env)) {
|
|
152
|
+
const endpointFile = getBrokerPortFilePath(intercomDir);
|
|
153
|
+
const raw = readFileSync(endpointFile, "utf-8");
|
|
154
|
+
const parsed = JSON.parse(raw);
|
|
155
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
156
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}: expected a JSON object`);
|
|
157
|
+
}
|
|
158
|
+
const endpoint = parsed;
|
|
159
|
+
if (endpoint.transport !== "tcp" || endpoint.host !== INTERCOM_TCP_HOST || typeof endpoint.port !== "number" || !Number.isSafeInteger(endpoint.port) || endpoint.port <= 0 || endpoint.port > 65535 || typeof endpoint.stateId !== "string" || endpoint.stateId.length === 0) {
|
|
160
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}`);
|
|
161
|
+
}
|
|
162
|
+
return { transport: "tcp", host: endpoint.host, port: endpoint.port, stateId: endpoint.stateId };
|
|
163
|
+
}
|
|
164
|
+
return getBrokerSocketPath(platform, getAgentDirPath(env));
|
|
165
|
+
}
|
|
166
|
+
function ensureIntercomRuntimeDir(intercomDir = getIntercomDirPath(), platform = process.platform) {
|
|
167
|
+
mkdirSync(intercomDir, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
168
|
+
if (platform !== "win32") {
|
|
169
|
+
chmodSync(intercomDir, INTERCOM_DIR_MODE);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function restrictIntercomRuntimeFile(filePath, platform = process.platform) {
|
|
173
|
+
if (platform !== "win32") {
|
|
174
|
+
chmodSync(filePath, INTERCOM_RUNTIME_FILE_MODE);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// durable-json.ts
|
|
179
|
+
import { randomUUID } from "crypto";
|
|
180
|
+
import { closeSync, fsyncSync, openSync, renameSync, writeFileSync } from "fs";
|
|
181
|
+
import { dirname } from "path";
|
|
182
|
+
function writeDurableJson(filePath, value) {
|
|
183
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
184
|
+
writeFileSync(temporaryPath, JSON.stringify(value), { encoding: "utf-8", mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
185
|
+
const fileDescriptor = openSync(temporaryPath, "r");
|
|
186
|
+
try {
|
|
187
|
+
fsyncSync(fileDescriptor);
|
|
188
|
+
} finally {
|
|
189
|
+
closeSync(fileDescriptor);
|
|
190
|
+
}
|
|
191
|
+
renameSync(temporaryPath, filePath);
|
|
192
|
+
restrictIntercomRuntimeFile(filePath);
|
|
193
|
+
if (process.platform !== "win32") {
|
|
194
|
+
const directoryDescriptor = openSync(dirname(filePath), "r");
|
|
195
|
+
try {
|
|
196
|
+
fsyncSync(directoryDescriptor);
|
|
197
|
+
} finally {
|
|
198
|
+
closeSync(directoryDescriptor);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// outbound-outbox.ts
|
|
204
|
+
var OUTBOX_STATE_VERSION = 1;
|
|
205
|
+
var MAX_OUTBOX_MESSAGES = 256;
|
|
206
|
+
function fingerprint(entry) {
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
to: entry.to,
|
|
209
|
+
replyTo: entry.message.replyTo,
|
|
210
|
+
expectsReply: entry.message.expectsReply,
|
|
211
|
+
content: entry.message.content
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function isStoredOutboundMessage(value) {
|
|
215
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
216
|
+
const entry = value;
|
|
217
|
+
if (typeof entry.to !== "string" || typeof entry.queuedAt !== "number") return false;
|
|
218
|
+
if (typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) return false;
|
|
219
|
+
const message = entry.message;
|
|
220
|
+
return typeof message.id === "string" && typeof message.timestamp === "number" && typeof message.content === "object" && message.content !== null && typeof message.content.text === "string";
|
|
221
|
+
}
|
|
222
|
+
function fileName(sessionId) {
|
|
223
|
+
return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
|
|
224
|
+
}
|
|
225
|
+
var PersistentOutboundOutbox = class {
|
|
226
|
+
directory;
|
|
227
|
+
filePath;
|
|
228
|
+
state;
|
|
229
|
+
constructor(sessionId, intercomDir = getIntercomDirPath()) {
|
|
230
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
231
|
+
this.directory = join2(intercomDir, "outbox");
|
|
232
|
+
mkdirSync2(this.directory, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
233
|
+
if (process.platform !== "win32") chmodSync2(this.directory, INTERCOM_DIR_MODE);
|
|
234
|
+
this.filePath = join2(this.directory, fileName(sessionId));
|
|
235
|
+
this.state = this.load();
|
|
236
|
+
}
|
|
237
|
+
list() {
|
|
238
|
+
return this.state.entries.map((entry) => ({ ...entry, message: { ...entry.message, content: { ...entry.message.content } } }));
|
|
239
|
+
}
|
|
240
|
+
enqueue(to, message) {
|
|
241
|
+
const existing = this.state.entries.find((entry) => entry.message.id === message.id);
|
|
242
|
+
if (existing) {
|
|
243
|
+
if (fingerprint(existing) !== fingerprint({ to, message })) {
|
|
244
|
+
throw new Error(`Message ID ${message.id} is already queued with a different payload`);
|
|
245
|
+
}
|
|
246
|
+
return "existing";
|
|
247
|
+
}
|
|
248
|
+
if (this.state.entries.length >= MAX_OUTBOX_MESSAGES) {
|
|
249
|
+
throw new Error(`Durable outbox is full (${MAX_OUTBOX_MESSAGES} messages)`);
|
|
250
|
+
}
|
|
251
|
+
this.state.entries.push({ to, message, queuedAt: Date.now() });
|
|
252
|
+
this.persist();
|
|
253
|
+
return "added";
|
|
254
|
+
}
|
|
255
|
+
remove(messageId) {
|
|
256
|
+
const remaining = this.state.entries.filter((entry) => entry.message.id !== messageId);
|
|
257
|
+
if (remaining.length === this.state.entries.length) return;
|
|
258
|
+
this.state.entries = remaining;
|
|
259
|
+
this.persist();
|
|
260
|
+
}
|
|
261
|
+
clear() {
|
|
262
|
+
if (this.state.entries.length === 0) return;
|
|
263
|
+
this.state.entries = [];
|
|
264
|
+
this.persist();
|
|
265
|
+
}
|
|
266
|
+
load() {
|
|
267
|
+
if (!existsSync(this.filePath)) return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(readFileSync2(this.filePath, "utf-8"));
|
|
270
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
271
|
+
const state = parsed;
|
|
272
|
+
if (state.version !== OUTBOX_STATE_VERSION || !Array.isArray(state.entries) || !state.entries.every(isStoredOutboundMessage)) {
|
|
273
|
+
throw new Error("invalid outbox state");
|
|
274
|
+
}
|
|
275
|
+
return { version: OUTBOX_STATE_VERSION, entries: state.entries };
|
|
276
|
+
} catch {
|
|
277
|
+
const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
|
|
278
|
+
renameSync2(this.filePath, corruptPath);
|
|
279
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
280
|
+
return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
persist() {
|
|
284
|
+
writeDurableJson(this.filePath, this.state);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// boss-control-outbox.ts
|
|
289
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
290
|
+
import { chmodSync as chmodSync3, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3 } from "node:fs";
|
|
291
|
+
import { join as join3 } from "node:path";
|
|
292
|
+
import { canonicalHash } from "@ctliz/agent-intercom-core/canonical";
|
|
293
|
+
import { parseBossControlEnvelope as parseBossControlEnvelope2 } from "@ctliz/agent-intercom-core/boss";
|
|
294
|
+
|
|
295
|
+
// broker/boss-adapter.ts
|
|
296
|
+
import {
|
|
297
|
+
BOSS_CAPABILITY_FEATURE_DIGEST,
|
|
298
|
+
BOSS_CONTROL_ENVELOPE_VERSION,
|
|
299
|
+
BOSS_POLICY_PRINCIPAL_VERSION,
|
|
300
|
+
BOSS_POLICY_SEMANTICS_HASH,
|
|
301
|
+
BOSS_RUN_FEATURE,
|
|
302
|
+
BOSS_RUN_FEATURE_CONTRACT,
|
|
303
|
+
BOSS_RUN_FEATURE_SEMANTICS_HASH,
|
|
304
|
+
BOSS_RUN_FEATURE_VERSION,
|
|
305
|
+
BOSS_RUN_PROTOCOL_FEATURE_CONTRACT_HASH,
|
|
306
|
+
BROKER_FEATURE_ATTESTATION_VERSION,
|
|
307
|
+
INTERCOM_BASE_PROTOCOL_VERSION,
|
|
308
|
+
authorizeFeatureAware,
|
|
309
|
+
brokerFeatureSetHash,
|
|
310
|
+
parseBossControlEnvelope,
|
|
311
|
+
parseBossParticipantBinding,
|
|
312
|
+
parseBossParticipantCredentialEnvelope,
|
|
313
|
+
parseBossRunFeatureContract,
|
|
314
|
+
parseBrokerCapabilityAdvertisement,
|
|
315
|
+
parseParticipantState,
|
|
316
|
+
parseWorkerIdentityV2
|
|
317
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
318
|
+
import {
|
|
319
|
+
ContractValidationError,
|
|
320
|
+
assertExactKeys,
|
|
321
|
+
assertRecord,
|
|
322
|
+
canonicalJson
|
|
323
|
+
} from "@ctliz/agent-intercom-core/canonical";
|
|
324
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
325
|
+
var BOSS_ADVERTISEMENT_PREDICATES = [
|
|
326
|
+
"protectedProvider",
|
|
327
|
+
"brokerIdentity",
|
|
328
|
+
"credentialRegistry",
|
|
329
|
+
"authorityTransitions",
|
|
330
|
+
"participantHealth"
|
|
331
|
+
];
|
|
332
|
+
var DORMANT_BOSS_ADVERTISEMENT_READINESS = Object.freeze({
|
|
333
|
+
protectedProvider: false,
|
|
334
|
+
brokerIdentity: false,
|
|
335
|
+
credentialRegistry: false,
|
|
336
|
+
authorityTransitions: false,
|
|
337
|
+
participantHealth: false
|
|
338
|
+
});
|
|
339
|
+
var ORDINARY_SESSION_REGISTRATION_KEYS = [
|
|
340
|
+
"cwd",
|
|
341
|
+
"model",
|
|
342
|
+
"pid",
|
|
343
|
+
"startedAt",
|
|
344
|
+
"lastActivity"
|
|
345
|
+
];
|
|
346
|
+
var OPTIONAL_SESSION_REGISTRATION_KEYS = ["name", "status", "runtimeInstanceId"];
|
|
347
|
+
function assertBossCanonicalData(value, path = "$", seen = /* @__PURE__ */ new WeakSet()) {
|
|
348
|
+
if (typeof value !== "object" || value === null) return;
|
|
349
|
+
if (nodeUtilTypes.isProxy(value)) {
|
|
350
|
+
throw new ContractValidationError(path, "proxies are not supported");
|
|
351
|
+
}
|
|
352
|
+
if (seen.has(value)) throw new ContractValidationError(path, "cyclic values are not supported");
|
|
353
|
+
seen.add(value);
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
356
|
+
throw new ContractValidationError(path, "must use the exact Array prototype");
|
|
357
|
+
}
|
|
358
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
359
|
+
const expectedKeys = /* @__PURE__ */ new Set(["length"]);
|
|
360
|
+
for (let index = 0; index < value.length; index += 1) expectedKeys.add(String(index));
|
|
361
|
+
if (ownKeys.length !== expectedKeys.size || ownKeys.some((key) => !expectedKeys.has(key))) {
|
|
362
|
+
throw new ContractValidationError(path, "must be a dense array without symbols or extra properties");
|
|
363
|
+
}
|
|
364
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
365
|
+
if (!Object.hasOwn(value, index)) {
|
|
366
|
+
throw new ContractValidationError(`${path}[${index}]`, "sparse array holes are not supported");
|
|
367
|
+
}
|
|
368
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
369
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
370
|
+
throw new ContractValidationError(`${path}[${index}]`, "must be an own enumerable data property");
|
|
371
|
+
}
|
|
372
|
+
assertBossCanonicalData(descriptor.value, `${path}[${index}]`, seen);
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
377
|
+
throw new ContractValidationError(path, "must use the exact Object prototype");
|
|
378
|
+
}
|
|
379
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
380
|
+
if (typeof key !== "string") throw new ContractValidationError(path, "symbol properties are not supported");
|
|
381
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
382
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
383
|
+
throw new ContractValidationError(`${path}.${key}`, "must be an own enumerable data property");
|
|
384
|
+
}
|
|
385
|
+
assertBossCanonicalData(descriptor.value, `${path}.${key}`, seen);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function parseBossAdvertisementReadiness(value) {
|
|
389
|
+
assertBossCanonicalData(value, "$.readiness");
|
|
390
|
+
assertRecord(value);
|
|
391
|
+
assertExactKeys(value, BOSS_ADVERTISEMENT_PREDICATES);
|
|
392
|
+
const parsed = {};
|
|
393
|
+
for (const predicate of BOSS_ADVERTISEMENT_PREDICATES) {
|
|
394
|
+
const enabled = ownDataValue(value, predicate);
|
|
395
|
+
if (typeof enabled !== "boolean") {
|
|
396
|
+
throw new ContractValidationError(`$.readiness.${predicate}`, "must be a boolean");
|
|
397
|
+
}
|
|
398
|
+
parsed[predicate] = enabled;
|
|
399
|
+
}
|
|
400
|
+
return parsed;
|
|
401
|
+
}
|
|
402
|
+
function missingBossAdvertisementPredicates(readiness = DORMANT_BOSS_ADVERTISEMENT_READINESS) {
|
|
403
|
+
const parsed = parseBossAdvertisementReadiness(readiness);
|
|
404
|
+
return BOSS_ADVERTISEMENT_PREDICATES.filter((predicate) => parsed[predicate] !== true);
|
|
405
|
+
}
|
|
406
|
+
function bossCapabilityAdvertisement(readiness = DORMANT_BOSS_ADVERTISEMENT_READINESS) {
|
|
407
|
+
if (missingBossAdvertisementPredicates(readiness).length > 0) return void 0;
|
|
408
|
+
const features = [{
|
|
409
|
+
version: BROKER_FEATURE_ATTESTATION_VERSION,
|
|
410
|
+
feature: BOSS_RUN_FEATURE,
|
|
411
|
+
featureVersion: BOSS_RUN_FEATURE_VERSION,
|
|
412
|
+
semanticsHash: BOSS_RUN_FEATURE_SEMANTICS_HASH,
|
|
413
|
+
controlEnvelopeVersion: BOSS_CONTROL_ENVELOPE_VERSION,
|
|
414
|
+
capabilityDigest: BOSS_CAPABILITY_FEATURE_DIGEST
|
|
415
|
+
}];
|
|
416
|
+
return parseBrokerCapabilityAdvertisement({
|
|
417
|
+
baseProtocolVersion: INTERCOM_BASE_PROTOCOL_VERSION,
|
|
418
|
+
features,
|
|
419
|
+
protocolFeatureContractHash: BOSS_RUN_PROTOCOL_FEATURE_CONTRACT_HASH,
|
|
420
|
+
featureSetHash: brokerFeatureSetHash(features),
|
|
421
|
+
controlEnvelopeVersion: BOSS_CONTROL_ENVELOPE_VERSION,
|
|
422
|
+
capabilityDigest: BOSS_CAPABILITY_FEATURE_DIGEST
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function optionalOwnDataValue(value, key) {
|
|
426
|
+
assertRecord(value);
|
|
427
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
428
|
+
if (descriptor === void 0) return void 0;
|
|
429
|
+
if (!descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
430
|
+
throw new ContractValidationError(`$.${key}`, "must be an own enumerable data property");
|
|
431
|
+
}
|
|
432
|
+
return descriptor.value;
|
|
433
|
+
}
|
|
434
|
+
function ownDataValue(value, key) {
|
|
435
|
+
const result = optionalOwnDataValue(value, key);
|
|
436
|
+
if (result === void 0) throw new ContractValidationError(`$.${key}`, "is required");
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
function parseBossParticipantRegistrationMetadata(value) {
|
|
440
|
+
assertBossCanonicalData(value);
|
|
441
|
+
assertRecord(value);
|
|
442
|
+
assertExactKeys(value, ["featureContract", "credential"]);
|
|
443
|
+
const featureContract = parseBossRunFeatureContract(ownDataValue(value, "featureContract"));
|
|
444
|
+
if (featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || canonicalJson(featureContract) !== canonicalJson(BOSS_RUN_FEATURE_CONTRACT)) {
|
|
445
|
+
throw new ContractValidationError("$.featureContract", "must exactly negotiate boss-run-v1 over base protocol v3");
|
|
446
|
+
}
|
|
447
|
+
const credential = parseBossParticipantCredentialEnvelope(ownDataValue(value, "credential"));
|
|
448
|
+
if (credential.namespace !== featureContract.feature) {
|
|
449
|
+
throw new ContractValidationError("$.credential.namespace", "must match the negotiated feature namespace");
|
|
450
|
+
}
|
|
451
|
+
return { featureContract, credential };
|
|
452
|
+
}
|
|
453
|
+
function exactRegistrationKind(session, value) {
|
|
454
|
+
assertBossCanonicalData(session, "$.session");
|
|
455
|
+
assertRecord(session);
|
|
456
|
+
const boss = optionalOwnDataValue(session, "boss");
|
|
457
|
+
if (boss === void 0) {
|
|
458
|
+
if (value === void 0) return "ordinary";
|
|
459
|
+
throw new ContractValidationError("$.registrationKind", "must be absent when Boss metadata is absent");
|
|
460
|
+
}
|
|
461
|
+
if (value !== "boss") {
|
|
462
|
+
throw new ContractValidationError("$.registrationKind", "must be boss when Boss metadata is present");
|
|
463
|
+
}
|
|
464
|
+
return "boss";
|
|
465
|
+
}
|
|
466
|
+
function parseExactRegistrationFrame(value) {
|
|
467
|
+
assertBossCanonicalData(value, "$.register");
|
|
468
|
+
assertRecord(value);
|
|
469
|
+
const session = ownDataValue(value, "session");
|
|
470
|
+
assertRecord(session);
|
|
471
|
+
const registrationKind = optionalOwnDataValue(value, "registrationKind");
|
|
472
|
+
const kind = exactRegistrationKind(session, registrationKind);
|
|
473
|
+
if (kind === "ordinary") {
|
|
474
|
+
assertExactKeys(value, ["type", "protocol", "version", "session"], ["sessionId", "stateId", "access", "scopeId"]);
|
|
475
|
+
assertExactKeys(session, ORDINARY_SESSION_REGISTRATION_KEYS, OPTIONAL_SESSION_REGISTRATION_KEYS);
|
|
476
|
+
} else {
|
|
477
|
+
assertExactKeys(value, ["type", "registrationKind", "protocol", "version", "session"], ["sessionId", "stateId", "scopeId"]);
|
|
478
|
+
assertExactKeys(session, [...ORDINARY_SESSION_REGISTRATION_KEYS, "boss"], OPTIONAL_SESSION_REGISTRATION_KEYS);
|
|
479
|
+
parseBossParticipantRegistrationMetadata(ownDataValue(session, "boss"));
|
|
480
|
+
}
|
|
481
|
+
if (ownDataValue(value, "type") !== "register") {
|
|
482
|
+
throw new ContractValidationError("$.register.type", "must be register");
|
|
483
|
+
}
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
function parseExactRegisteredFrame(value, expected) {
|
|
487
|
+
assertBossCanonicalData(value, "$.registered");
|
|
488
|
+
assertRecord(value);
|
|
489
|
+
if (expected === "boss") {
|
|
490
|
+
assertExactKeys(value, ["type", "registrationKind", "sessionId", "protocol", "version", "capabilities", "boss"]);
|
|
491
|
+
if (ownDataValue(value, "registrationKind") !== "boss") {
|
|
492
|
+
throw new ContractValidationError("$.registered.registrationKind", "must be boss");
|
|
493
|
+
}
|
|
494
|
+
const sessionId = ownDataValue(value, "sessionId");
|
|
495
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
496
|
+
throw new ContractValidationError("$.registered.sessionId", "must be a non-empty string");
|
|
497
|
+
}
|
|
498
|
+
const advertisement = parseBrokerCapabilityAdvertisement(ownDataValue(value, "capabilities"));
|
|
499
|
+
const expectedAdvertisement = bossCapabilityAdvertisement({
|
|
500
|
+
protectedProvider: true,
|
|
501
|
+
brokerIdentity: true,
|
|
502
|
+
credentialRegistry: true,
|
|
503
|
+
authorityTransitions: true,
|
|
504
|
+
participantHealth: true
|
|
505
|
+
});
|
|
506
|
+
const bossFeature = advertisement.features.find((feature) => feature.feature === BOSS_RUN_FEATURE);
|
|
507
|
+
if (bossFeature === void 0 || canonicalJson(bossFeature) !== canonicalJson(expectedAdvertisement.features[0]) || advertisement.baseProtocolVersion !== expectedAdvertisement.baseProtocolVersion || advertisement.protocolFeatureContractHash !== expectedAdvertisement.protocolFeatureContractHash || advertisement.controlEnvelopeVersion !== expectedAdvertisement.controlEnvelopeVersion || advertisement.capabilityDigest !== expectedAdvertisement.capabilityDigest) throw new ContractValidationError("$.registered.capabilities", "must exactly echo the requested boss-run-v1 contract");
|
|
508
|
+
parseBossParticipantBindingMetadata(ownDataValue(value, "boss"), sessionId);
|
|
509
|
+
} else if (expected === "ordinary-remote") {
|
|
510
|
+
assertExactKeys(value, ["type", "sessionId", "protocol", "version", "remoteAccess", "access"]);
|
|
511
|
+
} else {
|
|
512
|
+
assertExactKeys(value, ["type", "sessionId", "protocol", "version"]);
|
|
513
|
+
}
|
|
514
|
+
if (ownDataValue(value, "type") !== "registered") {
|
|
515
|
+
throw new ContractValidationError("$.registered.type", "must be registered");
|
|
516
|
+
}
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
function parseBossParticipantBindingMetadata(value, expectedSessionId) {
|
|
520
|
+
assertBossCanonicalData(value);
|
|
521
|
+
assertRecord(value);
|
|
522
|
+
assertExactKeys(
|
|
523
|
+
value,
|
|
524
|
+
["featureContract", "binding", "brokerIdentityVerified"],
|
|
525
|
+
["assignedParticipantIds", "requestingPrincipalId", "workerIdentity", "participantState"]
|
|
526
|
+
);
|
|
527
|
+
const featureContract = parseBossRunFeatureContract(ownDataValue(value, "featureContract"));
|
|
528
|
+
if (featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || canonicalJson(featureContract) !== canonicalJson(BOSS_RUN_FEATURE_CONTRACT)) {
|
|
529
|
+
throw new ContractValidationError("$.featureContract", "must exactly bind boss-run-v1 over base protocol v3");
|
|
530
|
+
}
|
|
531
|
+
const binding = parseBossParticipantBinding(ownDataValue(value, "binding"));
|
|
532
|
+
if (ownDataValue(value, "brokerIdentityVerified") !== true) {
|
|
533
|
+
throw new ContractValidationError("$.brokerIdentityVerified", "must be true for a broker-owned Boss binding");
|
|
534
|
+
}
|
|
535
|
+
if (expectedSessionId !== void 0 && binding.sessionId !== expectedSessionId) {
|
|
536
|
+
throw new ContractValidationError("$.binding.sessionId", "must match the registered intercom session");
|
|
537
|
+
}
|
|
538
|
+
const rawAssignedParticipantIds = optionalOwnDataValue(value, "assignedParticipantIds");
|
|
539
|
+
let assignedParticipantIds;
|
|
540
|
+
if (rawAssignedParticipantIds !== void 0) {
|
|
541
|
+
if (binding.role !== "manager" || !Array.isArray(rawAssignedParticipantIds) || rawAssignedParticipantIds.some((entry) => typeof entry !== "string" || entry.length === 0) || new Set(rawAssignedParticipantIds).size !== rawAssignedParticipantIds.length) {
|
|
542
|
+
throw new ContractValidationError("$.assignedParticipantIds", "must be a unique participant list present only for a Manager");
|
|
543
|
+
}
|
|
544
|
+
assignedParticipantIds = rawAssignedParticipantIds;
|
|
545
|
+
}
|
|
546
|
+
if (binding.role === "manager" && assignedParticipantIds === void 0) {
|
|
547
|
+
throw new ContractValidationError("$.assignedParticipantIds", "is required for a Manager policy binding");
|
|
548
|
+
}
|
|
549
|
+
const rawRequestingPrincipalId = optionalOwnDataValue(value, "requestingPrincipalId");
|
|
550
|
+
if (binding.role === "council" !== (typeof rawRequestingPrincipalId === "string" && rawRequestingPrincipalId.length > 0)) {
|
|
551
|
+
throw new ContractValidationError("$.requestingPrincipalId", "is required exactly for a Council policy binding");
|
|
552
|
+
}
|
|
553
|
+
const requestingPrincipalId = typeof rawRequestingPrincipalId === "string" ? rawRequestingPrincipalId : void 0;
|
|
554
|
+
const rawWorkerIdentity = optionalOwnDataValue(value, "workerIdentity");
|
|
555
|
+
const rawParticipantState = optionalOwnDataValue(value, "participantState");
|
|
556
|
+
if (rawWorkerIdentity === void 0 !== (rawParticipantState === void 0)) {
|
|
557
|
+
throw new ContractValidationError("$.workerIdentity", "workerIdentity and participantState must be supplied together");
|
|
558
|
+
}
|
|
559
|
+
const workerIdentity = rawWorkerIdentity === void 0 ? void 0 : parseWorkerIdentityV2(rawWorkerIdentity);
|
|
560
|
+
const participantState = rawParticipantState === void 0 ? void 0 : parseParticipantState(rawParticipantState, "$.participantState");
|
|
561
|
+
if (workerIdentity !== void 0 && (!("bossRunId" in workerIdentity) || workerIdentity.bossRunId !== binding.bossRunId || workerIdentity.participantId !== binding.participantId || workerIdentity.bindingEpoch !== binding.bindingEpoch)) throw new ContractValidationError("$.workerIdentity", "must match the broker-owned participant binding");
|
|
562
|
+
return {
|
|
563
|
+
featureContract,
|
|
564
|
+
binding,
|
|
565
|
+
brokerIdentityVerified: true,
|
|
566
|
+
...assignedParticipantIds === void 0 ? {} : { assignedParticipantIds: [...assignedParticipantIds] },
|
|
567
|
+
...requestingPrincipalId === void 0 ? {} : { requestingPrincipalId },
|
|
568
|
+
...workerIdentity === void 0 ? {} : { workerIdentity, participantState }
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
var BOSS_CONTROL_KIND_BY_TYPE = {
|
|
572
|
+
"boss.assignment.created": "assignment_request",
|
|
573
|
+
"boss.assignment.accepted": "assignment_response",
|
|
574
|
+
"boss.assignment.checkpoint": "assignment_response",
|
|
575
|
+
"boss.assignment.submitted": "assignment_response",
|
|
576
|
+
"boss.assignment.rejected": "assignment_response",
|
|
577
|
+
"boss.assignment.cancelled": "lifecycle",
|
|
578
|
+
"boss.staffing.requested": "staffing",
|
|
579
|
+
"boss.staffing.resolved": "staffing",
|
|
580
|
+
"boss.review.requested": "review_request",
|
|
581
|
+
"boss.review.submitted": "review_result",
|
|
582
|
+
"boss.council.requested": "review_request",
|
|
583
|
+
"boss.council.submitted": "review_result",
|
|
584
|
+
"boss.proof.submitted": "proof",
|
|
585
|
+
"boss.worker.health": "health",
|
|
586
|
+
"boss.worker.blocked": "health",
|
|
587
|
+
"boss.worker.failed": "health",
|
|
588
|
+
"boss.worker.notice": "lifecycle",
|
|
589
|
+
"boss.worker.notice_delivery_failed": "lifecycle",
|
|
590
|
+
"boss.decision.required": "decision"
|
|
591
|
+
};
|
|
592
|
+
function bossControlKind(envelopeValue) {
|
|
593
|
+
assertBossCanonicalData(envelopeValue);
|
|
594
|
+
const envelope = parseBossControlEnvelope(envelopeValue);
|
|
595
|
+
return { envelope, controlKind: BOSS_CONTROL_KIND_BY_TYPE[envelope.type] };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// boss-control-outbox.ts
|
|
599
|
+
var BOSS_CONTROL_OUTBOX_VERSION = 2;
|
|
600
|
+
var MAX_BOSS_CONTROL_OUTBOX_ENTRIES = 256;
|
|
601
|
+
function scope(envelope) {
|
|
602
|
+
return canonicalHash("agent-intercom-codex/boss-control/outbox-scope/v1", {
|
|
603
|
+
bossRunId: envelope.bossRunId,
|
|
604
|
+
participantId: envelope.participantId,
|
|
605
|
+
bindingEpoch: Number(envelope.bindingEpoch),
|
|
606
|
+
idempotencyKey: envelope.idempotencyKey
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function fingerprint2(to, envelope) {
|
|
610
|
+
const { messageId: _transportMessageId, ...stableEnvelope } = envelope;
|
|
611
|
+
return canonicalHash("agent-intercom-codex/boss-control/outbox-request/v1", { to, envelope: stableEnvelope });
|
|
612
|
+
}
|
|
613
|
+
function exactKeys(value, required, optional = []) {
|
|
614
|
+
const permitted = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
615
|
+
const keys = Reflect.ownKeys(value);
|
|
616
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => typeof key === "string" && permitted.has(key));
|
|
617
|
+
}
|
|
618
|
+
function parseEntry(value) {
|
|
619
|
+
assertBossCanonicalData(value, "$.bossControlOutbox.entries[]");
|
|
620
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid Boss outbox entry");
|
|
621
|
+
const entry = value;
|
|
622
|
+
if (!exactKeys(entry, ["to", "envelope", "scope", "fingerprint", "queuedAt", "state"], ["deliveryId"])) {
|
|
623
|
+
throw new Error("Invalid Boss outbox entry fields");
|
|
624
|
+
}
|
|
625
|
+
if (typeof entry.to !== "string" || entry.to.length === 0 || typeof entry.scope !== "string" || !/^[a-f0-9]{64}$/.test(entry.scope) || typeof entry.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(entry.fingerprint) || typeof entry.queuedAt !== "number" || !Number.isSafeInteger(entry.queuedAt) || entry.state !== "queued" && entry.state !== "accepted" || entry.state === "queued" && Object.hasOwn(entry, "deliveryId") || entry.state === "accepted" && (!Object.hasOwn(entry, "deliveryId") || typeof entry.deliveryId !== "string" || entry.deliveryId.length === 0)) throw new Error("Invalid Boss outbox entry binding");
|
|
626
|
+
const envelope = parseBossControlEnvelope2(entry.envelope);
|
|
627
|
+
if (entry.scope !== scope(envelope) || entry.fingerprint !== fingerprint2(entry.to, envelope)) {
|
|
628
|
+
throw new Error("Boss outbox entry canonical binding mismatch");
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
to: entry.to,
|
|
632
|
+
envelope,
|
|
633
|
+
scope: entry.scope,
|
|
634
|
+
fingerprint: entry.fingerprint,
|
|
635
|
+
queuedAt: entry.queuedAt,
|
|
636
|
+
state: entry.state,
|
|
637
|
+
...entry.deliveryId === void 0 ? {} : { deliveryId: entry.deliveryId }
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function fileName2(sessionId) {
|
|
641
|
+
return `${createHash2("sha256").update(sessionId).digest("hex")}.json`;
|
|
642
|
+
}
|
|
643
|
+
var PersistentBossControlOutbox = class {
|
|
644
|
+
path;
|
|
645
|
+
state;
|
|
646
|
+
constructor(sessionId, intercomDir = getIntercomDirPath()) {
|
|
647
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
648
|
+
const directory = join3(intercomDir, "boss-control-outbox");
|
|
649
|
+
mkdirSync3(directory, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
650
|
+
if (process.platform !== "win32") chmodSync3(directory, INTERCOM_DIR_MODE);
|
|
651
|
+
this.path = join3(directory, fileName2(sessionId));
|
|
652
|
+
this.state = this.load();
|
|
653
|
+
}
|
|
654
|
+
list() {
|
|
655
|
+
return structuredClone(this.state.entries);
|
|
656
|
+
}
|
|
657
|
+
find(idempotencyKey) {
|
|
658
|
+
const entry = this.state.entries.find((candidate) => candidate.envelope.idempotencyKey === idempotencyKey);
|
|
659
|
+
return entry === void 0 ? void 0 : structuredClone(entry);
|
|
660
|
+
}
|
|
661
|
+
enqueue(to, envelopeValue) {
|
|
662
|
+
if (typeof to !== "string" || to.length === 0) throw new Error("Boss target session ID is required");
|
|
663
|
+
assertBossCanonicalData(envelopeValue, "$.envelope");
|
|
664
|
+
const envelope = parseBossControlEnvelope2(envelopeValue);
|
|
665
|
+
const candidateScope = scope(envelope);
|
|
666
|
+
const candidateFingerprint = fingerprint2(to, envelope);
|
|
667
|
+
const existing = this.state.entries.find((entry) => entry.scope === candidateScope);
|
|
668
|
+
if (existing) {
|
|
669
|
+
if (existing.fingerprint !== candidateFingerprint) {
|
|
670
|
+
throw new Error(`Boss idempotency key ${envelope.idempotencyKey} is queued with a different canonical request`);
|
|
671
|
+
}
|
|
672
|
+
if (existing.envelope.messageId !== envelope.messageId) {
|
|
673
|
+
existing.envelope = envelope;
|
|
674
|
+
existing.queuedAt = Date.now();
|
|
675
|
+
this.persist();
|
|
676
|
+
}
|
|
677
|
+
return "existing";
|
|
678
|
+
}
|
|
679
|
+
if (this.state.entries.some((entry) => entry.envelope.messageId === envelope.messageId)) {
|
|
680
|
+
throw new Error(`Boss message ID ${envelope.messageId} is queued with a different idempotency scope`);
|
|
681
|
+
}
|
|
682
|
+
if (this.state.entries.length >= MAX_BOSS_CONTROL_OUTBOX_ENTRIES) throw new Error("Durable Boss control outbox is full");
|
|
683
|
+
this.state.entries.push({
|
|
684
|
+
to,
|
|
685
|
+
envelope,
|
|
686
|
+
scope: candidateScope,
|
|
687
|
+
fingerprint: candidateFingerprint,
|
|
688
|
+
queuedAt: Date.now(),
|
|
689
|
+
state: "queued"
|
|
690
|
+
});
|
|
691
|
+
this.persist();
|
|
692
|
+
return "added";
|
|
693
|
+
}
|
|
694
|
+
markAccepted(idempotencyKey, messageId, deliveryId) {
|
|
695
|
+
const entry = this.state.entries.find((candidate) => candidate.envelope.idempotencyKey === idempotencyKey);
|
|
696
|
+
if (!entry || entry.envelope.messageId !== messageId || !deliveryId) {
|
|
697
|
+
throw new Error("Boss acknowledgement does not match the durable outbox binding");
|
|
698
|
+
}
|
|
699
|
+
if (entry.state === "accepted") {
|
|
700
|
+
if (entry.deliveryId !== deliveryId) throw new Error("Boss acknowledgement changed the durable deliveryId");
|
|
701
|
+
return "already-accepted";
|
|
702
|
+
}
|
|
703
|
+
entry.state = "accepted";
|
|
704
|
+
entry.deliveryId = deliveryId;
|
|
705
|
+
this.persist();
|
|
706
|
+
return "accepted";
|
|
707
|
+
}
|
|
708
|
+
removeCorrelated(idempotencyKey, messageId, deliveryId) {
|
|
709
|
+
const index = this.state.entries.findIndex((candidate) => candidate.envelope.idempotencyKey === idempotencyKey);
|
|
710
|
+
if (index < 0) throw new Error("Boss terminal result has no durable outbox binding");
|
|
711
|
+
const entry = this.state.entries[index];
|
|
712
|
+
if (entry.envelope.messageId !== messageId) throw new Error("Boss terminal result messageId does not match the durable caller");
|
|
713
|
+
if (deliveryId === void 0) {
|
|
714
|
+
if (entry.state !== "queued") throw new Error("Boss post-acceptance failure omitted the durable deliveryId");
|
|
715
|
+
} else if (entry.state !== "accepted" || entry.deliveryId !== deliveryId) {
|
|
716
|
+
throw new Error("Boss terminal result arrived before the matching durable acknowledgement");
|
|
717
|
+
}
|
|
718
|
+
this.state.entries.splice(index, 1);
|
|
719
|
+
this.persist();
|
|
720
|
+
}
|
|
721
|
+
load() {
|
|
722
|
+
if (!existsSync2(this.path)) return { version: BOSS_CONTROL_OUTBOX_VERSION, entries: [] };
|
|
723
|
+
try {
|
|
724
|
+
const parsed = JSON.parse(readFileSync3(this.path, "utf8"));
|
|
725
|
+
assertBossCanonicalData(parsed, "$.bossControlOutbox");
|
|
726
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
727
|
+
const state = parsed;
|
|
728
|
+
if (!exactKeys(state, ["version", "entries"]) || state.version !== BOSS_CONTROL_OUTBOX_VERSION || !Array.isArray(state.entries)) {
|
|
729
|
+
throw new Error("invalid Boss outbox state");
|
|
730
|
+
}
|
|
731
|
+
return { version: BOSS_CONTROL_OUTBOX_VERSION, entries: state.entries.map(parseEntry) };
|
|
732
|
+
} catch (error2) {
|
|
733
|
+
const corruptPath = `${this.path}.corrupt-${Date.now()}`;
|
|
734
|
+
renameSync3(this.path, corruptPath);
|
|
735
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
736
|
+
throw new Error(`Boss control outbox was corrupt and quarantined at ${corruptPath}`, { cause: error2 });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
persist() {
|
|
740
|
+
writeDurableJson(this.path, this.state);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// broker/access-credential.ts
|
|
745
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
746
|
+
var ACCESS_CREDENTIAL_ENV = "AGENT_INTERCOM_ACCESS_CREDENTIAL_PATH";
|
|
747
|
+
var ACCESS_CREDENTIAL_VERSION = 1;
|
|
748
|
+
function nonEmptyString(value) {
|
|
749
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
750
|
+
}
|
|
751
|
+
function loadRemoteAccessCredential(env = process.env) {
|
|
752
|
+
const path = env[ACCESS_CREDENTIAL_ENV]?.trim();
|
|
753
|
+
if (!path) return void 0;
|
|
754
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
755
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
756
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
757
|
+
}
|
|
758
|
+
const credential = parsed;
|
|
759
|
+
if (nonEmptyString(credential.enrollmentToken)) {
|
|
760
|
+
return { path, access: { enrollmentToken: credential.enrollmentToken }, enrollment: true };
|
|
761
|
+
}
|
|
762
|
+
if (credential.version === ACCESS_CREDENTIAL_VERSION && nonEmptyString(credential.sessionCredential) && nonEmptyString(credential.sessionId) && typeof credential.generation === "number" && Number.isSafeInteger(credential.generation) && credential.generation > 0) {
|
|
763
|
+
return {
|
|
764
|
+
path,
|
|
765
|
+
access: {
|
|
766
|
+
sessionCredential: credential.sessionCredential,
|
|
767
|
+
sessionId: credential.sessionId,
|
|
768
|
+
generation: credential.generation
|
|
769
|
+
},
|
|
770
|
+
enrollment: false
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
774
|
+
}
|
|
775
|
+
function writeRemoteSessionCredential(path, sessionId, metadata) {
|
|
776
|
+
if (!metadata.sessionCredential) {
|
|
777
|
+
throw new Error("Remote enrollment response omitted the session credential");
|
|
778
|
+
}
|
|
779
|
+
writeDurableJson(path, {
|
|
780
|
+
version: ACCESS_CREDENTIAL_VERSION,
|
|
781
|
+
sessionCredential: metadata.sessionCredential,
|
|
782
|
+
sessionId,
|
|
783
|
+
generation: metadata.generation
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// broker/boss-control-ledger.ts
|
|
788
|
+
import { canonicalJson as canonicalJson2 } from "@ctliz/agent-intercom-core/canonical";
|
|
789
|
+
var BOSS_CONTROL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
790
|
+
"INVALID_CONTROL",
|
|
791
|
+
"IDEMPOTENCY_CONFLICT",
|
|
792
|
+
"SESSION_NOT_FOUND",
|
|
793
|
+
"POLICY_DENIED",
|
|
794
|
+
"RECIPIENT_DISCONNECTED",
|
|
795
|
+
"DELIVERY_TIMEOUT"
|
|
796
|
+
]);
|
|
797
|
+
function exactStringKeys(value, required, optional = []) {
|
|
798
|
+
const keys = Reflect.ownKeys(value);
|
|
799
|
+
const permitted = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
800
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => typeof key === "string" && permitted.has(key));
|
|
801
|
+
}
|
|
802
|
+
function parseBossControlResult(value) {
|
|
803
|
+
assertBossCanonicalData(value, "$.bossControlResult");
|
|
804
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
805
|
+
throw new Error("Boss control result must be an exact plain object");
|
|
806
|
+
}
|
|
807
|
+
const result = value;
|
|
808
|
+
const base = typeof result.requestId === "string" && result.requestId.length > 0 && result.messageId === result.requestId && typeof result.idempotencyKey === "string" && result.idempotencyKey.length > 0;
|
|
809
|
+
if (!base || result.type !== "boss_control_result") throw new Error("Invalid Boss control result binding");
|
|
810
|
+
if (result.status === "delivered" && result.delivered === true && typeof result.deliveryId === "string" && result.deliveryId.length > 0 && exactStringKeys(result, ["type", "requestId", "messageId", "idempotencyKey", "status", "delivered", "deliveryId"])) return result;
|
|
811
|
+
if (result.status === "rejected" && result.delivered === false && typeof result.code === "string" && BOSS_CONTROL_FAILURE_CODES.has(result.code) && typeof result.reason === "string" && result.reason.length > 0 && (!Object.hasOwn(result, "deliveryId") || typeof result.deliveryId === "string" && result.deliveryId.length > 0) && exactStringKeys(
|
|
812
|
+
result,
|
|
813
|
+
["type", "requestId", "messageId", "idempotencyKey", "status", "delivered", "code", "reason"],
|
|
814
|
+
["deliveryId"]
|
|
815
|
+
)) return result;
|
|
816
|
+
throw new Error("Invalid Boss control result discriminant");
|
|
817
|
+
}
|
|
818
|
+
function parseBossControlAck(value) {
|
|
819
|
+
assertBossCanonicalData(value, "$.bossControlAck");
|
|
820
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
821
|
+
throw new Error("Boss control acknowledgement must be an exact plain object");
|
|
822
|
+
}
|
|
823
|
+
const ack = value;
|
|
824
|
+
if (!exactStringKeys(ack, ["type", "requestId", "messageId", "idempotencyKey", "status", "deliveryId"]) || ack.type !== "boss_control_ack" || typeof ack.requestId !== "string" || ack.requestId.length === 0 || ack.messageId !== ack.requestId || typeof ack.idempotencyKey !== "string" || ack.idempotencyKey.length === 0 || ack.status !== "accepted" || typeof ack.deliveryId !== "string" || ack.deliveryId.length === 0) throw new Error("Invalid Boss control acknowledgement discriminant");
|
|
825
|
+
return ack;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// broker/client.ts
|
|
829
|
+
import { types as nodeUtilTypes2 } from "node:util";
|
|
830
|
+
function toError(error2) {
|
|
831
|
+
return error2 instanceof Error ? error2 : new Error(String(error2));
|
|
832
|
+
}
|
|
833
|
+
function connectToBrokerTarget(target) {
|
|
834
|
+
return typeof target === "string" ? net.connect(target) : net.connect({ host: target.host, port: target.port });
|
|
835
|
+
}
|
|
836
|
+
function isAttachment(value) {
|
|
837
|
+
if (typeof value !== "object" || value === null) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
const attachment = value;
|
|
841
|
+
if (attachment.type !== "file" && attachment.type !== "snippet" && attachment.type !== "context") {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (typeof attachment.name !== "string" || typeof attachment.content !== "string") {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
return attachment.language === void 0 || typeof attachment.language === "string";
|
|
848
|
+
}
|
|
849
|
+
function isMessage(value) {
|
|
850
|
+
if (typeof value !== "object" || value === null) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
const message = value;
|
|
854
|
+
if (typeof message.id !== "string" || typeof message.timestamp !== "number") {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
if (message.replyTo !== void 0 && typeof message.replyTo !== "string") {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (message.expectsReply !== void 0 && typeof message.expectsReply !== "boolean") {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (typeof message.content !== "object" || message.content === null) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
const content = message.content;
|
|
867
|
+
if (typeof content.text !== "string") {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.every(isAttachment);
|
|
871
|
+
}
|
|
872
|
+
function isSessionInfo(value) {
|
|
873
|
+
if (typeof value !== "object" || value === null) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
const session = value;
|
|
877
|
+
if (typeof session.id !== "string" || typeof session.cwd !== "string" || typeof session.model !== "string" || typeof session.pid !== "number" || typeof session.startedAt !== "number" || typeof session.lastActivity !== "number") {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
if (session.name !== void 0 && typeof session.name !== "string") {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (session.status !== void 0 && typeof session.status !== "string") {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
if (session.peerUid !== void 0 && typeof session.peerUid !== "number") {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
if (session.trustedLocal !== void 0 && typeof session.trustedLocal !== "boolean") return false;
|
|
890
|
+
if (session.origin !== void 0 && session.origin !== "local" && session.origin !== "remote") return false;
|
|
891
|
+
if (session.remoteHostId !== void 0 && typeof session.remoteHostId !== "string") return false;
|
|
892
|
+
if (session.parentSessionId !== void 0 && typeof session.parentSessionId !== "string") return false;
|
|
893
|
+
if (session.rootSessionId !== void 0 && typeof session.rootSessionId !== "string") return false;
|
|
894
|
+
if (session.generation !== void 0 && (typeof session.generation !== "number" || !Number.isSafeInteger(session.generation))) return false;
|
|
895
|
+
if (session.canDelegate !== void 0 && typeof session.canDelegate !== "boolean") return false;
|
|
896
|
+
for (const field of ["depth", "maxDepth", "maxChildren"]) {
|
|
897
|
+
if (session[field] !== void 0 && (typeof session[field] !== "number" || !Number.isSafeInteger(session[field]))) return false;
|
|
898
|
+
}
|
|
899
|
+
if (session.boss !== void 0) {
|
|
900
|
+
try {
|
|
901
|
+
parseBossParticipantBindingMetadata(session.boss, session.id);
|
|
902
|
+
} catch {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
function isRemoteAccessMetadata(value) {
|
|
909
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
910
|
+
const access = value;
|
|
911
|
+
return access.origin === "remote" && typeof access.remoteHostId === "string" && typeof access.parentSessionId === "string" && typeof access.rootSessionId === "string" && typeof access.generation === "number" && Number.isSafeInteger(access.generation) && access.generation > 0 && typeof access.canDelegate === "boolean" && typeof access.depth === "number" && Number.isSafeInteger(access.depth) && typeof access.maxDepth === "number" && Number.isSafeInteger(access.maxDepth) && typeof access.maxChildren === "number" && Number.isSafeInteger(access.maxChildren) && (access.sessionCredential === void 0 || typeof access.sessionCredential === "string");
|
|
912
|
+
}
|
|
913
|
+
var IntercomClient = class extends EventEmitter {
|
|
914
|
+
socket = null;
|
|
915
|
+
scopeId;
|
|
916
|
+
_sessionId = null;
|
|
917
|
+
pendingSends = /* @__PURE__ */ new Map();
|
|
918
|
+
pendingLists = /* @__PURE__ */ new Map();
|
|
919
|
+
pendingAskControls = /* @__PURE__ */ new Map();
|
|
920
|
+
pendingBossControls = /* @__PURE__ */ new Map();
|
|
921
|
+
outbox = null;
|
|
922
|
+
bossControlOutbox = null;
|
|
923
|
+
remoteAccessCredential;
|
|
924
|
+
requestedBossRegistration;
|
|
925
|
+
_bossBinding;
|
|
926
|
+
disconnecting = false;
|
|
927
|
+
disconnectError = null;
|
|
928
|
+
constructor(options = {}) {
|
|
929
|
+
super();
|
|
930
|
+
this.scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnv(options.env ?? process.env) : parseIntercomScopeId(options.scopeId, "scopeId");
|
|
931
|
+
}
|
|
932
|
+
failPending(error2) {
|
|
933
|
+
for (const pending of this.pendingSends.values()) {
|
|
934
|
+
pending.reject(error2);
|
|
935
|
+
}
|
|
936
|
+
this.pendingSends.clear();
|
|
937
|
+
for (const pending of this.pendingLists.values()) {
|
|
938
|
+
pending.reject(error2);
|
|
939
|
+
}
|
|
940
|
+
this.pendingLists.clear();
|
|
941
|
+
for (const pending of this.pendingAskControls.values()) {
|
|
942
|
+
clearTimeout(pending.timeout);
|
|
943
|
+
pending.resolve(false);
|
|
944
|
+
}
|
|
945
|
+
this.pendingAskControls.clear();
|
|
946
|
+
for (const pending of this.pendingBossControls.values()) {
|
|
947
|
+
clearTimeout(pending.timeout);
|
|
948
|
+
pending.reject(error2);
|
|
949
|
+
}
|
|
950
|
+
this.pendingBossControls.clear();
|
|
951
|
+
}
|
|
952
|
+
get sessionId() {
|
|
953
|
+
return this._sessionId;
|
|
954
|
+
}
|
|
955
|
+
get outboxSize() {
|
|
956
|
+
return this.outbox?.list().length ?? 0;
|
|
957
|
+
}
|
|
958
|
+
get bossBinding() {
|
|
959
|
+
return this._bossBinding;
|
|
960
|
+
}
|
|
961
|
+
get bossControlOutboxSize() {
|
|
962
|
+
return this.bossControlOutbox?.list().length ?? 0;
|
|
963
|
+
}
|
|
964
|
+
isConnected() {
|
|
965
|
+
const socket = this.socket;
|
|
966
|
+
return Boolean(socket && this._sessionId && !this.disconnecting && !socket.destroyed && !socket.writableEnded && socket.writable);
|
|
967
|
+
}
|
|
968
|
+
requireActiveSocket() {
|
|
969
|
+
if (this.disconnecting) {
|
|
970
|
+
throw new Error("Client disconnecting");
|
|
971
|
+
}
|
|
972
|
+
const socket = this.socket;
|
|
973
|
+
if (!socket || !this._sessionId) {
|
|
974
|
+
throw new Error("Not connected");
|
|
975
|
+
}
|
|
976
|
+
if (socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
977
|
+
throw new Error("Client disconnected");
|
|
978
|
+
}
|
|
979
|
+
return socket;
|
|
980
|
+
}
|
|
981
|
+
connect(session, sessionId) {
|
|
982
|
+
if (this.socket) {
|
|
983
|
+
return Promise.reject(new Error("Already connected"));
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const canonicalSession = parseExactRegistrationFrame({
|
|
987
|
+
type: "register",
|
|
988
|
+
...typeof session === "object" && session !== null && !nodeUtilTypes2.isProxy(session) && Object.getOwnPropertyDescriptor(session, "boss") !== void 0 ? { registrationKind: "boss" } : {},
|
|
989
|
+
protocol: INTERCOM_PROTOCOL_NAME2,
|
|
990
|
+
version: INTERCOM_PROTOCOL_VERSION2,
|
|
991
|
+
session
|
|
992
|
+
}).session;
|
|
993
|
+
this.requestedBossRegistration = session.boss === void 0 ? void 0 : parseBossParticipantRegistrationMetadata(session.boss);
|
|
994
|
+
if (canonicalSession !== session) throw new Error("Registration session identity changed during validation");
|
|
995
|
+
} catch (error2) {
|
|
996
|
+
return Promise.reject(toError(error2));
|
|
997
|
+
}
|
|
998
|
+
return new Promise((resolve3, reject) => {
|
|
999
|
+
let socket;
|
|
1000
|
+
let target;
|
|
1001
|
+
try {
|
|
1002
|
+
target = getBrokerConnectTarget();
|
|
1003
|
+
this.remoteAccessCredential = loadRemoteAccessCredential();
|
|
1004
|
+
socket = connectToBrokerTarget(target);
|
|
1005
|
+
} catch (error2) {
|
|
1006
|
+
reject(toError(error2));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
this.socket = socket;
|
|
1010
|
+
this.disconnectError = null;
|
|
1011
|
+
let settled = false;
|
|
1012
|
+
const timeout = setTimeout(() => {
|
|
1013
|
+
if (!this._sessionId) {
|
|
1014
|
+
cleanupConnectionAttempt();
|
|
1015
|
+
cleanupSocketListeners();
|
|
1016
|
+
if (this.socket === socket) {
|
|
1017
|
+
this.socket = null;
|
|
1018
|
+
}
|
|
1019
|
+
socket.destroy();
|
|
1020
|
+
reject(new Error("Connection timeout"));
|
|
1021
|
+
}
|
|
1022
|
+
}, 1e4);
|
|
1023
|
+
let connectionEstablished = false;
|
|
1024
|
+
const onRegistered = () => {
|
|
1025
|
+
settled = true;
|
|
1026
|
+
connectionEstablished = true;
|
|
1027
|
+
cleanupConnectionAttempt();
|
|
1028
|
+
resolve3();
|
|
1029
|
+
};
|
|
1030
|
+
const onError = (err) => {
|
|
1031
|
+
settled = true;
|
|
1032
|
+
cleanupConnectionAttempt();
|
|
1033
|
+
cleanupSocketListeners();
|
|
1034
|
+
if (this.socket === socket) {
|
|
1035
|
+
this.socket = null;
|
|
1036
|
+
}
|
|
1037
|
+
socket.destroy();
|
|
1038
|
+
reject(err);
|
|
1039
|
+
};
|
|
1040
|
+
const onClose = () => {
|
|
1041
|
+
const wasConnecting = !settled && !this._sessionId;
|
|
1042
|
+
const wasDisconnecting = this.disconnecting;
|
|
1043
|
+
const disconnectError = this.disconnectError ?? new Error("Client disconnected");
|
|
1044
|
+
this.disconnecting = false;
|
|
1045
|
+
cleanupConnectionAttempt();
|
|
1046
|
+
cleanupSocketListeners();
|
|
1047
|
+
this.failPending(disconnectError);
|
|
1048
|
+
if (this.socket === socket) {
|
|
1049
|
+
this.socket = null;
|
|
1050
|
+
}
|
|
1051
|
+
this._sessionId = null;
|
|
1052
|
+
this._bossBinding = void 0;
|
|
1053
|
+
this.requestedBossRegistration = void 0;
|
|
1054
|
+
this.disconnectError = null;
|
|
1055
|
+
if (connectionEstablished && !wasDisconnecting) {
|
|
1056
|
+
this.emit("disconnected", disconnectError);
|
|
1057
|
+
}
|
|
1058
|
+
if (wasConnecting) {
|
|
1059
|
+
reject(new Error("Connection closed before registration"));
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const onSocketError = (err) => {
|
|
1063
|
+
if (connectionEstablished) {
|
|
1064
|
+
this.disconnectError = err;
|
|
1065
|
+
this.emit("error", err);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const onReaderError = (error2) => {
|
|
1069
|
+
const protocolError = new Error(`Intercom protocol error: ${error2.message}`, { cause: error2 });
|
|
1070
|
+
if (!connectionEstablished) {
|
|
1071
|
+
onError(protocolError);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.disconnectError = protocolError;
|
|
1075
|
+
this.emit("error", protocolError);
|
|
1076
|
+
socket.destroy();
|
|
1077
|
+
};
|
|
1078
|
+
const reader = createMessageReader((msg) => {
|
|
1079
|
+
this.handleBrokerMessage(msg);
|
|
1080
|
+
}, onReaderError);
|
|
1081
|
+
const cleanupConnectionAttempt = () => {
|
|
1082
|
+
this.off("_registered", onRegistered);
|
|
1083
|
+
socket.off("error", onError);
|
|
1084
|
+
clearTimeout(timeout);
|
|
1085
|
+
};
|
|
1086
|
+
const cleanupSocketListeners = () => {
|
|
1087
|
+
socket.off("data", reader);
|
|
1088
|
+
socket.off("error", onSocketError);
|
|
1089
|
+
socket.off("close", onClose);
|
|
1090
|
+
};
|
|
1091
|
+
socket.on("data", reader);
|
|
1092
|
+
socket.on("error", onError);
|
|
1093
|
+
socket.on("close", onClose);
|
|
1094
|
+
socket.on("error", onSocketError);
|
|
1095
|
+
this.once("_registered", onRegistered);
|
|
1096
|
+
try {
|
|
1097
|
+
writeMessage(socket, {
|
|
1098
|
+
type: "register",
|
|
1099
|
+
...session.boss === void 0 ? {} : { registrationKind: "boss" },
|
|
1100
|
+
protocol: INTERCOM_PROTOCOL_NAME2,
|
|
1101
|
+
version: INTERCOM_PROTOCOL_VERSION2,
|
|
1102
|
+
session,
|
|
1103
|
+
...!this.remoteAccessCredential && sessionId ? { sessionId } : {},
|
|
1104
|
+
...this.remoteAccessCredential ? { access: this.remoteAccessCredential.access } : {},
|
|
1105
|
+
...this.scopeId ? { scopeId: this.scopeId } : {},
|
|
1106
|
+
...typeof target === "string" ? {} : { stateId: target.stateId }
|
|
1107
|
+
});
|
|
1108
|
+
} catch (error2) {
|
|
1109
|
+
cleanupConnectionAttempt();
|
|
1110
|
+
cleanupSocketListeners();
|
|
1111
|
+
if (this.socket === socket) {
|
|
1112
|
+
this.socket = null;
|
|
1113
|
+
}
|
|
1114
|
+
socket.destroy();
|
|
1115
|
+
reject(toError(error2));
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
handleBrokerMessage(msg) {
|
|
1120
|
+
if (typeof msg !== "object" || msg === null || nodeUtilTypes2.isProxy(msg)) {
|
|
1121
|
+
throw new Error("Invalid broker message");
|
|
1122
|
+
}
|
|
1123
|
+
const typeDescriptor = Object.getOwnPropertyDescriptor(msg, "type");
|
|
1124
|
+
if (typeDescriptor === void 0 || !typeDescriptor.enumerable || !Object.hasOwn(typeDescriptor, "value") || typeof typeDescriptor.value !== "string") throw new Error("Invalid broker message");
|
|
1125
|
+
const brokerMessage = msg;
|
|
1126
|
+
if (this._sessionId === null && brokerMessage.type !== "registered" && brokerMessage.type !== "error") {
|
|
1127
|
+
throw new Error(`Received ${brokerMessage.type} before registered`);
|
|
1128
|
+
}
|
|
1129
|
+
switch (brokerMessage.type) {
|
|
1130
|
+
case "registered": {
|
|
1131
|
+
parseExactRegisteredFrame(
|
|
1132
|
+
brokerMessage,
|
|
1133
|
+
this.requestedBossRegistration === void 0 ? this.remoteAccessCredential === void 0 ? "ordinary-local" : "ordinary-remote" : "boss"
|
|
1134
|
+
);
|
|
1135
|
+
if (typeof brokerMessage.sessionId !== "string" || brokerMessage.protocol !== INTERCOM_PROTOCOL_NAME2 || brokerMessage.version !== INTERCOM_PROTOCOL_VERSION2) {
|
|
1136
|
+
throw new Error("Invalid registered message");
|
|
1137
|
+
}
|
|
1138
|
+
if (this._sessionId !== null) {
|
|
1139
|
+
throw new Error("Received duplicate registered message");
|
|
1140
|
+
}
|
|
1141
|
+
if (this.remoteAccessCredential) {
|
|
1142
|
+
const contract = brokerMessage.remoteAccess;
|
|
1143
|
+
const contractFields = typeof contract === "object" && contract !== null ? contract : void 0;
|
|
1144
|
+
if (!contractFields || contractFields.feature !== "remote-access-v1" || contractFields.policySemanticsVersion !== POLICY_SEMANTICS_VERSION || contractFields.policySemanticsHash !== POLICY_SEMANTICS_HASH) {
|
|
1145
|
+
throw new Error("Remote Intercom policy contract is absent or incompatible");
|
|
1146
|
+
}
|
|
1147
|
+
if (!isRemoteAccessMetadata(brokerMessage.access)) {
|
|
1148
|
+
throw new Error("Remote Intercom registration omitted broker-owned provenance");
|
|
1149
|
+
}
|
|
1150
|
+
if (this.remoteAccessCredential.enrollment) {
|
|
1151
|
+
writeRemoteSessionCredential(this.remoteAccessCredential.path, brokerMessage.sessionId, brokerMessage.access);
|
|
1152
|
+
} else {
|
|
1153
|
+
const reconnect = this.remoteAccessCredential.access;
|
|
1154
|
+
if (!("sessionId" in reconnect) || reconnect.sessionId !== brokerMessage.sessionId || reconnect.generation !== brokerMessage.access.generation) {
|
|
1155
|
+
throw new Error("Remote Intercom reconnect identity or generation changed unexpectedly");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (this.requestedBossRegistration !== void 0) {
|
|
1160
|
+
if (brokerMessage.remoteAccess !== void 0 || brokerMessage.access !== void 0) {
|
|
1161
|
+
throw new Error("Boss registration returned folded remote-access metadata");
|
|
1162
|
+
}
|
|
1163
|
+
const advertisement = parseBrokerCapabilityAdvertisement2(brokerMessage.capabilities);
|
|
1164
|
+
if (!advertisement.features.some((feature) => feature.feature === BOSS_RUN_FEATURE2)) {
|
|
1165
|
+
throw new Error("Broker did not echo the required boss-run-v1 feature contract");
|
|
1166
|
+
}
|
|
1167
|
+
const binding = parseBossParticipantBindingMetadata(brokerMessage.boss, brokerMessage.sessionId);
|
|
1168
|
+
const credential = this.requestedBossRegistration.credential;
|
|
1169
|
+
if (binding.featureContract.feature !== this.requestedBossRegistration.featureContract.feature || binding.binding.bossRunId !== credential.bossRunId || binding.binding.participantId !== credential.participantId || binding.binding.role !== credential.role || binding.binding.communicationProfile !== credential.communicationProfile || binding.binding.bindingEpoch !== credential.bindingEpoch) {
|
|
1170
|
+
throw new Error("Broker returned a Boss binding that does not match the authenticated registration request");
|
|
1171
|
+
}
|
|
1172
|
+
this._bossBinding = binding;
|
|
1173
|
+
} else if (brokerMessage.boss !== void 0) {
|
|
1174
|
+
throw new Error("Broker attached unsolicited Boss binding metadata to an ordinary registration");
|
|
1175
|
+
}
|
|
1176
|
+
this._sessionId = brokerMessage.sessionId;
|
|
1177
|
+
this.outbox = new PersistentOutboundOutbox(brokerMessage.sessionId);
|
|
1178
|
+
this.bossControlOutbox = this._bossBinding === void 0 ? null : new PersistentBossControlOutbox(brokerMessage.sessionId);
|
|
1179
|
+
this.replayOutbox();
|
|
1180
|
+
this.replayBossControlOutbox();
|
|
1181
|
+
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
case "sessions": {
|
|
1185
|
+
const { requestId, sessions } = brokerMessage;
|
|
1186
|
+
if (typeof requestId !== "string" || !Array.isArray(sessions) || !sessions.every(isSessionInfo)) {
|
|
1187
|
+
throw new Error("Invalid sessions message");
|
|
1188
|
+
}
|
|
1189
|
+
const pending = this.pendingLists.get(requestId);
|
|
1190
|
+
if (!pending) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this.pendingLists.delete(requestId);
|
|
1194
|
+
pending.resolve(sessions);
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "message": {
|
|
1198
|
+
const { deliveryId, from, message } = brokerMessage;
|
|
1199
|
+
if (typeof deliveryId !== "string" || !isSessionInfo(from) || !isMessage(message)) {
|
|
1200
|
+
throw new Error("Invalid message event");
|
|
1201
|
+
}
|
|
1202
|
+
this.emit("message", from, message, deliveryId);
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
case "boss_control": {
|
|
1206
|
+
const { deliveryId, from } = brokerMessage;
|
|
1207
|
+
if (typeof deliveryId !== "string" || !isSessionInfo(from)) {
|
|
1208
|
+
throw new Error("Invalid boss_control event");
|
|
1209
|
+
}
|
|
1210
|
+
const envelope = bossControlKind(brokerMessage.envelope).envelope;
|
|
1211
|
+
const source = from.boss === void 0 ? void 0 : parseBossParticipantBindingMetadata(from.boss, from.id).binding;
|
|
1212
|
+
if (source === void 0 || source.state !== "active" || source.bossRunId !== envelope.bossRunId || source.participantId !== envelope.participantId || source.bindingEpoch !== envelope.bindingEpoch) throw new Error("Boss control event sender does not match its broker-owned binding");
|
|
1213
|
+
this.emit("boss_control", from, envelope, deliveryId);
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
case "boss_control_result": {
|
|
1217
|
+
const result = parseBossControlResult(brokerMessage);
|
|
1218
|
+
const { requestId, messageId, idempotencyKey, deliveryId } = result;
|
|
1219
|
+
const stored = this.bossControlOutbox?.find(idempotencyKey);
|
|
1220
|
+
if (!stored || stored.envelope.messageId !== requestId) throw new Error("Boss control result does not match the durable outbox binding");
|
|
1221
|
+
const pending = this.pendingBossControls.get(requestId);
|
|
1222
|
+
if (pending && (pending.messageId !== messageId || pending.idempotencyKey !== idempotencyKey)) {
|
|
1223
|
+
throw new Error("Boss control result correlation does not match the pending request");
|
|
1224
|
+
}
|
|
1225
|
+
this.bossControlOutbox.removeCorrelated(idempotencyKey, messageId, deliveryId);
|
|
1226
|
+
if (pending) {
|
|
1227
|
+
clearTimeout(pending.timeout);
|
|
1228
|
+
this.pendingBossControls.delete(requestId);
|
|
1229
|
+
pending.resolve(result);
|
|
1230
|
+
}
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
case "boss_control_ack": {
|
|
1234
|
+
const { requestId, messageId, idempotencyKey, deliveryId } = parseBossControlAck(brokerMessage);
|
|
1235
|
+
const pending = this.pendingBossControls.get(requestId);
|
|
1236
|
+
if (pending && (pending.messageId !== messageId || pending.idempotencyKey !== idempotencyKey)) {
|
|
1237
|
+
throw new Error("Boss control acknowledgement correlation does not match the pending request");
|
|
1238
|
+
}
|
|
1239
|
+
const transition = this.bossControlOutbox?.markAccepted(idempotencyKey, messageId, deliveryId);
|
|
1240
|
+
if (transition === void 0) throw new Error("Boss control acknowledgement has no durable outbox");
|
|
1241
|
+
if (pending?.deliveryId !== void 0 && pending.deliveryId !== deliveryId) {
|
|
1242
|
+
throw new Error("Boss control acknowledgement changed the pending deliveryId");
|
|
1243
|
+
}
|
|
1244
|
+
if (pending) pending.deliveryId = deliveryId;
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
case "delivery_accepted": {
|
|
1248
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1249
|
+
if (typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1250
|
+
throw new Error("Invalid delivery_accepted message");
|
|
1251
|
+
}
|
|
1252
|
+
const pending = this.pendingSends.get(messageId);
|
|
1253
|
+
if (!pending) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
pending.accepted = true;
|
|
1257
|
+
pending.deliveryId = deliveryId;
|
|
1258
|
+
this.emit("delivery_accepted", messageId, deliveryId);
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
case "delivered": {
|
|
1262
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1263
|
+
if (typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1264
|
+
throw new Error("Invalid delivered message");
|
|
1265
|
+
}
|
|
1266
|
+
this.outbox?.remove(messageId);
|
|
1267
|
+
const pending = this.pendingSends.get(messageId);
|
|
1268
|
+
if (!pending) {
|
|
1269
|
+
this.emit("outbox_delivered", messageId, deliveryId);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
this.pendingSends.delete(messageId);
|
|
1273
|
+
pending.resolve({ id: messageId, accepted: true, delivered: true, deliveryId });
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case "delivery_failed": {
|
|
1277
|
+
const { accepted, code, messageId, reason } = brokerMessage;
|
|
1278
|
+
if (typeof accepted !== "boolean" || typeof code !== "string" || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1279
|
+
throw new Error("Invalid delivery_failed message");
|
|
1280
|
+
}
|
|
1281
|
+
this.outbox?.remove(messageId);
|
|
1282
|
+
const pending = this.pendingSends.get(messageId);
|
|
1283
|
+
if (!pending) {
|
|
1284
|
+
this.emit("outbox_failed", messageId, code, reason);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
this.pendingSends.delete(messageId);
|
|
1288
|
+
pending.resolve({
|
|
1289
|
+
id: messageId,
|
|
1290
|
+
accepted,
|
|
1291
|
+
delivered: false,
|
|
1292
|
+
code,
|
|
1293
|
+
reason,
|
|
1294
|
+
...pending.deliveryId ? { deliveryId: pending.deliveryId } : {}
|
|
1295
|
+
});
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
case "ask_deferred": {
|
|
1299
|
+
const { fromSessionId, messageId } = brokerMessage;
|
|
1300
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string") {
|
|
1301
|
+
throw new Error("Invalid ask_deferred message");
|
|
1302
|
+
}
|
|
1303
|
+
this.emit("ask_deferred", messageId, fromSessionId);
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
case "ask_cancelled": {
|
|
1307
|
+
const { fromSessionId, messageId, reason } = brokerMessage;
|
|
1308
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1309
|
+
throw new Error("Invalid ask_cancelled message");
|
|
1310
|
+
}
|
|
1311
|
+
this.emit("ask_cancelled", messageId, fromSessionId, reason);
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
case "ask_control_result": {
|
|
1315
|
+
const { action, applied, messageId, requestId } = brokerMessage;
|
|
1316
|
+
if (action !== "defer" && action !== "cancel" || typeof applied !== "boolean" || typeof messageId !== "string" || typeof requestId !== "string") {
|
|
1317
|
+
throw new Error("Invalid ask_control_result message");
|
|
1318
|
+
}
|
|
1319
|
+
const pending = this.pendingAskControls.get(requestId);
|
|
1320
|
+
if (!pending) return;
|
|
1321
|
+
clearTimeout(pending.timeout);
|
|
1322
|
+
this.pendingAskControls.delete(requestId);
|
|
1323
|
+
pending.resolve(applied);
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
case "session_joined": {
|
|
1327
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1328
|
+
throw new Error("Invalid session_joined message");
|
|
1329
|
+
}
|
|
1330
|
+
this.emit("session_joined", brokerMessage.session);
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
case "session_left": {
|
|
1334
|
+
if (typeof brokerMessage.sessionId !== "string") {
|
|
1335
|
+
throw new Error("Invalid session_left message");
|
|
1336
|
+
}
|
|
1337
|
+
this.emit("session_left", brokerMessage.sessionId);
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
case "presence_update": {
|
|
1341
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1342
|
+
throw new Error("Invalid presence_update message");
|
|
1343
|
+
}
|
|
1344
|
+
this.emit("presence_update", brokerMessage.session);
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
case "error": {
|
|
1348
|
+
if (typeof brokerMessage.code !== "string" || typeof brokerMessage.error !== "string") {
|
|
1349
|
+
throw new Error("Invalid error message");
|
|
1350
|
+
}
|
|
1351
|
+
if (this._sessionId === null) {
|
|
1352
|
+
const error3 = new Error(brokerMessage.error);
|
|
1353
|
+
error3.code = brokerMessage.code;
|
|
1354
|
+
throw error3;
|
|
1355
|
+
}
|
|
1356
|
+
const error2 = new Error(brokerMessage.error);
|
|
1357
|
+
error2.code = brokerMessage.code;
|
|
1358
|
+
this.emit("error", error2);
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
default:
|
|
1362
|
+
throw new Error(`Unknown broker message type: ${brokerMessage.type}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async disconnect(preserveAsks = false) {
|
|
1366
|
+
const socket = this.socket;
|
|
1367
|
+
if (!socket) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
this.disconnecting = true;
|
|
1371
|
+
this.disconnectError = null;
|
|
1372
|
+
this.failPending(new Error("Client disconnected"));
|
|
1373
|
+
if (!preserveAsks) this.outbox?.clear();
|
|
1374
|
+
await new Promise((resolve3) => {
|
|
1375
|
+
let settled = false;
|
|
1376
|
+
const finish = () => {
|
|
1377
|
+
if (settled) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
settled = true;
|
|
1381
|
+
clearTimeout(timeout);
|
|
1382
|
+
socket.off("close", onClose);
|
|
1383
|
+
socket.off("error", onError);
|
|
1384
|
+
resolve3();
|
|
1385
|
+
};
|
|
1386
|
+
const onClose = () => finish();
|
|
1387
|
+
const onError = () => {
|
|
1388
|
+
socket.destroy();
|
|
1389
|
+
};
|
|
1390
|
+
const timeout = setTimeout(() => {
|
|
1391
|
+
socket.destroy();
|
|
1392
|
+
}, 2e3);
|
|
1393
|
+
socket.once("close", onClose);
|
|
1394
|
+
socket.once("error", onError);
|
|
1395
|
+
try {
|
|
1396
|
+
writeMessage(socket, { type: "unregister", ...preserveAsks ? { preserveAsks: true } : {} });
|
|
1397
|
+
socket.end();
|
|
1398
|
+
} catch {
|
|
1399
|
+
socket.destroy();
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
listSessions() {
|
|
1404
|
+
let socket;
|
|
1405
|
+
try {
|
|
1406
|
+
socket = this.requireActiveSocket();
|
|
1407
|
+
} catch (error2) {
|
|
1408
|
+
return Promise.reject(toError(error2));
|
|
1409
|
+
}
|
|
1410
|
+
return new Promise((resolve3, reject) => {
|
|
1411
|
+
const requestId = randomUUID2();
|
|
1412
|
+
const wrappedResolve = (sessions) => {
|
|
1413
|
+
clearTimeout(timeout);
|
|
1414
|
+
resolve3(sessions);
|
|
1415
|
+
};
|
|
1416
|
+
const wrappedReject = (error2) => {
|
|
1417
|
+
clearTimeout(timeout);
|
|
1418
|
+
reject(error2);
|
|
1419
|
+
};
|
|
1420
|
+
const timeout = setTimeout(() => {
|
|
1421
|
+
if (this.pendingLists.has(requestId)) {
|
|
1422
|
+
this.pendingLists.delete(requestId);
|
|
1423
|
+
wrappedReject(new Error("List sessions timeout"));
|
|
1424
|
+
}
|
|
1425
|
+
}, 5e3);
|
|
1426
|
+
this.pendingLists.set(requestId, { resolve: wrappedResolve, reject: wrappedReject });
|
|
1427
|
+
try {
|
|
1428
|
+
writeMessage(socket, { type: "list", requestId });
|
|
1429
|
+
} catch (error2) {
|
|
1430
|
+
clearTimeout(timeout);
|
|
1431
|
+
this.pendingLists.delete(requestId);
|
|
1432
|
+
reject(toError(error2));
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
send(to, options) {
|
|
1437
|
+
let socket;
|
|
1438
|
+
try {
|
|
1439
|
+
socket = this.requireActiveSocket();
|
|
1440
|
+
} catch (error2) {
|
|
1441
|
+
return Promise.reject(toError(error2));
|
|
1442
|
+
}
|
|
1443
|
+
const messageId = options.messageId ?? randomUUID2();
|
|
1444
|
+
if (this.pendingSends.has(messageId)) {
|
|
1445
|
+
return Promise.resolve({
|
|
1446
|
+
id: messageId,
|
|
1447
|
+
accepted: false,
|
|
1448
|
+
delivered: false,
|
|
1449
|
+
code: "DUPLICATE_MESSAGE_ID",
|
|
1450
|
+
reason: `Message ID ${messageId} is already pending`
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
const message = {
|
|
1454
|
+
id: messageId,
|
|
1455
|
+
timestamp: Date.now(),
|
|
1456
|
+
replyTo: options.replyTo,
|
|
1457
|
+
expectsReply: options.expectsReply,
|
|
1458
|
+
content: {
|
|
1459
|
+
text: options.text,
|
|
1460
|
+
attachments: options.attachments
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
try {
|
|
1464
|
+
this.outbox?.enqueue(to, message);
|
|
1465
|
+
} catch (error2) {
|
|
1466
|
+
return Promise.reject(toError(error2));
|
|
1467
|
+
}
|
|
1468
|
+
return new Promise((resolve3, reject) => {
|
|
1469
|
+
const wrappedResolve = (result) => {
|
|
1470
|
+
clearTimeout(timeout);
|
|
1471
|
+
resolve3(result);
|
|
1472
|
+
};
|
|
1473
|
+
const wrappedReject = (error2) => {
|
|
1474
|
+
clearTimeout(timeout);
|
|
1475
|
+
reject(error2);
|
|
1476
|
+
};
|
|
1477
|
+
const timeout = setTimeout(() => {
|
|
1478
|
+
if (this.pendingSends.has(messageId)) {
|
|
1479
|
+
this.pendingSends.delete(messageId);
|
|
1480
|
+
wrappedReject(new Error("Send timeout"));
|
|
1481
|
+
}
|
|
1482
|
+
}, 1e4);
|
|
1483
|
+
this.pendingSends.set(messageId, {
|
|
1484
|
+
accepted: false,
|
|
1485
|
+
resolve: wrappedResolve,
|
|
1486
|
+
reject: wrappedReject
|
|
1487
|
+
});
|
|
1488
|
+
try {
|
|
1489
|
+
writeMessage(socket, { type: "send", to, message });
|
|
1490
|
+
} catch (error2) {
|
|
1491
|
+
clearTimeout(timeout);
|
|
1492
|
+
this.pendingSends.delete(messageId);
|
|
1493
|
+
reject(toError(error2));
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
sendBossControl(to, envelopeValue) {
|
|
1498
|
+
let socket;
|
|
1499
|
+
try {
|
|
1500
|
+
socket = this.requireActiveSocket();
|
|
1501
|
+
} catch (error2) {
|
|
1502
|
+
return Promise.reject(toError(error2));
|
|
1503
|
+
}
|
|
1504
|
+
let envelope;
|
|
1505
|
+
try {
|
|
1506
|
+
envelope = bossControlKind(envelopeValue).envelope;
|
|
1507
|
+
const binding = this._bossBinding?.binding;
|
|
1508
|
+
if (binding === void 0 || binding.state !== "active" || envelope.bossRunId !== binding.bossRunId || envelope.participantId !== binding.participantId || envelope.bindingEpoch !== binding.bindingEpoch) throw new Error("Boss control envelope does not match this client's active participant binding");
|
|
1509
|
+
} catch (error2) {
|
|
1510
|
+
return Promise.reject(toError(error2));
|
|
1511
|
+
}
|
|
1512
|
+
const requestId = envelope.messageId;
|
|
1513
|
+
if (this.pendingBossControls.has(requestId)) {
|
|
1514
|
+
return Promise.resolve({
|
|
1515
|
+
requestId,
|
|
1516
|
+
messageId: envelope.messageId,
|
|
1517
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
1518
|
+
status: "rejected",
|
|
1519
|
+
delivered: false,
|
|
1520
|
+
code: "INVALID_CONTROL",
|
|
1521
|
+
reason: "Boss requestId is already pending"
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
if (!this.bossControlOutbox) throw new Error("Durable Boss control outbox is unavailable");
|
|
1526
|
+
this.bossControlOutbox.enqueue(to, envelope);
|
|
1527
|
+
} catch (error2) {
|
|
1528
|
+
return Promise.reject(toError(error2));
|
|
1529
|
+
}
|
|
1530
|
+
return new Promise((resolve3, reject) => {
|
|
1531
|
+
const timeout = setTimeout(() => {
|
|
1532
|
+
if (!this.pendingBossControls.delete(requestId)) return;
|
|
1533
|
+
reject(new Error("Boss control delivery timeout"));
|
|
1534
|
+
}, 1e4);
|
|
1535
|
+
timeout.unref?.();
|
|
1536
|
+
this.pendingBossControls.set(requestId, {
|
|
1537
|
+
messageId: envelope.messageId,
|
|
1538
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
1539
|
+
resolve: resolve3,
|
|
1540
|
+
reject,
|
|
1541
|
+
timeout
|
|
1542
|
+
});
|
|
1543
|
+
try {
|
|
1544
|
+
writeMessage(socket, { type: "boss_control", requestId, to, envelope });
|
|
1545
|
+
} catch (error2) {
|
|
1546
|
+
clearTimeout(timeout);
|
|
1547
|
+
this.pendingBossControls.delete(requestId);
|
|
1548
|
+
reject(toError(error2));
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
acknowledgeBossControl(deliveryId, messageId, idempotencyKey) {
|
|
1553
|
+
return this.writeControlMessage({ type: "boss_control_received", deliveryId, messageId, idempotencyKey });
|
|
1554
|
+
}
|
|
1555
|
+
acknowledgeMessage(deliveryId) {
|
|
1556
|
+
return this.writeControlMessage({ type: "message_received", deliveryId });
|
|
1557
|
+
}
|
|
1558
|
+
rejectMessage(deliveryId, reason) {
|
|
1559
|
+
return this.writeControlMessage({ type: "message_rejected", deliveryId, code: "CONFLICTING_MESSAGE_ID", reason });
|
|
1560
|
+
}
|
|
1561
|
+
deferAsk(messageId) {
|
|
1562
|
+
return this.sendAskControl("defer", messageId);
|
|
1563
|
+
}
|
|
1564
|
+
cancelAsk(messageId) {
|
|
1565
|
+
return this.sendAskControl("cancel", messageId);
|
|
1566
|
+
}
|
|
1567
|
+
sendAskControl(action, messageId) {
|
|
1568
|
+
const requestId = randomUUID2();
|
|
1569
|
+
return new Promise((resolve3) => {
|
|
1570
|
+
const timeout = setTimeout(() => {
|
|
1571
|
+
this.pendingAskControls.delete(requestId);
|
|
1572
|
+
resolve3(false);
|
|
1573
|
+
}, 2e3);
|
|
1574
|
+
timeout.unref?.();
|
|
1575
|
+
this.pendingAskControls.set(requestId, { resolve: resolve3, timeout });
|
|
1576
|
+
if (!this.writeControlMessage({ type: action === "defer" ? "defer_ask" : "cancel_ask", requestId, messageId })) {
|
|
1577
|
+
clearTimeout(timeout);
|
|
1578
|
+
this.pendingAskControls.delete(requestId);
|
|
1579
|
+
resolve3(false);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
writeControlMessage(message) {
|
|
1584
|
+
if (this.disconnecting) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
const socket = this.socket;
|
|
1588
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
writeMessage(socket, message);
|
|
1593
|
+
return true;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
replayOutbox() {
|
|
1599
|
+
const socket = this.socket;
|
|
1600
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) return;
|
|
1601
|
+
for (const entry of this.outbox?.list() ?? []) {
|
|
1602
|
+
if (this.pendingSends.has(entry.message.id)) continue;
|
|
1603
|
+
try {
|
|
1604
|
+
writeMessage(socket, { type: "send", to: entry.to, message: entry.message });
|
|
1605
|
+
} catch {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
replayBossControlOutbox() {
|
|
1611
|
+
const socket = this.socket;
|
|
1612
|
+
if (!socket || socket.destroyed || !this._sessionId || !this.bossControlOutbox) return;
|
|
1613
|
+
for (const entry of this.bossControlOutbox.list()) {
|
|
1614
|
+
try {
|
|
1615
|
+
writeMessage(socket, {
|
|
1616
|
+
type: "boss_control",
|
|
1617
|
+
requestId: entry.envelope.messageId,
|
|
1618
|
+
to: entry.to,
|
|
1619
|
+
envelope: entry.envelope
|
|
1620
|
+
});
|
|
1621
|
+
} catch {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
updatePresence(updates) {
|
|
1627
|
+
if (this.disconnecting) {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const socket = this.socket;
|
|
1631
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
writeMessage(socket, { type: "presence", ...updates });
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// broker/spawn.ts
|
|
1639
|
+
import { spawn } from "child_process";
|
|
1640
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1641
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1642
|
+
import { fileURLToPath } from "url";
|
|
1643
|
+
import { createRequire } from "module";
|
|
1644
|
+
import net2 from "net";
|
|
1645
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1646
|
+
import { POLICY_SEMANTICS_HASH as POLICY_SEMANTICS_HASH2, POLICY_SEMANTICS_VERSION as POLICY_SEMANTICS_VERSION2 } from "@ctliz/agent-intercom-core";
|
|
1647
|
+
var INTERCOM_DIR = getIntercomDirPath();
|
|
1648
|
+
var EXTENSION_DIR = join4(dirname2(fileURLToPath(import.meta.url)), "..");
|
|
1649
|
+
var BROKER_PID = join4(INTERCOM_DIR, "broker.pid");
|
|
1650
|
+
var BROKER_SPAWN_LOCK = join4(INTERCOM_DIR, "broker.spawn.lock");
|
|
1651
|
+
function sleep(ms) {
|
|
1652
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1653
|
+
}
|
|
1654
|
+
function getBrokerEntryPath(moduleUrl = import.meta.url) {
|
|
1655
|
+
const moduleDir = dirname2(fileURLToPath(moduleUrl));
|
|
1656
|
+
const bundledBroker = join4(moduleDir, "broker.mjs");
|
|
1657
|
+
return existsSync3(bundledBroker) ? bundledBroker : join4(moduleDir, "broker.ts");
|
|
1658
|
+
}
|
|
1659
|
+
function getTsxCliPath(extensionDir = EXTENSION_DIR) {
|
|
1660
|
+
try {
|
|
1661
|
+
const requireFromExtension = createRequire(import.meta.url);
|
|
1662
|
+
const tsxMain = requireFromExtension.resolve("tsx");
|
|
1663
|
+
return join4(dirname2(tsxMain), "cli.mjs");
|
|
1664
|
+
} catch {
|
|
1665
|
+
return join4(extensionDir, "node_modules", "tsx", "dist", "cli.mjs");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function quoteWindowsArg(value) {
|
|
1669
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1670
|
+
}
|
|
1671
|
+
function getWindowsHiddenLauncherPath(intercomDir = INTERCOM_DIR) {
|
|
1672
|
+
return join4(intercomDir, "broker-launch.vbs");
|
|
1673
|
+
}
|
|
1674
|
+
function usesDefaultBrokerCommand(brokerCommand, brokerArgs) {
|
|
1675
|
+
return brokerCommand === "npx" && brokerArgs.length === 2 && brokerArgs[0] === "--no-install" && brokerArgs[1] === "tsx";
|
|
1676
|
+
}
|
|
1677
|
+
function getWindowsBrokerCommandLine(brokerPath, extensionDir = EXTENSION_DIR, nodePath = process.execPath, brokerCommand = "npx", brokerArgs = ["--no-install", "tsx"]) {
|
|
1678
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
1679
|
+
return [quoteWindowsArg(nodePath), quoteWindowsArg(getTsxCliPath(extensionDir)), quoteWindowsArg(brokerPath)].join(" ");
|
|
1680
|
+
}
|
|
1681
|
+
return [quoteWindowsArg(brokerCommand), ...brokerArgs.map(quoteWindowsArg), quoteWindowsArg(brokerPath)].join(" ");
|
|
1682
|
+
}
|
|
1683
|
+
function getWindowsHiddenLauncherScript(commandLine) {
|
|
1684
|
+
return [
|
|
1685
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
1686
|
+
`WshShell.Run "${commandLine.replace(/"/g, '""')}", 0, False`,
|
|
1687
|
+
"Set WshShell = Nothing",
|
|
1688
|
+
""
|
|
1689
|
+
].join("\r\n");
|
|
1690
|
+
}
|
|
1691
|
+
function isBrokerHealthOkMessage(message, requestId) {
|
|
1692
|
+
if (typeof message !== "object" || message === null || !("type" in message)) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
const response = message;
|
|
1696
|
+
if (response.type !== "health_ok" || response.requestId !== requestId || response.protocol !== INTERCOM_PROTOCOL_NAME2 || response.version !== INTERCOM_PROTOCOL_VERSION2 || response.endpoint !== "local") return false;
|
|
1697
|
+
const remoteAccess = response.remoteAccess;
|
|
1698
|
+
if (typeof remoteAccess !== "object" || remoteAccess === null || Array.isArray(remoteAccess)) return false;
|
|
1699
|
+
const contract = remoteAccess;
|
|
1700
|
+
return contract.feature === "remote-access-v1" && contract.policySemanticsVersion === POLICY_SEMANTICS_VERSION2 && contract.policySemanticsHash === POLICY_SEMANTICS_HASH2;
|
|
1701
|
+
}
|
|
1702
|
+
function writeWindowsHiddenLauncher(commandLine, launcherPath = getWindowsHiddenLauncherPath()) {
|
|
1703
|
+
ensureIntercomRuntimeDir(dirname2(launcherPath));
|
|
1704
|
+
writeFileSync2(launcherPath, getWindowsHiddenLauncherScript(commandLine), {
|
|
1705
|
+
encoding: "utf-8",
|
|
1706
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
1707
|
+
});
|
|
1708
|
+
restrictIntercomRuntimeFile(launcherPath);
|
|
1709
|
+
return launcherPath;
|
|
1710
|
+
}
|
|
1711
|
+
function getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs, extensionDir = EXTENSION_DIR, platform = process.platform, intercomDir = INTERCOM_DIR, nodePath = process.execPath) {
|
|
1712
|
+
if (platform === "win32") {
|
|
1713
|
+
const launcherPath = getWindowsHiddenLauncherPath(intercomDir);
|
|
1714
|
+
return {
|
|
1715
|
+
kind: "windows-launcher",
|
|
1716
|
+
command: "wscript.exe",
|
|
1717
|
+
args: [launcherPath],
|
|
1718
|
+
launcherPath,
|
|
1719
|
+
launcherCommandLine: getWindowsBrokerCommandLine(brokerPath, extensionDir, nodePath, brokerCommand, brokerArgs)
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
1723
|
+
return {
|
|
1724
|
+
kind: "direct",
|
|
1725
|
+
command: nodePath,
|
|
1726
|
+
args: [getTsxCliPath(extensionDir), brokerPath]
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
return {
|
|
1730
|
+
kind: "direct",
|
|
1731
|
+
command: brokerCommand,
|
|
1732
|
+
args: [...brokerArgs, brokerPath]
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env) {
|
|
1736
|
+
return {
|
|
1737
|
+
detached: true,
|
|
1738
|
+
stdio: "ignore",
|
|
1739
|
+
cwd: extensionDir,
|
|
1740
|
+
env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
|
|
1741
|
+
windowsHide: true
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function toError2(error2) {
|
|
1745
|
+
return error2 instanceof Error ? error2 : new Error(String(error2));
|
|
1746
|
+
}
|
|
1747
|
+
async function spawnBrokerIfNeeded(brokerCommand, brokerArgs) {
|
|
1748
|
+
ensureIntercomRuntimeDir(INTERCOM_DIR);
|
|
1749
|
+
if (await isBrokerRunning()) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const ownsLock = acquireSpawnLock();
|
|
1753
|
+
if (!ownsLock) {
|
|
1754
|
+
await waitForBroker();
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
if (await isBrokerRunning()) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (await checkBrokerHealth() === "incompatible") {
|
|
1762
|
+
throw new Error(`Incompatible live intercom broker; expected ${INTERCOM_PROTOCOL_NAME2} v${INTERCOM_PROTOCOL_VERSION2}. Stop it explicitly during a coordinated migration.`);
|
|
1763
|
+
}
|
|
1764
|
+
const brokerPath = getBrokerEntryPath();
|
|
1765
|
+
const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
|
|
1766
|
+
if (launch.kind === "windows-launcher") {
|
|
1767
|
+
writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
|
|
1768
|
+
}
|
|
1769
|
+
const child = spawn(launch.command, launch.args, getBrokerSpawnOptions());
|
|
1770
|
+
child.unref();
|
|
1771
|
+
await new Promise((resolve3, reject) => {
|
|
1772
|
+
const cleanup = () => {
|
|
1773
|
+
child.off("error", onError);
|
|
1774
|
+
child.off("exit", onExit);
|
|
1775
|
+
};
|
|
1776
|
+
const onError = (error2) => {
|
|
1777
|
+
cleanup();
|
|
1778
|
+
reject(new Error(`Failed to spawn intercom broker: ${error2.message}`, { cause: error2 }));
|
|
1779
|
+
};
|
|
1780
|
+
const onExit = (code, signal) => {
|
|
1781
|
+
if (launch.kind === "windows-launcher" && code === 0 && signal === null) {
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
cleanup();
|
|
1785
|
+
if (signal) {
|
|
1786
|
+
reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
|
|
1790
|
+
};
|
|
1791
|
+
child.once("error", onError);
|
|
1792
|
+
child.once("exit", onExit);
|
|
1793
|
+
waitForBroker().then(() => {
|
|
1794
|
+
cleanup();
|
|
1795
|
+
resolve3();
|
|
1796
|
+
}, (error2) => {
|
|
1797
|
+
cleanup();
|
|
1798
|
+
reject(toError2(error2));
|
|
1799
|
+
});
|
|
1800
|
+
});
|
|
1801
|
+
} finally {
|
|
1802
|
+
releaseSpawnLock();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async function isBrokerRunning() {
|
|
1806
|
+
if (await checkSocketConnectable()) {
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
if (!existsSync3(BROKER_PID)) return false;
|
|
1810
|
+
try {
|
|
1811
|
+
const pid = parseInt(readFileSync5(BROKER_PID, "utf-8").trim(), 10);
|
|
1812
|
+
if (!Number.isFinite(pid)) return false;
|
|
1813
|
+
process.kill(pid, 0);
|
|
1814
|
+
return checkSocketConnectable();
|
|
1815
|
+
} catch {
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function connectToBrokerTarget2(target) {
|
|
1820
|
+
return typeof target === "string" ? net2.connect(target) : net2.connect({ host: target.host, port: target.port });
|
|
1821
|
+
}
|
|
1822
|
+
async function checkSocketConnectable() {
|
|
1823
|
+
return await checkBrokerHealth() === "compatible";
|
|
1824
|
+
}
|
|
1825
|
+
function checkBrokerHealth() {
|
|
1826
|
+
return new Promise((resolve3) => {
|
|
1827
|
+
let target;
|
|
1828
|
+
try {
|
|
1829
|
+
target = getBrokerConnectTarget();
|
|
1830
|
+
} catch {
|
|
1831
|
+
resolve3("unreachable");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const socket = connectToBrokerTarget2(target);
|
|
1835
|
+
const requestId = randomUUID3();
|
|
1836
|
+
const expectedStateId = typeof target === "string" ? void 0 : target.stateId;
|
|
1837
|
+
let settled = false;
|
|
1838
|
+
const finish = (health) => {
|
|
1839
|
+
if (settled) {
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
settled = true;
|
|
1843
|
+
clearTimeout(timeout);
|
|
1844
|
+
socket.off("connect", onConnect);
|
|
1845
|
+
socket.off("error", onError);
|
|
1846
|
+
socket.off("data", reader);
|
|
1847
|
+
socket.destroy();
|
|
1848
|
+
resolve3(health);
|
|
1849
|
+
};
|
|
1850
|
+
const onConnect = () => {
|
|
1851
|
+
try {
|
|
1852
|
+
writeMessage(socket, {
|
|
1853
|
+
type: "health",
|
|
1854
|
+
requestId,
|
|
1855
|
+
...expectedStateId ? { stateId: expectedStateId } : {}
|
|
1856
|
+
});
|
|
1857
|
+
} catch {
|
|
1858
|
+
finish("unreachable");
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
const onError = () => finish("unreachable");
|
|
1862
|
+
const reader = createMessageReader((message) => {
|
|
1863
|
+
if (isBrokerHealthOkMessage(message, requestId)) {
|
|
1864
|
+
finish("compatible");
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (typeof message === "object" && message !== null && "type" in message && message.type === "health_ok" && "requestId" in message && message.requestId === requestId) {
|
|
1868
|
+
finish("incompatible");
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
finish("unreachable");
|
|
1872
|
+
}, () => finish("unreachable"));
|
|
1873
|
+
socket.on("connect", onConnect);
|
|
1874
|
+
socket.on("error", onError);
|
|
1875
|
+
socket.on("data", reader);
|
|
1876
|
+
const timeout = setTimeout(() => finish("unreachable"), 1e3);
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
function acquireSpawnLock() {
|
|
1880
|
+
const maxRetries = 5;
|
|
1881
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1882
|
+
try {
|
|
1883
|
+
writeFileSync2(BROKER_SPAWN_LOCK, `${process.pid}
|
|
1884
|
+
${Date.now()}
|
|
1885
|
+
`, {
|
|
1886
|
+
flag: "wx",
|
|
1887
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
1888
|
+
});
|
|
1889
|
+
restrictIntercomRuntimeFile(BROKER_SPAWN_LOCK);
|
|
1890
|
+
return true;
|
|
1891
|
+
} catch (error2) {
|
|
1892
|
+
if (!(error2 instanceof Error) || error2.code !== "EEXIST") {
|
|
1893
|
+
throw error2;
|
|
1894
|
+
}
|
|
1895
|
+
if (isSpawnLockStale()) {
|
|
1896
|
+
try {
|
|
1897
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
function isSpawnLockStale() {
|
|
1908
|
+
if (!existsSync3(BROKER_SPAWN_LOCK)) {
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
const [pidLine = "", createdAtLine = "0"] = readFileSync5(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
|
|
1913
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
1914
|
+
const createdAt = Number.parseInt(createdAtLine, 10);
|
|
1915
|
+
const ageMs = Date.now() - createdAt;
|
|
1916
|
+
if (Number.isFinite(pid)) {
|
|
1917
|
+
try {
|
|
1918
|
+
process.kill(pid, 0);
|
|
1919
|
+
} catch {
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return !Number.isFinite(createdAt) || ageMs > 1e4;
|
|
1924
|
+
} catch {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function releaseSpawnLock() {
|
|
1929
|
+
try {
|
|
1930
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
1931
|
+
} catch {
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
async function waitForBroker(timeoutMs = 5e3) {
|
|
1935
|
+
const start = Date.now();
|
|
1936
|
+
while (Date.now() - start < timeoutMs) {
|
|
1937
|
+
if (await checkSocketConnectable()) {
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
await sleep(100);
|
|
1941
|
+
}
|
|
1942
|
+
throw new Error("Broker failed to start within timeout");
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// config.ts
|
|
1946
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
1947
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
1948
|
+
import { homedir as homedir2 } from "os";
|
|
1949
|
+
var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
|
|
1950
|
+
var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
|
|
1951
|
+
function validateAskTimeoutMs(value, name = "timeout_ms") {
|
|
1952
|
+
if (!Number.isSafeInteger(value) || typeof value !== "number" || value <= 0) {
|
|
1953
|
+
throw new Error(`${name} must be a positive integer number of milliseconds`);
|
|
1954
|
+
}
|
|
1955
|
+
if (value > MAX_ASK_TIMEOUT_MS) {
|
|
1956
|
+
throw new Error(`${name} must be ${MAX_ASK_TIMEOUT_MS} ms or less; use intercom_send plus intercom_pending for longer-running work`);
|
|
1957
|
+
}
|
|
1958
|
+
return value;
|
|
1959
|
+
}
|
|
1960
|
+
function getAskTimeoutMs() {
|
|
1961
|
+
const raw = process.env.PI_INTERCOM_ASK_TIMEOUT_MS;
|
|
1962
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1963
|
+
return DEFAULT_ASK_TIMEOUT_MS;
|
|
1964
|
+
}
|
|
1965
|
+
const value = Number(raw);
|
|
1966
|
+
return validateAskTimeoutMs(value, "PI_INTERCOM_ASK_TIMEOUT_MS");
|
|
1967
|
+
}
|
|
1968
|
+
function getConfigPath() {
|
|
1969
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ? resolve2(process.env.PI_CODING_AGENT_DIR) : join5(homedir2(), ".pi", "agent");
|
|
1970
|
+
return join5(agentDir, "intercom", "config.json");
|
|
1971
|
+
}
|
|
1972
|
+
var defaults = {
|
|
1973
|
+
brokerCommand: "npx",
|
|
1974
|
+
brokerArgs: ["--no-install", "tsx"],
|
|
1975
|
+
confirmSend: false,
|
|
1976
|
+
enabled: true,
|
|
1977
|
+
replyHint: true,
|
|
1978
|
+
inboundForkHandlers: {
|
|
1979
|
+
enabled: true,
|
|
1980
|
+
when: "auto",
|
|
1981
|
+
notify: "summary",
|
|
1982
|
+
triggerParentOnSummary: "auto"
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
function loadConfig() {
|
|
1986
|
+
const configPath = getConfigPath();
|
|
1987
|
+
if (!existsSync4(configPath)) {
|
|
1988
|
+
return { ...defaults };
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
1992
|
+
const parsed = JSON.parse(raw);
|
|
1993
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1994
|
+
throw new Error("Config must be a JSON object");
|
|
1995
|
+
}
|
|
1996
|
+
const parsedConfig = parsed;
|
|
1997
|
+
const config = { ...defaults };
|
|
1998
|
+
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
|
|
1999
|
+
if (typeof parsedConfig.brokerCommand !== "string") {
|
|
2000
|
+
throw new Error(`"brokerCommand" must be a string`);
|
|
2001
|
+
}
|
|
2002
|
+
const brokerCommand = parsedConfig.brokerCommand.trim();
|
|
2003
|
+
if (!brokerCommand) {
|
|
2004
|
+
throw new Error(`"brokerCommand" must not be empty`);
|
|
2005
|
+
}
|
|
2006
|
+
config.brokerCommand = brokerCommand;
|
|
2007
|
+
}
|
|
2008
|
+
if (Object.hasOwn(parsedConfig, "brokerArgs")) {
|
|
2009
|
+
if (!Array.isArray(parsedConfig.brokerArgs)) {
|
|
2010
|
+
throw new Error(`"brokerArgs" must be an array`);
|
|
2011
|
+
}
|
|
2012
|
+
const brokerArgs = [];
|
|
2013
|
+
for (const arg of parsedConfig.brokerArgs) {
|
|
2014
|
+
if (typeof arg !== "string") {
|
|
2015
|
+
throw new Error(`"brokerArgs" items must be strings`);
|
|
2016
|
+
}
|
|
2017
|
+
brokerArgs.push(arg);
|
|
2018
|
+
}
|
|
2019
|
+
config.brokerArgs = brokerArgs;
|
|
2020
|
+
}
|
|
2021
|
+
if (Object.hasOwn(parsedConfig, "confirmSend")) {
|
|
2022
|
+
if (typeof parsedConfig.confirmSend !== "boolean") {
|
|
2023
|
+
throw new Error(`"confirmSend" must be a boolean`);
|
|
2024
|
+
}
|
|
2025
|
+
config.confirmSend = parsedConfig.confirmSend;
|
|
2026
|
+
}
|
|
2027
|
+
if (Object.hasOwn(parsedConfig, "enabled")) {
|
|
2028
|
+
if (typeof parsedConfig.enabled !== "boolean") {
|
|
2029
|
+
throw new Error(`"enabled" must be a boolean`);
|
|
2030
|
+
}
|
|
2031
|
+
config.enabled = parsedConfig.enabled;
|
|
2032
|
+
}
|
|
2033
|
+
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
2034
|
+
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
2035
|
+
throw new Error(`"replyHint" must be a boolean`);
|
|
2036
|
+
}
|
|
2037
|
+
config.replyHint = parsedConfig.replyHint;
|
|
2038
|
+
}
|
|
2039
|
+
if (Object.hasOwn(parsedConfig, "status")) {
|
|
2040
|
+
if (typeof parsedConfig.status !== "string") {
|
|
2041
|
+
throw new Error(`"status" must be a string`);
|
|
2042
|
+
}
|
|
2043
|
+
config.status = parsedConfig.status;
|
|
2044
|
+
}
|
|
2045
|
+
if (Object.hasOwn(parsedConfig, "inboundForkHandlers")) {
|
|
2046
|
+
if (typeof parsedConfig.inboundForkHandlers !== "object" || parsedConfig.inboundForkHandlers === null || Array.isArray(parsedConfig.inboundForkHandlers)) {
|
|
2047
|
+
throw new Error(`"inboundForkHandlers" must be an object`);
|
|
2048
|
+
}
|
|
2049
|
+
const forkConfig = parsedConfig.inboundForkHandlers;
|
|
2050
|
+
config.inboundForkHandlers = { ...defaults.inboundForkHandlers };
|
|
2051
|
+
if (Object.hasOwn(forkConfig, "enabled")) {
|
|
2052
|
+
if (typeof forkConfig.enabled !== "boolean") throw new Error(`"inboundForkHandlers.enabled" must be a boolean`);
|
|
2053
|
+
config.inboundForkHandlers.enabled = forkConfig.enabled;
|
|
2054
|
+
}
|
|
2055
|
+
if (Object.hasOwn(forkConfig, "when")) {
|
|
2056
|
+
if (forkConfig.when !== "auto" && forkConfig.when !== "busy" && forkConfig.when !== "always") throw new Error(`"inboundForkHandlers.when" must be "auto", "busy", or "always"`);
|
|
2057
|
+
config.inboundForkHandlers.when = forkConfig.when;
|
|
2058
|
+
}
|
|
2059
|
+
if (Object.hasOwn(forkConfig, "notify")) {
|
|
2060
|
+
if (forkConfig.notify !== "ack-and-summary" && forkConfig.notify !== "summary" && forkConfig.notify !== "none") throw new Error(`"inboundForkHandlers.notify" must be "ack-and-summary", "summary", or "none"`);
|
|
2061
|
+
config.inboundForkHandlers.notify = forkConfig.notify;
|
|
2062
|
+
}
|
|
2063
|
+
if (Object.hasOwn(forkConfig, "piCommand")) {
|
|
2064
|
+
if (typeof forkConfig.piCommand !== "string") throw new Error(`"inboundForkHandlers.piCommand" must be a string`);
|
|
2065
|
+
const piCommand = forkConfig.piCommand.trim();
|
|
2066
|
+
if (piCommand) config.inboundForkHandlers.piCommand = piCommand;
|
|
2067
|
+
}
|
|
2068
|
+
if (Object.hasOwn(forkConfig, "triggerParentOnSummary")) {
|
|
2069
|
+
const triggerParentOnSummary = forkConfig.triggerParentOnSummary;
|
|
2070
|
+
if (typeof triggerParentOnSummary !== "boolean" && triggerParentOnSummary !== "auto") {
|
|
2071
|
+
throw new Error(`"inboundForkHandlers.triggerParentOnSummary" must be a boolean or "auto"`);
|
|
2072
|
+
}
|
|
2073
|
+
config.inboundForkHandlers.triggerParentOnSummary = triggerParentOnSummary;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return config;
|
|
2077
|
+
} catch (error2) {
|
|
2078
|
+
console.error(`Failed to load intercom config at ${configPath}:`, error2);
|
|
2079
|
+
return { ...defaults };
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// codex/team.ts
|
|
2084
|
+
import { readFile } from "node:fs/promises";
|
|
2085
|
+
import { join as join6 } from "node:path";
|
|
2086
|
+
import {
|
|
2087
|
+
BOSS_PARTICIPANT_ROLES,
|
|
2088
|
+
parseParticipantState as parseParticipantState2,
|
|
2089
|
+
parseWorkerIdentityV2 as parseWorkerIdentityV22,
|
|
2090
|
+
workerIdentityFromEnvironment
|
|
2091
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
2092
|
+
var LEGACY_LIVE_STATES = /* @__PURE__ */ new Set(["provisioning", "running", "idle", "needs_attention", "stopping"]);
|
|
2093
|
+
var CANONICAL_LIVE_STATES = /* @__PURE__ */ new Set(["provisioning", "registering", "ready", "working", "waiting", "paused", "stalled", "blocked", "unreachable"]);
|
|
2094
|
+
var stringValue = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2095
|
+
var connectedTo = (sessions, target) => {
|
|
2096
|
+
const normalized = target.toLowerCase();
|
|
2097
|
+
return sessions.some((session) => session.id === target || session.name?.toLowerCase() === normalized);
|
|
2098
|
+
};
|
|
2099
|
+
function bossIdentityFromEnvironment(env) {
|
|
2100
|
+
const bossKeys = ["AGENT_INTERCOM_BOSS_RUN_ID", "AGENT_INTERCOM_PARTICIPANT_ID", "AGENT_INTERCOM_BINDING_EPOCH"];
|
|
2101
|
+
if (!bossKeys.some((key) => env[key] !== void 0)) return void 0;
|
|
2102
|
+
const identity = workerIdentityFromEnvironment(env);
|
|
2103
|
+
if (!("bossRunId" in identity)) throw new Error("Incomplete Boss worker identity cannot discover a team");
|
|
2104
|
+
return identity;
|
|
2105
|
+
}
|
|
2106
|
+
function canonicalWorker(value) {
|
|
2107
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("worker must be an object");
|
|
2108
|
+
const worker = value;
|
|
2109
|
+
const identity = parseWorkerIdentityV22({
|
|
2110
|
+
version: "orc.worker-identity.v2",
|
|
2111
|
+
workerId: worker.id,
|
|
2112
|
+
workerIncarnationId: worker.workerIncarnationId,
|
|
2113
|
+
workerGeneration: worker.workerGeneration,
|
|
2114
|
+
...worker.bossRunId === void 0 ? {} : { bossRunId: worker.bossRunId },
|
|
2115
|
+
...worker.participantId === void 0 ? {} : { participantId: worker.participantId },
|
|
2116
|
+
...worker.bindingEpoch === void 0 ? {} : { bindingEpoch: worker.bindingEpoch }
|
|
2117
|
+
});
|
|
2118
|
+
parseParticipantState2(worker.state, "$.worker.state");
|
|
2119
|
+
if (typeof worker.role !== "string" || !BOSS_PARTICIPANT_ROLES.includes(worker.role)) {
|
|
2120
|
+
throw new Error("worker role is not canonical");
|
|
2121
|
+
}
|
|
2122
|
+
if (worker.owned !== true || !stringValue(worker.managerSessionId) || !stringValue(worker.intercomTarget)) {
|
|
2123
|
+
throw new Error("canonical worker ownership routing is incomplete");
|
|
2124
|
+
}
|
|
2125
|
+
return { ...worker, canonicalIdentity: identity };
|
|
2126
|
+
}
|
|
2127
|
+
function exactBossRosterSession(sessions, worker) {
|
|
2128
|
+
const identity = worker.canonicalIdentity;
|
|
2129
|
+
const target = stringValue(worker.intercomTarget);
|
|
2130
|
+
const role = stringValue(worker.role);
|
|
2131
|
+
const state = stringValue(worker.state);
|
|
2132
|
+
if (!identity || !("bossRunId" in identity) || !target || !role || !state) return void 0;
|
|
2133
|
+
const matches = sessions.filter((candidate) => candidate.id === target);
|
|
2134
|
+
if (matches.length !== 1) return void 0;
|
|
2135
|
+
const [session] = matches;
|
|
2136
|
+
if (!session?.boss?.binding || session.boss.workerIdentity === void 0 || session.boss.participantState === void 0) return void 0;
|
|
2137
|
+
try {
|
|
2138
|
+
const sessionIdentity = parseWorkerIdentityV22(session.boss.workerIdentity);
|
|
2139
|
+
const sessionState = parseParticipantState2(session.boss.participantState, "$.session.boss.participantState");
|
|
2140
|
+
const binding = session.boss.binding;
|
|
2141
|
+
return "bossRunId" in sessionIdentity && session.id === target && binding.sessionId === session.id && binding.state === "active" && binding.bossRunId === identity.bossRunId && binding.participantId === identity.participantId && binding.bindingEpoch === identity.bindingEpoch && binding.role === role && sessionIdentity.workerId === identity.workerId && sessionIdentity.workerIncarnationId === identity.workerIncarnationId && sessionIdentity.workerGeneration === identity.workerGeneration && sessionIdentity.bossRunId === identity.bossRunId && sessionIdentity.participantId === identity.participantId && sessionIdentity.bindingEpoch === identity.bindingEpoch && sessionState === state ? session : void 0;
|
|
2142
|
+
} catch {
|
|
2143
|
+
return void 0;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
async function readWorkers(agentDir) {
|
|
2147
|
+
try {
|
|
2148
|
+
const parsed = JSON.parse(await readFile(join6(agentDir, "intercom", "orchestrator", "workers.json"), "utf8"));
|
|
2149
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("worker snapshot must be an object");
|
|
2150
|
+
const snapshot = parsed;
|
|
2151
|
+
if (snapshot.version !== 1 && snapshot.version !== 2 || !Array.isArray(snapshot.workers)) throw new Error("unsupported worker snapshot version");
|
|
2152
|
+
if (snapshot.version === 1) return { version: 1, workers: snapshot.workers };
|
|
2153
|
+
return { version: 2, workers: snapshot.workers.map(canonicalWorker) };
|
|
2154
|
+
} catch {
|
|
2155
|
+
return { version: 1, workers: [] };
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async function resolveIntercomTeam(input) {
|
|
2159
|
+
const env = input.env ?? process.env;
|
|
2160
|
+
const snapshot = await readWorkers(input.agentDir ?? getAgentDirPath());
|
|
2161
|
+
const workers = snapshot.workers;
|
|
2162
|
+
const workerId = stringValue(env.AGENT_INTERCOM_WORKER_ID);
|
|
2163
|
+
const bossIdentity = bossIdentityFromEnvironment(env);
|
|
2164
|
+
const runId = stringValue(env.AGENT_INTERCOM_RUN_ID);
|
|
2165
|
+
const currentMatches = workerId ? workers.filter((worker) => stringValue(worker.id) === workerId && (bossIdentity === void 0 ? !runId || stringValue(worker.runId) === runId : snapshot.version === 2 && worker.canonicalIdentity?.workerId === bossIdentity.workerId && worker.canonicalIdentity.workerIncarnationId === bossIdentity.workerIncarnationId && worker.canonicalIdentity.workerGeneration === bossIdentity.workerGeneration && "bossRunId" in worker.canonicalIdentity && "bossRunId" in bossIdentity && worker.canonicalIdentity.bossRunId === bossIdentity.bossRunId && worker.canonicalIdentity.participantId === bossIdentity.participantId && worker.canonicalIdentity.bindingEpoch === bossIdentity.bindingEpoch)) : [];
|
|
2166
|
+
const current = bossIdentity === void 0 ? currentMatches[0] : currentMatches.length === 1 ? currentMatches[0] : void 0;
|
|
2167
|
+
const currentTarget = stringValue(current?.intercomTarget);
|
|
2168
|
+
const exactCurrentProjection = current !== void 0 && currentTarget === input.selfId && workers.filter((worker) => stringValue(worker.id) === workerId).length === 1 && workers.filter((worker) => stringValue(worker.intercomTarget) === currentTarget).length === 1 && exactBossRosterSession(input.sessions, current) !== void 0;
|
|
2169
|
+
if (bossIdentity !== void 0 && !exactCurrentProjection) {
|
|
2170
|
+
return { self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: false }, coworkers: [] };
|
|
2171
|
+
}
|
|
2172
|
+
const managerTarget = stringValue(current?.managerSessionId) ?? (bossIdentity === void 0 ? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID) : void 0);
|
|
2173
|
+
const teamId = managerTarget ?? input.selfId;
|
|
2174
|
+
const currentRole = stringValue(current?.role);
|
|
2175
|
+
const canDiscoverOwnedRoster = bossIdentity === void 0 || currentRole === "manager" || currentRole === "controller";
|
|
2176
|
+
const coworkers = (canDiscoverOwnedRoster ? workers : []).filter((worker) => worker.owned === true).filter((worker) => bossIdentity === void 0 || snapshot.version === 2 && worker.canonicalIdentity !== void 0 && "bossRunId" in worker.canonicalIdentity && "bossRunId" in bossIdentity && worker.canonicalIdentity.bossRunId === bossIdentity.bossRunId).filter((worker) => stringValue(worker.managerSessionId) === teamId).filter((worker) => stringValue(worker.intercomTarget) !== managerTarget).filter((worker) => (snapshot.version === 2 ? CANONICAL_LIVE_STATES : LEGACY_LIVE_STATES).has(stringValue(worker.state) ?? "")).filter((worker) => stringValue(worker.id) !== workerId).map((worker) => {
|
|
2177
|
+
const id = stringValue(worker.id);
|
|
2178
|
+
if (!id) return void 0;
|
|
2179
|
+
const target = stringValue(worker.intercomTarget) ?? id;
|
|
2180
|
+
const connected = bossIdentity === void 0 ? connectedTo(input.sessions, target) : exactBossRosterSession(input.sessions, worker) !== void 0;
|
|
2181
|
+
if (!connected) return void 0;
|
|
2182
|
+
return {
|
|
2183
|
+
id,
|
|
2184
|
+
target,
|
|
2185
|
+
...stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {},
|
|
2186
|
+
...stringValue(worker.role) ? { role: stringValue(worker.role) } : {},
|
|
2187
|
+
...stringValue(worker.state) ? { state: stringValue(worker.state) } : {},
|
|
2188
|
+
connected
|
|
2189
|
+
};
|
|
2190
|
+
}).filter((member) => Boolean(member));
|
|
2191
|
+
const managerWorker = managerTarget === void 0 ? void 0 : workers.find((worker) => stringValue(worker.intercomTarget) === managerTarget && (bossIdentity === void 0 || snapshot.version === 2 && stringValue(worker.role) === "manager" && worker.canonicalIdentity !== void 0 && "bossRunId" in worker.canonicalIdentity && "bossRunId" in bossIdentity && worker.canonicalIdentity.bossRunId === bossIdentity.bossRunId));
|
|
2192
|
+
const managerConnected = managerTarget === void 0 ? true : bossIdentity === void 0 ? connectedTo(input.sessions, managerTarget) : managerWorker !== void 0 && exactBossRosterSession(input.sessions, managerWorker) !== void 0;
|
|
2193
|
+
return {
|
|
2194
|
+
teamId,
|
|
2195
|
+
self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: bossIdentity === void 0 && !managerTarget },
|
|
2196
|
+
...managerTarget ? { manager: { target: managerTarget, connected: managerConnected } } : bossIdentity === void 0 ? { manager: { target: input.selfId, connected: true } } : {},
|
|
2197
|
+
coworkers
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
function formatIntercomTeam(team) {
|
|
2201
|
+
const lines = [
|
|
2202
|
+
`Manager: ${team.manager ? `${team.manager.target} [${team.manager.connected ? "connected" : "not connected"}]` : "unknown"}`,
|
|
2203
|
+
`You: ${team.self.workerId ?? team.self.id}${team.self.isManager ? " [manager]" : ""}`
|
|
2204
|
+
];
|
|
2205
|
+
if (!team.coworkers.length) lines.push("Coworkers: none");
|
|
2206
|
+
else {
|
|
2207
|
+
lines.push("Coworkers:");
|
|
2208
|
+
for (const coworker of team.coworkers) {
|
|
2209
|
+
const metadata = [coworker.harness, coworker.role, coworker.state].filter(Boolean).join(", ");
|
|
2210
|
+
lines.push(`- ${coworker.id} target=${coworker.target}${metadata ? ` (${metadata})` : ""} [${coworker.connected ? "connected" : "not connected"}]`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return lines.join("\n");
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// codex/runtime.ts
|
|
2217
|
+
function matchesPendingSender(entry, to) {
|
|
2218
|
+
return entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to);
|
|
2219
|
+
}
|
|
2220
|
+
function selectPendingAsk(entries, to, which) {
|
|
2221
|
+
const sorted = [...entries].sort((a, b) => a.receivedAt - b.receivedAt);
|
|
2222
|
+
if (sorted.length === 0) throw new Error("No matching pending ask. Call intercom_pending to inspect unresolved asks.");
|
|
2223
|
+
const matches = to ? sorted.filter((entry) => matchesPendingSender(entry, to)) : sorted;
|
|
2224
|
+
if (matches.length === 0) throw new Error(`No pending ask from "${to}".`);
|
|
2225
|
+
if (matches.length === 1) return matches[0];
|
|
2226
|
+
if (!to && new Set(matches.map((entry) => entry.from.id)).size > 1) {
|
|
2227
|
+
throw new Error("Multiple pending asks \u2014 specify `to` using a sender from intercom_pending.");
|
|
2228
|
+
}
|
|
2229
|
+
if (!which) {
|
|
2230
|
+
const sender = to ? ` from "${to}"` : "";
|
|
2231
|
+
throw new Error(`Multiple pending asks${sender} \u2014 specify \`which\` as \`oldest\` or \`latest\`.`);
|
|
2232
|
+
}
|
|
2233
|
+
return which === "oldest" ? matches[0] : matches[matches.length - 1];
|
|
2234
|
+
}
|
|
2235
|
+
function pendingSelector(entries, entry) {
|
|
2236
|
+
const sameSender = entries.filter((candidate) => candidate.from.id === entry.from.id);
|
|
2237
|
+
if (sameSender.length <= 1) return void 0;
|
|
2238
|
+
const index = sameSender.findIndex((candidate) => candidate.message.id === entry.message.id);
|
|
2239
|
+
if (index === 0) return "oldest";
|
|
2240
|
+
if (index === sameSender.length - 1) return "latest";
|
|
2241
|
+
return "queued";
|
|
2242
|
+
}
|
|
2243
|
+
function publicPendingEntry(entry, selector) {
|
|
2244
|
+
return {
|
|
2245
|
+
from: {
|
|
2246
|
+
id: entry.from.id,
|
|
2247
|
+
name: entry.from.name,
|
|
2248
|
+
origin: entry.from.origin ?? "local",
|
|
2249
|
+
...entry.from.remoteHostId ? { remote_host_id: entry.from.remoteHostId } : {},
|
|
2250
|
+
...entry.from.parentSessionId ? { parent_session_id: entry.from.parentSessionId } : {},
|
|
2251
|
+
...entry.from.generation ? { generation: entry.from.generation } : {}
|
|
2252
|
+
},
|
|
2253
|
+
received_at: entry.receivedAt,
|
|
2254
|
+
read: entry.read,
|
|
2255
|
+
text: entry.message.content.text,
|
|
2256
|
+
attachments: entry.message.content.attachments,
|
|
2257
|
+
expects_reply: entry.message.expectsReply,
|
|
2258
|
+
...selector ? { selector } : {}
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function shortHash(value) {
|
|
2262
|
+
return createHash3("sha256").update(value).digest("hex").slice(0, 8);
|
|
2263
|
+
}
|
|
2264
|
+
function buildCodexRuntimeIdentity(env = process.env, cwd = env.PWD || processCwd(), pid = process.pid) {
|
|
2265
|
+
const sessionId = env.CODEX_INTERCOM_SESSION_ID?.trim() || env.CODEX_PEER_ID?.trim() || `codex-${pid}-${shortHash(cwd)}`;
|
|
2266
|
+
const cwdName = basename(cwd) || "workspace";
|
|
2267
|
+
const name = env.CODEX_INTERCOM_NAME?.trim() || env.CODEX_PEER_NAME?.trim() || `codex-${cwdName}-${pid}`;
|
|
2268
|
+
return {
|
|
2269
|
+
sessionId,
|
|
2270
|
+
name,
|
|
2271
|
+
cwd,
|
|
2272
|
+
model: env.CODEX_INTERCOM_MODEL?.trim() || env.CODEX_MODEL?.trim() || "codex",
|
|
2273
|
+
startedAt: Date.now()
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function formatAttachments(attachments) {
|
|
2277
|
+
if (!attachments?.length) return "";
|
|
2278
|
+
return attachments.map((attachment) => {
|
|
2279
|
+
if (attachment.language) {
|
|
2280
|
+
return `
|
|
2281
|
+
|
|
2282
|
+
---
|
|
2283
|
+
Attachment: ${attachment.name}
|
|
2284
|
+
~~~${attachment.language}
|
|
2285
|
+
${attachment.content}
|
|
2286
|
+
~~~`;
|
|
2287
|
+
}
|
|
2288
|
+
return `
|
|
2289
|
+
|
|
2290
|
+
---
|
|
2291
|
+
Attachment: ${attachment.name}
|
|
2292
|
+
${attachment.content}`;
|
|
2293
|
+
}).join("");
|
|
2294
|
+
}
|
|
2295
|
+
function resolveSessionTarget(sessions, nameOrId) {
|
|
2296
|
+
const byId = sessions.find((session) => session.id === nameOrId);
|
|
2297
|
+
if (byId) return byId.id;
|
|
2298
|
+
const lowerName = nameOrId.toLowerCase();
|
|
2299
|
+
const byName = sessions.filter((session) => session.name?.toLowerCase() === lowerName);
|
|
2300
|
+
if (byName.length > 1) {
|
|
2301
|
+
throw new Error(`Multiple sessions named "${nameOrId}" are connected. Use the session ID instead.`);
|
|
2302
|
+
}
|
|
2303
|
+
if (byName[0]) return byName[0].id;
|
|
2304
|
+
if (nameOrId.length >= 4) {
|
|
2305
|
+
const byPrefix = sessions.filter((session) => session.id.startsWith(nameOrId));
|
|
2306
|
+
if (byPrefix.length > 1) {
|
|
2307
|
+
throw new Error(`Multiple sessions match the ID prefix "${nameOrId}". Use the full session ID or a unique name.`);
|
|
2308
|
+
}
|
|
2309
|
+
if (byPrefix[0]) return byPrefix[0].id;
|
|
2310
|
+
}
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
function formatSessionDisplay(session) {
|
|
2314
|
+
const name = session.name || session.id;
|
|
2315
|
+
return session.origin === "remote" ? `${name} [remote:${session.remoteHostId || "unknown-host"}]` : name;
|
|
2316
|
+
}
|
|
2317
|
+
function formatSessionList(sessions, currentSessionId, currentCwd) {
|
|
2318
|
+
if (!sessions.length) return "No intercom sessions connected.";
|
|
2319
|
+
return sessions.map((session) => {
|
|
2320
|
+
const tags = [
|
|
2321
|
+
session.id === currentSessionId ? "self" : void 0,
|
|
2322
|
+
session.cwd === currentCwd ? "same cwd" : void 0,
|
|
2323
|
+
session.status
|
|
2324
|
+
].filter((tag) => Boolean(tag));
|
|
2325
|
+
const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
2326
|
+
return `- ${formatSessionDisplay(session)} (${session.id.slice(0, 8)}) - ${session.cwd} (${session.model})${suffix}`;
|
|
2327
|
+
}).join("\n");
|
|
2328
|
+
}
|
|
2329
|
+
function detectGitRoot(cwd) {
|
|
2330
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2331
|
+
cwd,
|
|
2332
|
+
encoding: "utf8",
|
|
2333
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2334
|
+
});
|
|
2335
|
+
if (result.status !== 0) return null;
|
|
2336
|
+
return result.stdout.trim() || null;
|
|
2337
|
+
}
|
|
2338
|
+
function textResult(text, structuredContent, isError = false) {
|
|
2339
|
+
return {
|
|
2340
|
+
content: [{ type: "text", text }],
|
|
2341
|
+
...structuredContent ? { structuredContent } : {},
|
|
2342
|
+
...isError ? { isError: true } : {}
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
var CodexIntercomRuntime = class {
|
|
2346
|
+
client = null;
|
|
2347
|
+
connectPromise = null;
|
|
2348
|
+
reconnectTimer = null;
|
|
2349
|
+
reconnectAttempt = 0;
|
|
2350
|
+
reconnectEnabled = true;
|
|
2351
|
+
identity;
|
|
2352
|
+
unread = [];
|
|
2353
|
+
unresolvedAsks = /* @__PURE__ */ new Map();
|
|
2354
|
+
replyWaiters = /* @__PURE__ */ new Map();
|
|
2355
|
+
clientFactory;
|
|
2356
|
+
prepareConnection;
|
|
2357
|
+
reconnectDelays;
|
|
2358
|
+
constructor(identity = buildCodexRuntimeIdentity(), options = {}) {
|
|
2359
|
+
this.identity = identity;
|
|
2360
|
+
const initialScopeId = intercomScopeIdFromEnv();
|
|
2361
|
+
this.clientFactory = options.clientFactory ?? (() => new IntercomClient(initialScopeId ? { scopeId: initialScopeId } : {}));
|
|
2362
|
+
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
2363
|
+
const config = loadConfig();
|
|
2364
|
+
if (!config.enabled) throw new Error("Intercom disabled");
|
|
2365
|
+
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
2366
|
+
});
|
|
2367
|
+
this.reconnectDelays = options.reconnectDelays?.length ? options.reconnectDelays : [250, 500, 1e3, 2e3, 5e3];
|
|
2368
|
+
}
|
|
2369
|
+
getIdentity() {
|
|
2370
|
+
return this.identity;
|
|
2371
|
+
}
|
|
2372
|
+
async connect() {
|
|
2373
|
+
this.reconnectEnabled = true;
|
|
2374
|
+
this.clearReconnectTimer();
|
|
2375
|
+
if (this.client?.isConnected()) return this.client;
|
|
2376
|
+
if (this.connectPromise) return this.connectPromise;
|
|
2377
|
+
this.connectPromise = this.connectOnce();
|
|
2378
|
+
try {
|
|
2379
|
+
return await this.connectPromise;
|
|
2380
|
+
} finally {
|
|
2381
|
+
this.connectPromise = null;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
async connectOnce() {
|
|
2385
|
+
await this.prepareConnection();
|
|
2386
|
+
const client = this.clientFactory();
|
|
2387
|
+
client.on("message", (from, message, deliveryId) => {
|
|
2388
|
+
this.handleIncomingMessage(from, message);
|
|
2389
|
+
client.acknowledgeMessage(deliveryId);
|
|
2390
|
+
});
|
|
2391
|
+
client.on("disconnected", (error2) => {
|
|
2392
|
+
for (const waiter of this.replyWaiters.values()) {
|
|
2393
|
+
clearTimeout(waiter.timeout);
|
|
2394
|
+
waiter.cleanup?.();
|
|
2395
|
+
waiter.reject(new Error(`Disconnected while waiting for reply: ${error2.message}`, { cause: error2 }));
|
|
2396
|
+
}
|
|
2397
|
+
this.replyWaiters.clear();
|
|
2398
|
+
if (this.client === client) this.client = null;
|
|
2399
|
+
this.scheduleReconnect();
|
|
2400
|
+
});
|
|
2401
|
+
await client.connect({
|
|
2402
|
+
name: this.identity.name,
|
|
2403
|
+
cwd: this.identity.cwd,
|
|
2404
|
+
model: this.identity.model,
|
|
2405
|
+
pid: process.pid,
|
|
2406
|
+
startedAt: this.identity.startedAt,
|
|
2407
|
+
lastActivity: Date.now(),
|
|
2408
|
+
status: "idle"
|
|
2409
|
+
}, this.identity.sessionId);
|
|
2410
|
+
this.client = client;
|
|
2411
|
+
this.reconnectAttempt = 0;
|
|
2412
|
+
return client;
|
|
2413
|
+
}
|
|
2414
|
+
scheduleReconnect() {
|
|
2415
|
+
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
2416
|
+
const delay = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
2417
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2418
|
+
this.reconnectTimer = null;
|
|
2419
|
+
void this.connect().then((client) => {
|
|
2420
|
+
if (!client.isConnected()) {
|
|
2421
|
+
this.reconnectAttempt += 1;
|
|
2422
|
+
this.scheduleReconnect();
|
|
2423
|
+
}
|
|
2424
|
+
}).catch(() => {
|
|
2425
|
+
this.reconnectAttempt += 1;
|
|
2426
|
+
this.scheduleReconnect();
|
|
2427
|
+
});
|
|
2428
|
+
}, delay);
|
|
2429
|
+
this.reconnectTimer.unref?.();
|
|
2430
|
+
}
|
|
2431
|
+
clearReconnectTimer() {
|
|
2432
|
+
if (!this.reconnectTimer) return;
|
|
2433
|
+
clearTimeout(this.reconnectTimer);
|
|
2434
|
+
this.reconnectTimer = null;
|
|
2435
|
+
}
|
|
2436
|
+
async disconnect() {
|
|
2437
|
+
this.reconnectEnabled = false;
|
|
2438
|
+
this.clearReconnectTimer();
|
|
2439
|
+
if (this.connectPromise) {
|
|
2440
|
+
try {
|
|
2441
|
+
await this.connectPromise;
|
|
2442
|
+
} catch {
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const client = this.client;
|
|
2446
|
+
this.client = null;
|
|
2447
|
+
if (client) await client.disconnect();
|
|
2448
|
+
}
|
|
2449
|
+
handleIncomingMessage(from, message) {
|
|
2450
|
+
const waiter = this.replyWaiters.get(message.replyTo ?? "");
|
|
2451
|
+
if (waiter) {
|
|
2452
|
+
const senderTarget = from.name || from.id;
|
|
2453
|
+
const fromMatches = senderTarget.toLowerCase() === waiter.from.toLowerCase() || from.id === waiter.from;
|
|
2454
|
+
if (fromMatches) {
|
|
2455
|
+
this.replyWaiters.delete(waiter.replyTo);
|
|
2456
|
+
clearTimeout(waiter.timeout);
|
|
2457
|
+
waiter.cleanup?.();
|
|
2458
|
+
waiter.resolve(message);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
const entry = { from, message, receivedAt: Date.now(), read: false };
|
|
2463
|
+
this.unread.push(entry);
|
|
2464
|
+
if (message.expectsReply) {
|
|
2465
|
+
this.unresolvedAsks.set(message.id, entry);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal) {
|
|
2469
|
+
return new Promise((resolve3, reject) => {
|
|
2470
|
+
if (signal?.aborted) {
|
|
2471
|
+
reject(new Error("intercom_ask cancelled"));
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
let timeout;
|
|
2475
|
+
const cleanup = () => {
|
|
2476
|
+
clearTimeout(timeout);
|
|
2477
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2478
|
+
};
|
|
2479
|
+
const onAbort = () => {
|
|
2480
|
+
this.replyWaiters.delete(replyTo);
|
|
2481
|
+
cleanup();
|
|
2482
|
+
void this.client?.cancelAsk(replyTo);
|
|
2483
|
+
reject(new Error("intercom_ask cancelled"));
|
|
2484
|
+
};
|
|
2485
|
+
timeout = setTimeout(() => {
|
|
2486
|
+
this.replyWaiters.delete(replyTo);
|
|
2487
|
+
void this.client?.deferAsk(replyTo);
|
|
2488
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2489
|
+
reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
|
|
2490
|
+
}, timeoutMs);
|
|
2491
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2492
|
+
this.replyWaiters.set(replyTo, { from, replyTo, resolve: resolve3, reject, timeout, cleanup });
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
async resolveTarget(to) {
|
|
2496
|
+
const client = await this.connect();
|
|
2497
|
+
const sessions = await client.listSessions();
|
|
2498
|
+
return resolveSessionTarget(sessions, to) ?? to;
|
|
2499
|
+
}
|
|
2500
|
+
async whoami() {
|
|
2501
|
+
const client = await this.connect();
|
|
2502
|
+
const sessionId = client.sessionId ?? this.identity.sessionId;
|
|
2503
|
+
return textResult(
|
|
2504
|
+
`session_id: ${sessionId}
|
|
2505
|
+
name: ${this.identity.name}
|
|
2506
|
+
cwd: ${this.identity.cwd}`,
|
|
2507
|
+
{ session_id: sessionId, name: this.identity.name, cwd: this.identity.cwd, model: this.identity.model }
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
async team() {
|
|
2511
|
+
const client = await this.connect();
|
|
2512
|
+
const sessions = await client.listSessions();
|
|
2513
|
+
const team = await resolveIntercomTeam({ selfId: client.sessionId ?? this.identity.sessionId, sessions });
|
|
2514
|
+
return textResult(formatIntercomTeam(team), team);
|
|
2515
|
+
}
|
|
2516
|
+
async status() {
|
|
2517
|
+
const client = await this.connect();
|
|
2518
|
+
const sessions = await client.listSessions();
|
|
2519
|
+
return textResult(
|
|
2520
|
+
`Connected: ${client.isConnected() ? "Yes" : "No"}
|
|
2521
|
+
Session ID: ${client.sessionId ?? "unknown"}
|
|
2522
|
+
Active sessions: ${sessions.length}
|
|
2523
|
+
Unread messages: ${this.unread.filter((entry) => !entry.read).length}
|
|
2524
|
+
Pending asks: ${this.unresolvedAsks.size}`,
|
|
2525
|
+
{
|
|
2526
|
+
connected: client.isConnected(),
|
|
2527
|
+
session_id: client.sessionId,
|
|
2528
|
+
active_sessions: sessions.length,
|
|
2529
|
+
unread_messages: this.unread.filter((entry) => !entry.read).length,
|
|
2530
|
+
pending_asks: this.unresolvedAsks.size
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
async list(scope2 = "machine", includeSelf = false) {
|
|
2535
|
+
const client = await this.connect();
|
|
2536
|
+
let sessions = await client.listSessions();
|
|
2537
|
+
if (scope2 === "directory") {
|
|
2538
|
+
sessions = sessions.filter((session) => session.cwd === this.identity.cwd);
|
|
2539
|
+
} else if (scope2 === "repo") {
|
|
2540
|
+
const currentRoot = detectGitRoot(this.identity.cwd);
|
|
2541
|
+
sessions = currentRoot ? sessions.filter((session) => detectGitRoot(session.cwd) === currentRoot) : [];
|
|
2542
|
+
}
|
|
2543
|
+
if (!includeSelf) {
|
|
2544
|
+
sessions = sessions.filter((session) => session.id !== client.sessionId);
|
|
2545
|
+
}
|
|
2546
|
+
return textResult(formatSessionList(sessions, client.sessionId, this.identity.cwd), { sessions });
|
|
2547
|
+
}
|
|
2548
|
+
async setSummary(summary) {
|
|
2549
|
+
const client = await this.connect();
|
|
2550
|
+
client.updatePresence({ status: summary.trim() || "idle" });
|
|
2551
|
+
return textResult("Summary updated.", { ok: true, summary });
|
|
2552
|
+
}
|
|
2553
|
+
async send(to, message, attachments, replyTo) {
|
|
2554
|
+
const client = await this.connect();
|
|
2555
|
+
const sendTo = await this.resolveTarget(to);
|
|
2556
|
+
const result = await client.send(sendTo, { text: message, attachments, replyTo });
|
|
2557
|
+
if (!result.delivered) {
|
|
2558
|
+
return textResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
|
|
2559
|
+
}
|
|
2560
|
+
if (replyTo) this.unresolvedAsks.delete(replyTo);
|
|
2561
|
+
return textResult(`Message sent to ${to}.`, { ok: true, message_id: result.id, to });
|
|
2562
|
+
}
|
|
2563
|
+
async ask(to, message, attachments, timeoutMs = getAskTimeoutMs(), signal) {
|
|
2564
|
+
const client = await this.connect();
|
|
2565
|
+
const sendTo = await this.resolveTarget(to);
|
|
2566
|
+
const questionId = randomUUID4();
|
|
2567
|
+
const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal);
|
|
2568
|
+
void replyPromise.catch(() => void 0);
|
|
2569
|
+
try {
|
|
2570
|
+
const result = await client.send(sendTo, {
|
|
2571
|
+
messageId: questionId,
|
|
2572
|
+
text: message,
|
|
2573
|
+
attachments,
|
|
2574
|
+
expectsReply: true
|
|
2575
|
+
});
|
|
2576
|
+
if (!result.delivered) {
|
|
2577
|
+
this.replyWaiters.get(questionId)?.reject(new Error(result.reason ?? "Session may not exist or has disconnected."));
|
|
2578
|
+
this.replyWaiters.delete(questionId);
|
|
2579
|
+
client.cancelAsk(questionId);
|
|
2580
|
+
return textResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
|
|
2581
|
+
}
|
|
2582
|
+
const reply = await replyPromise;
|
|
2583
|
+
const replyText = `${reply.content.text}${formatAttachments(reply.content.attachments)}`;
|
|
2584
|
+
return textResult(`Reply from ${to}:
|
|
2585
|
+
${replyText}`, { ok: true, message_id: result.id, reply });
|
|
2586
|
+
} catch (error2) {
|
|
2587
|
+
client.cancelAsk(questionId);
|
|
2588
|
+
return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
async pending(markRead = false) {
|
|
2592
|
+
const unreadMessages = this.unread.filter((entry) => !entry.read);
|
|
2593
|
+
if (markRead) {
|
|
2594
|
+
for (const entry of unreadMessages) entry.read = true;
|
|
2595
|
+
}
|
|
2596
|
+
const pendingAsks = Array.from(this.unresolvedAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
|
|
2597
|
+
const lines = [
|
|
2598
|
+
unreadMessages.length ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n") : "No unread messages.",
|
|
2599
|
+
pendingAsks.length ? `
|
|
2600
|
+
Pending asks:
|
|
2601
|
+
${pendingAsks.map((entry) => {
|
|
2602
|
+
const selector = pendingSelector(pendingAsks, entry);
|
|
2603
|
+
return `- ${formatSessionDisplay(entry.from)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
|
|
2604
|
+
}).join("\n")}` : ""
|
|
2605
|
+
].filter(Boolean);
|
|
2606
|
+
return textResult(lines.join("\n"), {
|
|
2607
|
+
unread_messages: unreadMessages.map((entry) => publicPendingEntry(entry)),
|
|
2608
|
+
pending_asks: pendingAsks.map((entry) => publicPendingEntry(entry, pendingSelector(pendingAsks, entry)))
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
async reply(message, to, which) {
|
|
2612
|
+
let target;
|
|
2613
|
+
try {
|
|
2614
|
+
target = selectPendingAsk(Array.from(this.unresolvedAsks.values()), to, which);
|
|
2615
|
+
} catch (error2) {
|
|
2616
|
+
return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
|
|
2617
|
+
}
|
|
2618
|
+
const result = await this.send(target.from.id, message, void 0, target.message.id);
|
|
2619
|
+
if (!result.isError) {
|
|
2620
|
+
this.unresolvedAsks.delete(target.message.id);
|
|
2621
|
+
}
|
|
2622
|
+
return result;
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
// codex/mcp-protocol.ts
|
|
2627
|
+
var inflightToolCalls = /* @__PURE__ */ new Map();
|
|
2628
|
+
function asString(value, name) {
|
|
2629
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2630
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
2631
|
+
}
|
|
2632
|
+
return value;
|
|
2633
|
+
}
|
|
2634
|
+
function asBoolean(value, defaultValue) {
|
|
2635
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
2636
|
+
}
|
|
2637
|
+
function asOptionalPositiveInteger(value, name) {
|
|
2638
|
+
if (value === void 0) return void 0;
|
|
2639
|
+
return validateAskTimeoutMs(value, name);
|
|
2640
|
+
}
|
|
2641
|
+
function asAttachmentArray(value) {
|
|
2642
|
+
if (value === void 0) return void 0;
|
|
2643
|
+
if (!Array.isArray(value)) throw new Error("attachments must be an array");
|
|
2644
|
+
return value.map((item, index) => {
|
|
2645
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`attachments[${index}] must be an object`);
|
|
2646
|
+
const raw = item;
|
|
2647
|
+
const type = raw.type;
|
|
2648
|
+
if (type !== "file" && type !== "snippet" && type !== "context") throw new Error(`attachments[${index}].type must be file, snippet, or context`);
|
|
2649
|
+
const name = asString(raw.name, `attachments[${index}].name`);
|
|
2650
|
+
const content = asString(raw.content, `attachments[${index}].content`);
|
|
2651
|
+
if (raw.language !== void 0 && typeof raw.language !== "string") throw new Error(`attachments[${index}].language must be a string`);
|
|
2652
|
+
const language = typeof raw.language === "string" ? raw.language : void 0;
|
|
2653
|
+
return { type, name, content, ...language ? { language } : {} };
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
var attachmentsSchema = {
|
|
2657
|
+
type: "array",
|
|
2658
|
+
items: {
|
|
2659
|
+
type: "object",
|
|
2660
|
+
properties: {
|
|
2661
|
+
type: { type: "string", enum: ["file", "snippet", "context"] },
|
|
2662
|
+
name: { type: "string" },
|
|
2663
|
+
content: { type: "string" },
|
|
2664
|
+
language: { type: "string" }
|
|
2665
|
+
},
|
|
2666
|
+
required: ["type", "name", "content"],
|
|
2667
|
+
additionalProperties: false
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
function buildToolDefinitions(runtime2) {
|
|
2671
|
+
return [
|
|
2672
|
+
{
|
|
2673
|
+
name: "intercom_whoami",
|
|
2674
|
+
description: "Return this Codex session's intercom identity for reliable targeting.",
|
|
2675
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2676
|
+
handler: async () => runtime2.whoami()
|
|
2677
|
+
},
|
|
2678
|
+
{
|
|
2679
|
+
name: "intercom_team",
|
|
2680
|
+
description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
|
|
2681
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2682
|
+
handler: async () => runtime2.team()
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: "intercom_status",
|
|
2686
|
+
description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
|
|
2687
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2688
|
+
handler: async () => runtime2.status()
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: "intercom_list",
|
|
2692
|
+
description: "List intercom-connected Pi or Codex sessions on this machine.",
|
|
2693
|
+
inputSchema: {
|
|
2694
|
+
type: "object",
|
|
2695
|
+
properties: {
|
|
2696
|
+
scope: { type: "string", enum: ["machine", "directory", "repo"], default: "machine" },
|
|
2697
|
+
include_self: { type: "boolean", default: false }
|
|
2698
|
+
},
|
|
2699
|
+
additionalProperties: false
|
|
2700
|
+
},
|
|
2701
|
+
handler: async (args) => runtime2.list(
|
|
2702
|
+
args.scope === "directory" || args.scope === "repo" ? args.scope : "machine",
|
|
2703
|
+
asBoolean(args.include_self, false)
|
|
2704
|
+
)
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
name: "intercom_set_summary",
|
|
2708
|
+
description: "Publish a short status summary so other sessions can discover what this Codex session is doing.",
|
|
2709
|
+
inputSchema: {
|
|
2710
|
+
type: "object",
|
|
2711
|
+
properties: { summary: { type: "string", maxLength: 400 } },
|
|
2712
|
+
required: ["summary"],
|
|
2713
|
+
additionalProperties: false
|
|
2714
|
+
},
|
|
2715
|
+
handler: async (args) => runtime2.setSummary(asString(args.summary, "summary"))
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
name: "intercom_send",
|
|
2719
|
+
description: "Send a non-blocking direct message to another intercom session by name, full ID, or unique ID prefix.",
|
|
2720
|
+
inputSchema: {
|
|
2721
|
+
type: "object",
|
|
2722
|
+
properties: {
|
|
2723
|
+
to: { type: "string" },
|
|
2724
|
+
message: { type: "string" },
|
|
2725
|
+
attachments: attachmentsSchema
|
|
2726
|
+
},
|
|
2727
|
+
required: ["to", "message"],
|
|
2728
|
+
additionalProperties: false
|
|
2729
|
+
},
|
|
2730
|
+
handler: async (args) => runtime2.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments))
|
|
2731
|
+
},
|
|
2732
|
+
{
|
|
2733
|
+
name: "intercom_ask",
|
|
2734
|
+
description: "Ask another intercom session a question only when the next step depends on its reply. Use intercom_send for assignments, progress/status checkpoints, and notifications.",
|
|
2735
|
+
inputSchema: {
|
|
2736
|
+
type: "object",
|
|
2737
|
+
properties: {
|
|
2738
|
+
to: { type: "string" },
|
|
2739
|
+
message: { type: "string" },
|
|
2740
|
+
attachments: attachmentsSchema,
|
|
2741
|
+
timeout_ms: { type: "integer", minimum: 1, maximum: 12e4, description: "Maximum time to wait for a reply before returning an error. Use intercom_send plus intercom_pending for longer work." }
|
|
2742
|
+
},
|
|
2743
|
+
required: ["to", "message"],
|
|
2744
|
+
additionalProperties: false
|
|
2745
|
+
},
|
|
2746
|
+
handler: async (args, signal) => runtime2.ask(
|
|
2747
|
+
asString(args.to, "to"),
|
|
2748
|
+
asString(args.message, "message"),
|
|
2749
|
+
asAttachmentArray(args.attachments),
|
|
2750
|
+
asOptionalPositiveInteger(args.timeout_ms, "timeout_ms"),
|
|
2751
|
+
signal
|
|
2752
|
+
)
|
|
2753
|
+
},
|
|
2754
|
+
{
|
|
2755
|
+
name: "intercom_pending",
|
|
2756
|
+
description: "Read unread inbound messages and unresolved asks for this Codex session.",
|
|
2757
|
+
inputSchema: {
|
|
2758
|
+
type: "object",
|
|
2759
|
+
properties: { mark_read: { type: "boolean", default: false } },
|
|
2760
|
+
additionalProperties: false
|
|
2761
|
+
},
|
|
2762
|
+
handler: async (args) => runtime2.pending(asBoolean(args.mark_read, false))
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
name: "intercom_reply",
|
|
2766
|
+
description: "Reply to a pending inbound ask. Use to plus which=oldest/latest when one sender has multiple unresolved asks.",
|
|
2767
|
+
inputSchema: {
|
|
2768
|
+
type: "object",
|
|
2769
|
+
properties: {
|
|
2770
|
+
message: { type: "string" },
|
|
2771
|
+
to: { type: "string", description: "Optional sender/session selector; never a message or thread ID." },
|
|
2772
|
+
which: { type: "string", enum: ["oldest", "latest"], description: "Select the oldest or latest ask from the chosen sender." }
|
|
2773
|
+
},
|
|
2774
|
+
required: ["message"],
|
|
2775
|
+
additionalProperties: false
|
|
2776
|
+
},
|
|
2777
|
+
handler: async (args) => runtime2.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0)
|
|
2778
|
+
}
|
|
2779
|
+
];
|
|
2780
|
+
}
|
|
2781
|
+
function ok(id, result) {
|
|
2782
|
+
return { jsonrpc: "2.0", id, result };
|
|
2783
|
+
}
|
|
2784
|
+
function error(id, code, message) {
|
|
2785
|
+
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
2786
|
+
}
|
|
2787
|
+
async function handleMcpRequest(request, runtime2) {
|
|
2788
|
+
if (!request.method) {
|
|
2789
|
+
return error(request.id, -32600, "Invalid request");
|
|
2790
|
+
}
|
|
2791
|
+
if (request.method === "notifications/cancelled") {
|
|
2792
|
+
const requestId = request.params?.requestId;
|
|
2793
|
+
if (typeof requestId === "string" || typeof requestId === "number") {
|
|
2794
|
+
inflightToolCalls.get(requestId)?.abort();
|
|
2795
|
+
}
|
|
2796
|
+
return void 0;
|
|
2797
|
+
}
|
|
2798
|
+
if (request.id === void 0 && request.method.startsWith("notifications/")) {
|
|
2799
|
+
return void 0;
|
|
2800
|
+
}
|
|
2801
|
+
const tools = buildToolDefinitions(runtime2);
|
|
2802
|
+
switch (request.method) {
|
|
2803
|
+
case "initialize":
|
|
2804
|
+
return ok(request.id, {
|
|
2805
|
+
protocolVersion: "2025-06-18",
|
|
2806
|
+
capabilities: { tools: {} },
|
|
2807
|
+
serverInfo: { name: "codex-intercom", version: "0.1.0" }
|
|
2808
|
+
});
|
|
2809
|
+
case "ping":
|
|
2810
|
+
return ok(request.id, {});
|
|
2811
|
+
case "tools/list":
|
|
2812
|
+
return ok(request.id, {
|
|
2813
|
+
tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }))
|
|
2814
|
+
});
|
|
2815
|
+
case "tools/call": {
|
|
2816
|
+
const name = request.params?.name;
|
|
2817
|
+
const args = request.params?.arguments;
|
|
2818
|
+
if (typeof name !== "string") return error(request.id, -32602, "tools/call requires params.name");
|
|
2819
|
+
if (args !== void 0 && (!args || typeof args !== "object" || Array.isArray(args))) {
|
|
2820
|
+
return error(request.id, -32602, "tools/call params.arguments must be an object");
|
|
2821
|
+
}
|
|
2822
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
2823
|
+
if (!tool) return error(request.id, -32602, `Unknown tool: ${name}`);
|
|
2824
|
+
const requestId = request.id;
|
|
2825
|
+
const abortController = typeof requestId === "string" || typeof requestId === "number" ? new AbortController() : null;
|
|
2826
|
+
if (abortController && requestId !== void 0) inflightToolCalls.set(requestId, abortController);
|
|
2827
|
+
try {
|
|
2828
|
+
return ok(request.id, await tool.handler(args ?? {}, abortController?.signal));
|
|
2829
|
+
} catch (caught) {
|
|
2830
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
2831
|
+
return ok(request.id, { content: [{ type: "text", text: message }], isError: true });
|
|
2832
|
+
} finally {
|
|
2833
|
+
if (abortController && requestId !== void 0) inflightToolCalls.delete(requestId);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
default:
|
|
2837
|
+
return error(request.id, -32601, `Method not found: ${request.method}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
// codex/server.ts
|
|
2842
|
+
var runtime = new CodexIntercomRuntime();
|
|
2843
|
+
var rl = readline.createInterface({
|
|
2844
|
+
input: stdin,
|
|
2845
|
+
crlfDelay: Infinity
|
|
2846
|
+
});
|
|
2847
|
+
var shuttingDown = false;
|
|
2848
|
+
var pendingRequests = 0;
|
|
2849
|
+
function writeResponse(response) {
|
|
2850
|
+
if (!response) return;
|
|
2851
|
+
stdout.write(`${JSON.stringify(response)}
|
|
2852
|
+
`);
|
|
2853
|
+
}
|
|
2854
|
+
function maybeShutdown() {
|
|
2855
|
+
if (!shuttingDown || pendingRequests > 0) return;
|
|
2856
|
+
void runtime.disconnect().finally(() => process.exit(0));
|
|
2857
|
+
}
|
|
2858
|
+
rl.on("line", (line) => {
|
|
2859
|
+
const trimmed = line.trim();
|
|
2860
|
+
if (!trimmed) return;
|
|
2861
|
+
pendingRequests += 1;
|
|
2862
|
+
void (async () => {
|
|
2863
|
+
try {
|
|
2864
|
+
const request = JSON.parse(trimmed);
|
|
2865
|
+
writeResponse(await handleMcpRequest(request, runtime));
|
|
2866
|
+
} catch (error2) {
|
|
2867
|
+
writeResponse({
|
|
2868
|
+
jsonrpc: "2.0",
|
|
2869
|
+
id: null,
|
|
2870
|
+
error: {
|
|
2871
|
+
code: -32700,
|
|
2872
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
})().finally(() => {
|
|
2877
|
+
pendingRequests -= 1;
|
|
2878
|
+
maybeShutdown();
|
|
2879
|
+
});
|
|
2880
|
+
});
|
|
2881
|
+
var shutdown = () => {
|
|
2882
|
+
if (shuttingDown) return;
|
|
2883
|
+
shuttingDown = true;
|
|
2884
|
+
rl.close();
|
|
2885
|
+
maybeShutdown();
|
|
2886
|
+
};
|
|
2887
|
+
rl.on("close", shutdown);
|
|
2888
|
+
process.on("SIGINT", shutdown);
|
|
2889
|
+
process.on("SIGTERM", shutdown);
|
|
2890
|
+
process.on("disconnect", shutdown);
|