@drisp/cli 0.5.1 → 0.5.4
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/dist/{WorkflowInstallWizard-X754ND4V.js → WorkflowInstallWizard-7Y5PWAKW.js} +2 -2
- package/dist/athena-gateway.js +4113 -4
- package/dist/{chunk-A54HGVML.js → chunk-7E54JMXH.js} +126 -114
- package/dist/{chunk-PEBITVZQ.js → chunk-QVXHUJPH.js} +1944 -1117
- package/dist/cli.js +21 -380
- package/dist/dashboard-daemon.js +103 -8
- package/package.json +1 -1
- package/dist/chunk-2OJ3GGIP.js +0 -104
- package/dist/chunk-OB4HZXR5.js +0 -4124
- package/dist/chunk-ZVOGOZNT.js +0 -395
- package/dist/supervisor.js +0 -692
package/dist/chunk-OB4HZXR5.js
DELETED
|
@@ -1,4124 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadOrCreateToken,
|
|
3
|
-
requireTokenForBind,
|
|
4
|
-
timingSafeTokenEqual
|
|
5
|
-
} from "./chunk-MRAM6EYI.js";
|
|
6
|
-
import {
|
|
7
|
-
CHANNEL_REQUEST_ID_REGEX,
|
|
8
|
-
createUdsServerTransport,
|
|
9
|
-
generateChannelRequestId,
|
|
10
|
-
isLoopbackHost,
|
|
11
|
-
isValidChannelRequestId,
|
|
12
|
-
refreshDashboardAccessToken,
|
|
13
|
-
resolveGatewayPaths,
|
|
14
|
-
resolveListenSpec,
|
|
15
|
-
traceGatewayFrame,
|
|
16
|
-
trackGatewayRuntimeExpired,
|
|
17
|
-
trackGatewayRuntimeRebind,
|
|
18
|
-
trackGatewayTransportConnect,
|
|
19
|
-
trackGatewayTransportDisconnect,
|
|
20
|
-
writeGatewayTrace
|
|
21
|
-
} from "./chunk-BTY7MYYT.js";
|
|
22
|
-
|
|
23
|
-
// src/gateway/daemon.ts
|
|
24
|
-
import fs4 from "fs";
|
|
25
|
-
|
|
26
|
-
// src/infra/config/channels.ts
|
|
27
|
-
import fs from "fs";
|
|
28
|
-
import os from "os";
|
|
29
|
-
import path from "path";
|
|
30
|
-
function channelSidecarDir(home = os.homedir()) {
|
|
31
|
-
return path.join(home, ".config", "athena", "channels");
|
|
32
|
-
}
|
|
33
|
-
function loadChannelSidecars(home = os.homedir()) {
|
|
34
|
-
const dir = channelSidecarDir(home);
|
|
35
|
-
const sidecars = [];
|
|
36
|
-
const errors = [];
|
|
37
|
-
let entries;
|
|
38
|
-
try {
|
|
39
|
-
entries = fs.readdirSync(dir);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
const code = err.code;
|
|
42
|
-
if (code === "ENOENT") return { sidecars, errors };
|
|
43
|
-
errors.push({
|
|
44
|
-
path: dir,
|
|
45
|
-
reason: `read dir failed: ${err instanceof Error ? err.message : String(err)}`
|
|
46
|
-
});
|
|
47
|
-
return { sidecars, errors };
|
|
48
|
-
}
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
if (!entry.endsWith(".json")) continue;
|
|
51
|
-
const full = path.join(dir, entry);
|
|
52
|
-
const name = entry.slice(0, -".json".length);
|
|
53
|
-
const result = loadOne(name, full);
|
|
54
|
-
if (result.ok) sidecars.push(result.sidecar);
|
|
55
|
-
else errors.push({ path: full, reason: result.reason });
|
|
56
|
-
}
|
|
57
|
-
return { sidecars, errors };
|
|
58
|
-
}
|
|
59
|
-
function loadOne(name, filePath) {
|
|
60
|
-
let raw;
|
|
61
|
-
try {
|
|
62
|
-
if (process.platform !== "win32") {
|
|
63
|
-
const stat = fs.statSync(filePath);
|
|
64
|
-
if ((stat.mode & 63) !== 0) {
|
|
65
|
-
return {
|
|
66
|
-
ok: false,
|
|
67
|
-
reason: `file ${filePath} is too permissive (mode ${(stat.mode & 511).toString(8)}); chmod 600`
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
72
|
-
} catch (err) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (typeof raw !== "object" || raw === null) {
|
|
79
|
-
return { ok: false, reason: "config root must be an object" };
|
|
80
|
-
}
|
|
81
|
-
const obj = raw;
|
|
82
|
-
const userIdsRaw = obj["allowed_user_ids"];
|
|
83
|
-
const allowedUserIds = [];
|
|
84
|
-
if (userIdsRaw !== void 0) {
|
|
85
|
-
if (!Array.isArray(userIdsRaw)) {
|
|
86
|
-
return { ok: false, reason: "allowed_user_ids must be an array" };
|
|
87
|
-
}
|
|
88
|
-
for (const id of userIdsRaw) {
|
|
89
|
-
if (typeof id === "string") allowedUserIds.push(id);
|
|
90
|
-
else if (typeof id === "number") allowedUserIds.push(String(id));
|
|
91
|
-
else
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
reason: "allowed_user_ids entries must be string or number"
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const kindRaw = obj["kind"];
|
|
99
|
-
if (kindRaw !== void 0 && (typeof kindRaw !== "string" || kindRaw.length === 0)) {
|
|
100
|
-
return { ok: false, reason: "kind must be a non-empty string" };
|
|
101
|
-
}
|
|
102
|
-
const kind = kindRaw ?? name;
|
|
103
|
-
const instanceIdRaw = obj["instance_id"];
|
|
104
|
-
if (instanceIdRaw !== void 0 && (typeof instanceIdRaw !== "string" || instanceIdRaw.length === 0)) {
|
|
105
|
-
return { ok: false, reason: "instance_id must be a non-empty string" };
|
|
106
|
-
}
|
|
107
|
-
const instanceId = instanceIdRaw ?? kind;
|
|
108
|
-
const runnerIdRaw = obj["runner_id"];
|
|
109
|
-
let attachmentId;
|
|
110
|
-
if (runnerIdRaw !== void 0) {
|
|
111
|
-
if (typeof runnerIdRaw !== "string" || runnerIdRaw.length === 0) {
|
|
112
|
-
return { ok: false, reason: "runner_id must be a non-empty string" };
|
|
113
|
-
}
|
|
114
|
-
attachmentId = runnerIdRaw;
|
|
115
|
-
}
|
|
116
|
-
const options = {};
|
|
117
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
118
|
-
if (key === "allowed_user_ids" || key === "kind" || key === "instance_id") {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
options[key] = value;
|
|
122
|
-
}
|
|
123
|
-
const sidecar = {
|
|
124
|
-
name,
|
|
125
|
-
path: filePath,
|
|
126
|
-
kind,
|
|
127
|
-
instanceId,
|
|
128
|
-
allowedUserIds,
|
|
129
|
-
options
|
|
130
|
-
};
|
|
131
|
-
if (attachmentId !== void 0) sidecar.attachmentId = attachmentId;
|
|
132
|
-
return { ok: true, sidecar };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// src/gateway/adapters/console/adapter.ts
|
|
136
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
137
|
-
|
|
138
|
-
// src/gateway/adapters/console/client.ts
|
|
139
|
-
import { readFileSync } from "fs";
|
|
140
|
-
import { WebSocket } from "ws";
|
|
141
|
-
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
142
|
-
var DEFAULT_INITIAL_RECONNECT_MS = 1e3;
|
|
143
|
-
var DEFAULT_MAX_RECONNECT_MS = 3e4;
|
|
144
|
-
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
145
|
-
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 9e4;
|
|
146
|
-
function createConsoleBrokerClient(opts) {
|
|
147
|
-
const hasStatic = typeof opts.pairingToken === "string" && opts.pairingToken.length > 0;
|
|
148
|
-
const hasProvider = typeof opts.pairingTokenProvider === "function";
|
|
149
|
-
if (hasStatic === hasProvider) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
"console broker client: exactly one of pairingToken or pairingTokenProvider is required"
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
const provider = hasStatic ? async () => opts.pairingToken : opts.pairingTokenProvider;
|
|
155
|
-
const initialDelay = opts.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_MS;
|
|
156
|
-
const maxDelay = opts.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_MS;
|
|
157
|
-
const heartbeatInterval = opts.heartbeat?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
158
|
-
const heartbeatTimeout = opts.heartbeat?.timeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
159
|
-
let ws = null;
|
|
160
|
-
let ready = null;
|
|
161
|
-
let closeRequested = false;
|
|
162
|
-
let reconnectAttempt = 0;
|
|
163
|
-
let reconnectTimer = null;
|
|
164
|
-
let lastHello = null;
|
|
165
|
-
let currentToken = null;
|
|
166
|
-
let heartbeatTimer = null;
|
|
167
|
-
let lastPongAt = null;
|
|
168
|
-
const frameHandlers = /* @__PURE__ */ new Set();
|
|
169
|
-
const closeHandlers = /* @__PURE__ */ new Set();
|
|
170
|
-
const readyHandlers = /* @__PURE__ */ new Set();
|
|
171
|
-
const tokenRedacted = "<redacted>";
|
|
172
|
-
function redact(message) {
|
|
173
|
-
if (!currentToken) return message;
|
|
174
|
-
return message.split(currentToken).join(tokenRedacted);
|
|
175
|
-
}
|
|
176
|
-
function stopHeartbeat() {
|
|
177
|
-
if (heartbeatTimer) {
|
|
178
|
-
clearInterval(heartbeatTimer);
|
|
179
|
-
heartbeatTimer = null;
|
|
180
|
-
}
|
|
181
|
-
lastPongAt = null;
|
|
182
|
-
}
|
|
183
|
-
function startHeartbeat(currentWs) {
|
|
184
|
-
stopHeartbeat();
|
|
185
|
-
lastPongAt = Date.now();
|
|
186
|
-
heartbeatTimer = setInterval(() => {
|
|
187
|
-
if (currentWs !== ws || currentWs.readyState !== WebSocket.OPEN) {
|
|
188
|
-
stopHeartbeat();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (lastPongAt !== null && Date.now() - lastPongAt >= heartbeatTimeout) {
|
|
192
|
-
opts.log(
|
|
193
|
-
"warn",
|
|
194
|
-
"console broker: heartbeat watchdog fired, no pong received \u2014 terminating"
|
|
195
|
-
);
|
|
196
|
-
stopHeartbeat();
|
|
197
|
-
try {
|
|
198
|
-
currentWs.terminate();
|
|
199
|
-
} catch {
|
|
200
|
-
}
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const ping = {
|
|
204
|
-
kind: "console.ping",
|
|
205
|
-
frameId: makeFrameId(),
|
|
206
|
-
sentAt: Date.now()
|
|
207
|
-
};
|
|
208
|
-
try {
|
|
209
|
-
currentWs.send(JSON.stringify(ping));
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
}, heartbeatInterval);
|
|
213
|
-
}
|
|
214
|
-
function emitClose(reason) {
|
|
215
|
-
for (const h of [...closeHandlers]) {
|
|
216
|
-
try {
|
|
217
|
-
h(reason);
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function scheduleReconnect() {
|
|
223
|
-
if (closeRequested || !lastHello) return;
|
|
224
|
-
const exp = Math.min(maxDelay, initialDelay * 2 ** reconnectAttempt);
|
|
225
|
-
const delay = Math.floor(Math.random() * exp);
|
|
226
|
-
reconnectAttempt++;
|
|
227
|
-
const hello = lastHello;
|
|
228
|
-
reconnectTimer = setTimeout(() => {
|
|
229
|
-
reconnectTimer = null;
|
|
230
|
-
void attemptConnect(hello).then(
|
|
231
|
-
() => {
|
|
232
|
-
reconnectAttempt = 0;
|
|
233
|
-
},
|
|
234
|
-
(err) => {
|
|
235
|
-
opts.log(
|
|
236
|
-
"warn",
|
|
237
|
-
`console broker reconnect failed: ${redact(err instanceof Error ? err.message : String(err))}`
|
|
238
|
-
);
|
|
239
|
-
scheduleReconnect();
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
}, delay);
|
|
243
|
-
}
|
|
244
|
-
async function attemptConnect(hello) {
|
|
245
|
-
const timeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
246
|
-
let token;
|
|
247
|
-
try {
|
|
248
|
-
token = await provider();
|
|
249
|
-
} catch (err) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`console broker connect failed: pairing token provider threw: ${err instanceof Error ? err.message : String(err)}`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
255
|
-
throw new Error(
|
|
256
|
-
"console broker connect failed: pairing token provider returned empty token"
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
currentToken = token;
|
|
260
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
261
|
-
const wsOpts = opts.tlsCaPath ? { headers, ca: readFileSync(opts.tlsCaPath) } : { headers };
|
|
262
|
-
const next = new WebSocket(opts.brokerUrl, wsOpts);
|
|
263
|
-
ws = next;
|
|
264
|
-
ready = null;
|
|
265
|
-
try {
|
|
266
|
-
await new Promise((resolve, reject) => {
|
|
267
|
-
let settled = false;
|
|
268
|
-
const finishOk = () => {
|
|
269
|
-
if (settled) return;
|
|
270
|
-
settled = true;
|
|
271
|
-
clearTimeout(timer);
|
|
272
|
-
resolve();
|
|
273
|
-
};
|
|
274
|
-
const finishErr = (err) => {
|
|
275
|
-
if (settled) return;
|
|
276
|
-
settled = true;
|
|
277
|
-
clearTimeout(timer);
|
|
278
|
-
reject(err);
|
|
279
|
-
};
|
|
280
|
-
const timer = setTimeout(() => {
|
|
281
|
-
finishErr(
|
|
282
|
-
new Error(`console broker connect timed out after ${timeoutMs}ms`)
|
|
283
|
-
);
|
|
284
|
-
}, timeoutMs);
|
|
285
|
-
next.once("open", () => {
|
|
286
|
-
try {
|
|
287
|
-
const helloFrame = {
|
|
288
|
-
kind: "console.hello",
|
|
289
|
-
frameId: makeFrameId(),
|
|
290
|
-
sentAt: Date.now(),
|
|
291
|
-
protocolVersion: 1,
|
|
292
|
-
clientName: hello.clientName,
|
|
293
|
-
clientVersion: hello.clientVersion,
|
|
294
|
-
address: {
|
|
295
|
-
runnerId: hello.runnerId,
|
|
296
|
-
...hello.workspaceId !== void 0 ? { workspaceId: hello.workspaceId } : {}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
next.send(JSON.stringify(helloFrame));
|
|
300
|
-
} catch (err) {
|
|
301
|
-
finishErr(err instanceof Error ? err : new Error(String(err)));
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
next.once("error", (err) => {
|
|
305
|
-
finishErr(
|
|
306
|
-
new Error(`console broker connect failed: ${redact(err.message)}`)
|
|
307
|
-
);
|
|
308
|
-
});
|
|
309
|
-
const earlyCloseListener = (code, reasonBuf) => {
|
|
310
|
-
if (!ready) {
|
|
311
|
-
const reason = reasonBuf.toString();
|
|
312
|
-
finishErr(
|
|
313
|
-
new Error(
|
|
314
|
-
`console broker closed before ready (code=${code}${reason ? ` reason=${reason}` : ""})`
|
|
315
|
-
)
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
next.once("close", earlyCloseListener);
|
|
320
|
-
next.on("message", (data) => {
|
|
321
|
-
let parsed;
|
|
322
|
-
try {
|
|
323
|
-
parsed = JSON.parse(String(data));
|
|
324
|
-
} catch (err) {
|
|
325
|
-
opts.log(
|
|
326
|
-
"warn",
|
|
327
|
-
`console broker frame parse failed: ${redact(String(err))}`
|
|
328
|
-
);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (!ready) {
|
|
332
|
-
if (parsed.kind === "console.ready") {
|
|
333
|
-
const claimedRunnerId = hello.runnerId;
|
|
334
|
-
const readyRunnerId = parsed.address.runnerId;
|
|
335
|
-
if (readyRunnerId !== claimedRunnerId) {
|
|
336
|
-
finishErr(
|
|
337
|
-
new Error(
|
|
338
|
-
`console broker ready runnerId mismatch: claimed ${claimedRunnerId}, got ${readyRunnerId}`
|
|
339
|
-
)
|
|
340
|
-
);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
ready = parsed;
|
|
344
|
-
next.removeListener("close", earlyCloseListener);
|
|
345
|
-
const address = parsed.address;
|
|
346
|
-
for (const h of [...readyHandlers]) {
|
|
347
|
-
try {
|
|
348
|
-
h(address);
|
|
349
|
-
} catch {
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
finishOk();
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (parsed.kind === "console.error") {
|
|
356
|
-
finishErr(
|
|
357
|
-
new Error(
|
|
358
|
-
`console broker rejected hello: ${parsed.code} ${parsed.message}`
|
|
359
|
-
)
|
|
360
|
-
);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
opts.log(
|
|
364
|
-
"warn",
|
|
365
|
-
`console broker pre-ready frame ignored: ${parsed.kind}`
|
|
366
|
-
);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (parsed.kind === "console.pong") {
|
|
370
|
-
lastPongAt = Date.now();
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
for (const h of [...frameHandlers]) {
|
|
374
|
-
try {
|
|
375
|
-
h(parsed);
|
|
376
|
-
} catch (err) {
|
|
377
|
-
opts.log(
|
|
378
|
-
"warn",
|
|
379
|
-
`console frame handler threw: ${redact(err instanceof Error ? err.message : String(err))}`
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
} catch (err) {
|
|
386
|
-
try {
|
|
387
|
-
next.terminate();
|
|
388
|
-
} catch {
|
|
389
|
-
}
|
|
390
|
-
if (ws === next) {
|
|
391
|
-
ws = null;
|
|
392
|
-
ready = null;
|
|
393
|
-
}
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
next.on("close", (_code, reasonBuf) => {
|
|
397
|
-
stopHeartbeat();
|
|
398
|
-
if (next !== ws) return;
|
|
399
|
-
ws = null;
|
|
400
|
-
ready = null;
|
|
401
|
-
emitClose(reasonBuf.toString() || "closed");
|
|
402
|
-
if (!closeRequested) scheduleReconnect();
|
|
403
|
-
});
|
|
404
|
-
startHeartbeat(next);
|
|
405
|
-
}
|
|
406
|
-
async function connect(hello) {
|
|
407
|
-
if (ws) throw new Error("console broker client already connected");
|
|
408
|
-
closeRequested = false;
|
|
409
|
-
lastHello = hello;
|
|
410
|
-
await attemptConnect(hello);
|
|
411
|
-
}
|
|
412
|
-
function close(reason) {
|
|
413
|
-
closeRequested = true;
|
|
414
|
-
stopHeartbeat();
|
|
415
|
-
if (reconnectTimer) {
|
|
416
|
-
clearTimeout(reconnectTimer);
|
|
417
|
-
reconnectTimer = null;
|
|
418
|
-
}
|
|
419
|
-
if (ws) {
|
|
420
|
-
try {
|
|
421
|
-
ws.close(1e3, reason);
|
|
422
|
-
} catch {
|
|
423
|
-
ws.terminate();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
ws = null;
|
|
427
|
-
ready = null;
|
|
428
|
-
emitClose(reason);
|
|
429
|
-
}
|
|
430
|
-
function sendFrame(frame) {
|
|
431
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
432
|
-
throw new Error("console broker client not connected");
|
|
433
|
-
}
|
|
434
|
-
ws.send(JSON.stringify(frame));
|
|
435
|
-
}
|
|
436
|
-
function onFrame(handler) {
|
|
437
|
-
frameHandlers.add(handler);
|
|
438
|
-
}
|
|
439
|
-
function onClose(handler) {
|
|
440
|
-
closeHandlers.add(handler);
|
|
441
|
-
}
|
|
442
|
-
function onReady(handler) {
|
|
443
|
-
readyHandlers.add(handler);
|
|
444
|
-
}
|
|
445
|
-
return {
|
|
446
|
-
connect,
|
|
447
|
-
close,
|
|
448
|
-
sendFrame,
|
|
449
|
-
onFrame,
|
|
450
|
-
onReady,
|
|
451
|
-
onClose,
|
|
452
|
-
getReadyAddress: () => ready?.address ?? null,
|
|
453
|
-
isReady: () => ready !== null
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
var frameCounter = 0;
|
|
457
|
-
function makeFrameId() {
|
|
458
|
-
frameCounter = (frameCounter + 1) % 1e6;
|
|
459
|
-
return `f${Date.now().toString(36)}-${frameCounter.toString(36)}`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// src/gateway/adapters/console/adapter.ts
|
|
463
|
-
var CONSOLE_DEFAULT_ID = "console";
|
|
464
|
-
var CLIENT_NAME = "athena-cli";
|
|
465
|
-
var CLIENT_VERSION = "0.0.0";
|
|
466
|
-
var ConsoleAdapter = class {
|
|
467
|
-
id;
|
|
468
|
-
capabilities = {
|
|
469
|
-
chat: true,
|
|
470
|
-
threads: true,
|
|
471
|
-
relayPermission: true,
|
|
472
|
-
relayQuestion: true
|
|
473
|
-
};
|
|
474
|
-
opts;
|
|
475
|
-
client = null;
|
|
476
|
-
ctx = null;
|
|
477
|
-
pendingPermissions = /* @__PURE__ */ new Map();
|
|
478
|
-
pendingQuestions = /* @__PURE__ */ new Map();
|
|
479
|
-
constructor(opts, id = CONSOLE_DEFAULT_ID) {
|
|
480
|
-
this.opts = opts;
|
|
481
|
-
this.id = id;
|
|
482
|
-
}
|
|
483
|
-
async start(ctx) {
|
|
484
|
-
if (this.client) {
|
|
485
|
-
throw new Error("console adapter already started");
|
|
486
|
-
}
|
|
487
|
-
this.ctx = ctx;
|
|
488
|
-
const tokenSource = resolvePairingTokenSource(this.opts);
|
|
489
|
-
const factory = this.opts.brokerClientFactory ?? ((input) => createConsoleBrokerClient(input));
|
|
490
|
-
const client = factory({
|
|
491
|
-
brokerUrl: this.opts.brokerUrl,
|
|
492
|
-
...tokenSource.kind === "static" ? { pairingToken: tokenSource.token } : { pairingTokenProvider: tokenSource.provider },
|
|
493
|
-
...this.opts.tlsCaPath !== void 0 ? { tlsCaPath: this.opts.tlsCaPath } : {},
|
|
494
|
-
log: ctx.log
|
|
495
|
-
});
|
|
496
|
-
client.onFrame((frame) => this.handleInboundFrame(frame));
|
|
497
|
-
client.onReady(() => {
|
|
498
|
-
ctx.emitHealth({ at: Date.now(), transportOk: true });
|
|
499
|
-
});
|
|
500
|
-
client.onClose((reason) => {
|
|
501
|
-
this.disposePermissions("connection_lost");
|
|
502
|
-
this.disposeQuestions("connection_lost");
|
|
503
|
-
ctx.emitHealth({
|
|
504
|
-
at: Date.now(),
|
|
505
|
-
transportOk: false,
|
|
506
|
-
note: `broker connection closed: ${reason}`
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
await client.connect({
|
|
510
|
-
runnerId: this.opts.runnerId,
|
|
511
|
-
...this.opts.workspaceId !== void 0 ? { workspaceId: this.opts.workspaceId } : {},
|
|
512
|
-
clientName: CLIENT_NAME,
|
|
513
|
-
clientVersion: CLIENT_VERSION
|
|
514
|
-
});
|
|
515
|
-
this.client = client;
|
|
516
|
-
ctx.signal.addEventListener("abort", () => {
|
|
517
|
-
this.client?.close("manager abort");
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
async stop(_reason) {
|
|
521
|
-
this.disposePermissions();
|
|
522
|
-
this.disposeQuestions();
|
|
523
|
-
this.client?.close("shutdown");
|
|
524
|
-
this.client = null;
|
|
525
|
-
this.ctx = null;
|
|
526
|
-
}
|
|
527
|
-
async send(msg) {
|
|
528
|
-
const client = this.client;
|
|
529
|
-
if (!client || !client.isReady()) {
|
|
530
|
-
throw new Error("console adapter: send called before broker is ready");
|
|
531
|
-
}
|
|
532
|
-
const messageId = makeOutboundMessageId();
|
|
533
|
-
const workspaceId = this.opts.workspaceId ?? msg.location.accountId;
|
|
534
|
-
const frame = {
|
|
535
|
-
kind: "console.message.out",
|
|
536
|
-
frameId: makeFrameId2(),
|
|
537
|
-
sentAt: Date.now(),
|
|
538
|
-
address: {
|
|
539
|
-
runnerId: this.opts.runnerId,
|
|
540
|
-
...workspaceId.length > 0 ? { workspaceId } : {},
|
|
541
|
-
...msg.location.peer?.id !== void 0 ? { userId: msg.location.peer.id } : {},
|
|
542
|
-
...msg.location.thread?.id !== void 0 ? { threadId: msg.location.thread.id } : {}
|
|
543
|
-
},
|
|
544
|
-
messageId,
|
|
545
|
-
idempotencyKey: msg.idempotencyKey,
|
|
546
|
-
text: msg.text
|
|
547
|
-
};
|
|
548
|
-
client.sendFrame(frame);
|
|
549
|
-
return {
|
|
550
|
-
providerMessageId: messageId,
|
|
551
|
-
deliveredAt: Date.now()
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
async probe() {
|
|
555
|
-
const ok2 = this.client?.isReady() ?? false;
|
|
556
|
-
return {
|
|
557
|
-
ok: ok2,
|
|
558
|
-
detail: ok2 ? "broker connected" : "broker not connected",
|
|
559
|
-
checkedAt: Date.now()
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
requestPermissionVerdict(req, signal) {
|
|
563
|
-
const client = this.client;
|
|
564
|
-
if (!client || !client.isReady()) {
|
|
565
|
-
return Promise.resolve({ kind: "no_relay" });
|
|
566
|
-
}
|
|
567
|
-
if (signal.aborted) {
|
|
568
|
-
return Promise.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
569
|
-
}
|
|
570
|
-
client.sendFrame({
|
|
571
|
-
kind: "console.permission.request",
|
|
572
|
-
frameId: makeFrameId2(),
|
|
573
|
-
sentAt: Date.now(),
|
|
574
|
-
address: {
|
|
575
|
-
runnerId: this.opts.runnerId,
|
|
576
|
-
...this.opts.workspaceId !== void 0 ? { workspaceId: this.opts.workspaceId } : {}
|
|
577
|
-
},
|
|
578
|
-
channelRequestId: req.channelRequestId,
|
|
579
|
-
toolName: req.toolName,
|
|
580
|
-
description: req.description,
|
|
581
|
-
inputPreview: req.inputPreview
|
|
582
|
-
});
|
|
583
|
-
return new Promise((resolve) => {
|
|
584
|
-
const abortListener = () => {
|
|
585
|
-
const entry = this.pendingPermissions.get(req.channelRequestId);
|
|
586
|
-
if (!entry) return;
|
|
587
|
-
this.pendingPermissions.delete(req.channelRequestId);
|
|
588
|
-
try {
|
|
589
|
-
this.client?.sendFrame({
|
|
590
|
-
kind: "console.permission.cancel",
|
|
591
|
-
frameId: makeFrameId2(),
|
|
592
|
-
sentAt: Date.now(),
|
|
593
|
-
channelRequestId: req.channelRequestId,
|
|
594
|
-
reason: "resolved_by_other_channel"
|
|
595
|
-
});
|
|
596
|
-
} catch {
|
|
597
|
-
}
|
|
598
|
-
resolve({ kind: "cancelled", reason: "resolved_by_other_channel" });
|
|
599
|
-
};
|
|
600
|
-
signal.addEventListener("abort", abortListener);
|
|
601
|
-
this.pendingPermissions.set(req.channelRequestId, {
|
|
602
|
-
resolve,
|
|
603
|
-
abortListener,
|
|
604
|
-
signal
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
settlePermissionResponse(channelRequestId, decision) {
|
|
609
|
-
const entry = this.pendingPermissions.get(channelRequestId);
|
|
610
|
-
if (!entry) return;
|
|
611
|
-
this.pendingPermissions.delete(channelRequestId);
|
|
612
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
613
|
-
entry.resolve({ kind: "verdict", behavior: decision, channelId: this.id });
|
|
614
|
-
}
|
|
615
|
-
disposePermissions(reason = "auto_resolved") {
|
|
616
|
-
for (const [id, entry] of [...this.pendingPermissions.entries()]) {
|
|
617
|
-
this.pendingPermissions.delete(id);
|
|
618
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
619
|
-
entry.resolve({ kind: "cancelled", reason });
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
requestQuestionAnswer(req, signal) {
|
|
623
|
-
const client = this.client;
|
|
624
|
-
if (!client || !client.isReady()) {
|
|
625
|
-
return Promise.resolve({ kind: "no_relay" });
|
|
626
|
-
}
|
|
627
|
-
if (signal.aborted) {
|
|
628
|
-
return Promise.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
629
|
-
}
|
|
630
|
-
client.sendFrame({
|
|
631
|
-
kind: "console.question.request",
|
|
632
|
-
frameId: makeFrameId2(),
|
|
633
|
-
sentAt: Date.now(),
|
|
634
|
-
address: {
|
|
635
|
-
runnerId: this.opts.runnerId,
|
|
636
|
-
...this.opts.workspaceId !== void 0 ? { workspaceId: this.opts.workspaceId } : {}
|
|
637
|
-
},
|
|
638
|
-
channelRequestId: req.channelRequestId,
|
|
639
|
-
title: req.title,
|
|
640
|
-
questions: req.questions
|
|
641
|
-
});
|
|
642
|
-
return new Promise((resolve) => {
|
|
643
|
-
const abortListener = () => {
|
|
644
|
-
const entry = this.pendingQuestions.get(req.channelRequestId);
|
|
645
|
-
if (!entry) return;
|
|
646
|
-
this.pendingQuestions.delete(req.channelRequestId);
|
|
647
|
-
try {
|
|
648
|
-
this.client?.sendFrame({
|
|
649
|
-
kind: "console.question.cancel",
|
|
650
|
-
frameId: makeFrameId2(),
|
|
651
|
-
sentAt: Date.now(),
|
|
652
|
-
channelRequestId: req.channelRequestId,
|
|
653
|
-
reason: "resolved_by_other_channel"
|
|
654
|
-
});
|
|
655
|
-
} catch {
|
|
656
|
-
}
|
|
657
|
-
resolve({ kind: "cancelled", reason: "resolved_by_other_channel" });
|
|
658
|
-
};
|
|
659
|
-
signal.addEventListener("abort", abortListener);
|
|
660
|
-
this.pendingQuestions.set(req.channelRequestId, {
|
|
661
|
-
questionKeys: req.questions.map((q) => q.key),
|
|
662
|
-
resolve,
|
|
663
|
-
abortListener,
|
|
664
|
-
signal
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
settleQuestionResponse(channelRequestId, answers) {
|
|
669
|
-
const entry = this.pendingQuestions.get(channelRequestId);
|
|
670
|
-
if (!entry) return;
|
|
671
|
-
const filtered = {};
|
|
672
|
-
for (const key of entry.questionKeys) {
|
|
673
|
-
const value = answers[key];
|
|
674
|
-
if (typeof value === "string") filtered[key] = value;
|
|
675
|
-
}
|
|
676
|
-
this.pendingQuestions.delete(channelRequestId);
|
|
677
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
678
|
-
if (Object.keys(filtered).length === 0) {
|
|
679
|
-
entry.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
entry.resolve({ kind: "answer", answers: filtered, channelId: this.id });
|
|
683
|
-
}
|
|
684
|
-
disposeQuestions(reason = "auto_resolved") {
|
|
685
|
-
for (const [id, entry] of [...this.pendingQuestions.entries()]) {
|
|
686
|
-
this.pendingQuestions.delete(id);
|
|
687
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
688
|
-
entry.resolve({ kind: "cancelled", reason });
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
handleInboundFrame(frame) {
|
|
692
|
-
switch (frame.kind) {
|
|
693
|
-
case "console.message.in":
|
|
694
|
-
this.handleInboundMessage(frame);
|
|
695
|
-
return;
|
|
696
|
-
case "console.permission.response":
|
|
697
|
-
this.handlePermissionResponse(frame);
|
|
698
|
-
return;
|
|
699
|
-
case "console.question.response":
|
|
700
|
-
this.handleQuestionResponse(frame);
|
|
701
|
-
return;
|
|
702
|
-
default:
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
handleInboundMessage(frame) {
|
|
707
|
-
if (frame.kind !== "console.message.in") return;
|
|
708
|
-
if (!isValidConsoleAddress(frame.address)) {
|
|
709
|
-
this.ctx?.log("warn", "console.message.in dropped: invalid address");
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (frame.address.runnerId !== this.opts.runnerId) {
|
|
713
|
-
this.ctx?.log(
|
|
714
|
-
"warn",
|
|
715
|
-
`console.message.in dropped: runner mismatch (claimed ${frame.address.runnerId}, expected ${this.opts.runnerId})`
|
|
716
|
-
);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (typeof frame.messageId !== "string" || frame.messageId.length === 0) {
|
|
720
|
-
this.ctx?.log("warn", "console.message.in dropped: missing messageId");
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
const inbound = normalizeInbound(frame, this.opts.runnerId, this.id);
|
|
724
|
-
if (!inbound || !this.ctx) return;
|
|
725
|
-
try {
|
|
726
|
-
this.ctx.emitInbound(inbound);
|
|
727
|
-
} catch (err) {
|
|
728
|
-
this.ctx.log(
|
|
729
|
-
"warn",
|
|
730
|
-
`console emitInbound threw: ${err instanceof Error ? err.message : String(err)}`
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
handlePermissionResponse(frame) {
|
|
735
|
-
if (frame.kind !== "console.permission.response") return;
|
|
736
|
-
if (typeof frame.channelRequestId !== "string" || frame.channelRequestId.length === 0) {
|
|
737
|
-
this.ctx?.log(
|
|
738
|
-
"warn",
|
|
739
|
-
"console.permission.response dropped: missing channelRequestId"
|
|
740
|
-
);
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
if (frame.decision !== "allow" && frame.decision !== "deny") {
|
|
744
|
-
this.ctx?.log(
|
|
745
|
-
"warn",
|
|
746
|
-
`console.permission.response dropped: invalid decision ${String(frame.decision)}`
|
|
747
|
-
);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
this.settlePermissionResponse(frame.channelRequestId, frame.decision);
|
|
751
|
-
}
|
|
752
|
-
handleQuestionResponse(frame) {
|
|
753
|
-
if (frame.kind !== "console.question.response") return;
|
|
754
|
-
if (typeof frame.channelRequestId !== "string" || frame.channelRequestId.length === 0) {
|
|
755
|
-
this.ctx?.log(
|
|
756
|
-
"warn",
|
|
757
|
-
"console.question.response dropped: missing channelRequestId"
|
|
758
|
-
);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (typeof frame.answers !== "object" || frame.answers === null || Array.isArray(frame.answers)) {
|
|
762
|
-
this.ctx?.log(
|
|
763
|
-
"warn",
|
|
764
|
-
"console.question.response dropped: answers must be a record"
|
|
765
|
-
);
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
const cleaned = {};
|
|
769
|
-
for (const [key, value] of Object.entries(frame.answers)) {
|
|
770
|
-
if (typeof key !== "string" || key.length === 0) continue;
|
|
771
|
-
if (typeof value !== "string") continue;
|
|
772
|
-
cleaned[key] = value;
|
|
773
|
-
}
|
|
774
|
-
this.settleQuestionResponse(frame.channelRequestId, cleaned);
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
function resolvePairingTokenSource(opts) {
|
|
778
|
-
if (opts.dashboardConfig === true) {
|
|
779
|
-
const provider = opts.pairingTokenProvider ?? (async () => {
|
|
780
|
-
const result = await refreshDashboardAccessToken();
|
|
781
|
-
return result.accessToken;
|
|
782
|
-
});
|
|
783
|
-
return { kind: "provider", provider };
|
|
784
|
-
}
|
|
785
|
-
if (opts.pairingToken !== void 0 && opts.pairingToken.length > 0) {
|
|
786
|
-
return { kind: "static", token: opts.pairingToken };
|
|
787
|
-
}
|
|
788
|
-
if (opts.tokenPath !== void 0 && opts.tokenPath.length > 0) {
|
|
789
|
-
try {
|
|
790
|
-
const value = readFileSync2(opts.tokenPath, "utf-8").trim();
|
|
791
|
-
if (value.length === 0) {
|
|
792
|
-
throw new Error(
|
|
793
|
-
`console adapter: token_path ${opts.tokenPath} is empty`
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
return { kind: "static", token: value };
|
|
797
|
-
} catch (err) {
|
|
798
|
-
const code = err.code;
|
|
799
|
-
throw new Error(
|
|
800
|
-
`console adapter: failed to read token_path ${opts.tokenPath}` + (code ? ` (${code})` : "") + (err instanceof Error ? `: ${err.message}` : "")
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
throw new Error(
|
|
805
|
-
"console adapter: no pairing_token, token_path, or dashboard_config configured"
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
var outboundCounter = 0;
|
|
809
|
-
function makeOutboundMessageId() {
|
|
810
|
-
outboundCounter = (outboundCounter + 1) % 1e6;
|
|
811
|
-
return `console-out-${Date.now().toString(36)}-${outboundCounter.toString(36)}`;
|
|
812
|
-
}
|
|
813
|
-
var frameCounter2 = 0;
|
|
814
|
-
function makeFrameId2() {
|
|
815
|
-
frameCounter2 = (frameCounter2 + 1) % 1e6;
|
|
816
|
-
return `f${Date.now().toString(36)}-${frameCounter2.toString(36)}`;
|
|
817
|
-
}
|
|
818
|
-
function isValidConsoleAddress(value) {
|
|
819
|
-
if (typeof value !== "object" || value === null) return false;
|
|
820
|
-
const v = value;
|
|
821
|
-
if (typeof v["runnerId"] !== "string" || v["runnerId"].length === 0) {
|
|
822
|
-
return false;
|
|
823
|
-
}
|
|
824
|
-
for (const key of [
|
|
825
|
-
"workspaceId",
|
|
826
|
-
"conversationId",
|
|
827
|
-
"threadId",
|
|
828
|
-
"userId"
|
|
829
|
-
]) {
|
|
830
|
-
if (v[key] !== void 0 && typeof v[key] !== "string") return false;
|
|
831
|
-
}
|
|
832
|
-
return true;
|
|
833
|
-
}
|
|
834
|
-
function normalizeInbound(frame, runnerId, channelId) {
|
|
835
|
-
if (typeof frame.text !== "string" || frame.text.length === 0) return null;
|
|
836
|
-
const userId = frame.address.userId ?? "console-user";
|
|
837
|
-
const idempotencyKey = typeof frame.idempotencyKey === "string" && frame.idempotencyKey.length > 0 ? frame.idempotencyKey : `console:${runnerId}:${frame.messageId}`;
|
|
838
|
-
return {
|
|
839
|
-
location: {
|
|
840
|
-
channelId,
|
|
841
|
-
accountId: frame.address.workspaceId ?? runnerId,
|
|
842
|
-
peer: { id: userId, kind: "user" },
|
|
843
|
-
...frame.address.threadId !== void 0 ? { thread: { id: frame.address.threadId } } : frame.address.conversationId !== void 0 ? { thread: { id: frame.address.conversationId } } : {}
|
|
844
|
-
},
|
|
845
|
-
sender: { id: userId },
|
|
846
|
-
text: frame.text,
|
|
847
|
-
receivedAt: frame.sentAt,
|
|
848
|
-
idempotencyKey,
|
|
849
|
-
providerMessageId: frame.messageId
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// src/gateway/adapters/console/module.ts
|
|
854
|
-
var consoleModule = {
|
|
855
|
-
name: "console",
|
|
856
|
-
parseConfig({ options }) {
|
|
857
|
-
const brokerUrl = options["broker_url"];
|
|
858
|
-
if (typeof brokerUrl !== "string" || brokerUrl.length === 0) {
|
|
859
|
-
return { ok: false, reason: "broker_url missing" };
|
|
860
|
-
}
|
|
861
|
-
if (!/^wss?:\/\//.test(brokerUrl)) {
|
|
862
|
-
return { ok: false, reason: "broker_url must start with ws:// or wss://" };
|
|
863
|
-
}
|
|
864
|
-
if (brokerUrl.startsWith("ws://") && !isLoopbackUrl(brokerUrl)) {
|
|
865
|
-
return {
|
|
866
|
-
ok: false,
|
|
867
|
-
reason: "broker_url must use wss:// for non-loopback hosts"
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
const runnerId = options["runner_id"];
|
|
871
|
-
if (typeof runnerId !== "string" || runnerId.length === 0) {
|
|
872
|
-
return { ok: false, reason: "runner_id missing" };
|
|
873
|
-
}
|
|
874
|
-
const pairingToken = options["pairing_token"];
|
|
875
|
-
const tokenPath = options["token_path"];
|
|
876
|
-
const dashboardConfig = options["dashboard_config"];
|
|
877
|
-
if (dashboardConfig !== void 0 && typeof dashboardConfig !== "boolean") {
|
|
878
|
-
return { ok: false, reason: "dashboard_config must be a boolean" };
|
|
879
|
-
}
|
|
880
|
-
const useDashboardConfig = dashboardConfig === true;
|
|
881
|
-
const hasInline = typeof pairingToken === "string" && pairingToken.length > 0;
|
|
882
|
-
const hasTokenPath = typeof tokenPath === "string" && tokenPath.length > 0;
|
|
883
|
-
if (useDashboardConfig && (hasInline || hasTokenPath)) {
|
|
884
|
-
return {
|
|
885
|
-
ok: false,
|
|
886
|
-
reason: "dashboard_config is mutually exclusive with pairing_token and token_path"
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
if (!useDashboardConfig && !hasInline && !hasTokenPath) {
|
|
890
|
-
return {
|
|
891
|
-
ok: false,
|
|
892
|
-
reason: "either pairing_token, token_path, or dashboard_config is required"
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
if (pairingToken !== void 0 && typeof pairingToken !== "string") {
|
|
896
|
-
return { ok: false, reason: "pairing_token must be a string" };
|
|
897
|
-
}
|
|
898
|
-
if (tokenPath !== void 0 && typeof tokenPath !== "string") {
|
|
899
|
-
return { ok: false, reason: "token_path must be a string" };
|
|
900
|
-
}
|
|
901
|
-
const workspaceId = options["workspace_id"];
|
|
902
|
-
if (workspaceId !== void 0 && typeof workspaceId !== "string") {
|
|
903
|
-
return { ok: false, reason: "workspace_id must be a string" };
|
|
904
|
-
}
|
|
905
|
-
const tlsCaPath = options["tls_ca_path"];
|
|
906
|
-
if (tlsCaPath !== void 0 && typeof tlsCaPath !== "string") {
|
|
907
|
-
return { ok: false, reason: "tls_ca_path must be a string" };
|
|
908
|
-
}
|
|
909
|
-
const config = {
|
|
910
|
-
brokerUrl,
|
|
911
|
-
runnerId,
|
|
912
|
-
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
913
|
-
...useDashboardConfig ? { dashboardConfig: true } : {
|
|
914
|
-
...pairingToken !== void 0 ? { pairingToken } : {},
|
|
915
|
-
...tokenPath !== void 0 ? { tokenPath } : {}
|
|
916
|
-
},
|
|
917
|
-
...tlsCaPath !== void 0 ? { tlsCaPath } : {}
|
|
918
|
-
};
|
|
919
|
-
return { ok: true, config };
|
|
920
|
-
},
|
|
921
|
-
create(config, instanceId) {
|
|
922
|
-
return new ConsoleAdapter(config, instanceId);
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
function isLoopbackUrl(url) {
|
|
926
|
-
try {
|
|
927
|
-
const parsed = new URL(url);
|
|
928
|
-
const host = parsed.hostname;
|
|
929
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
930
|
-
} catch {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// src/shared/utils/errorMessage.ts
|
|
936
|
-
function errorMessage(err) {
|
|
937
|
-
return err instanceof Error ? err.message : String(err);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// src/shared/telegram/bot.ts
|
|
941
|
-
var TelegramApiError = class extends Error {
|
|
942
|
-
constructor(status, message, retryAfterMs) {
|
|
943
|
-
super(message);
|
|
944
|
-
this.status = status;
|
|
945
|
-
this.retryAfterMs = retryAfterMs;
|
|
946
|
-
this.name = "TelegramApiError";
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
var TelegramBot = class {
|
|
950
|
-
token;
|
|
951
|
-
apiBase;
|
|
952
|
-
pollTimeoutSec;
|
|
953
|
-
offset = 0;
|
|
954
|
-
stopped = false;
|
|
955
|
-
log;
|
|
956
|
-
consecutiveAuthFailures = 0;
|
|
957
|
-
consecutiveErrors = 0;
|
|
958
|
-
constructor(opts, log) {
|
|
959
|
-
this.token = opts.token;
|
|
960
|
-
this.apiBase = opts.apiBase ?? "https://api.telegram.org";
|
|
961
|
-
this.pollTimeoutSec = opts.pollTimeoutSec ?? 25;
|
|
962
|
-
this.log = log;
|
|
963
|
-
}
|
|
964
|
-
/** Strip the bot token from any string before logging or surfacing. */
|
|
965
|
-
redact(text) {
|
|
966
|
-
if (!this.token) return text;
|
|
967
|
-
return text.split(this.token).join("<redacted>");
|
|
968
|
-
}
|
|
969
|
-
stop() {
|
|
970
|
-
this.stopped = true;
|
|
971
|
-
}
|
|
972
|
-
isStopped() {
|
|
973
|
-
return this.stopped;
|
|
974
|
-
}
|
|
975
|
-
/** Current update offset; persist to skip already-seen updates on restart. */
|
|
976
|
-
getOffset() {
|
|
977
|
-
return this.offset;
|
|
978
|
-
}
|
|
979
|
-
setOffset(offset) {
|
|
980
|
-
if (offset > this.offset) this.offset = offset;
|
|
981
|
-
}
|
|
982
|
-
async *poll() {
|
|
983
|
-
while (!this.isStopped()) {
|
|
984
|
-
try {
|
|
985
|
-
const updates = await this.getUpdates();
|
|
986
|
-
this.consecutiveAuthFailures = 0;
|
|
987
|
-
this.consecutiveErrors = 0;
|
|
988
|
-
for (const update of updates) {
|
|
989
|
-
if (this.isStopped()) return;
|
|
990
|
-
if (update.update_id >= this.offset) {
|
|
991
|
-
this.offset = update.update_id + 1;
|
|
992
|
-
}
|
|
993
|
-
yield update;
|
|
994
|
-
}
|
|
995
|
-
} catch (err) {
|
|
996
|
-
const message = this.redact(errorMessage(err));
|
|
997
|
-
const apiErr = err instanceof TelegramApiError ? err : void 0;
|
|
998
|
-
const status = apiErr?.status;
|
|
999
|
-
const retryAfterMs = apiErr?.retryAfterMs;
|
|
1000
|
-
if (status === 401 || status === 409) {
|
|
1001
|
-
this.consecutiveAuthFailures++;
|
|
1002
|
-
this.log(
|
|
1003
|
-
"error",
|
|
1004
|
-
`getUpdates failed (HTTP ${status}, attempt ${this.consecutiveAuthFailures}): ${message}`
|
|
1005
|
-
);
|
|
1006
|
-
if (this.consecutiveAuthFailures >= 3) {
|
|
1007
|
-
this.stopped = true;
|
|
1008
|
-
throw new Error(
|
|
1009
|
-
`telegram channel: persistent HTTP ${status} from getUpdates after 3 attempts`
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
} else if (status === 429 && retryAfterMs) {
|
|
1013
|
-
this.log(
|
|
1014
|
-
"warn",
|
|
1015
|
-
`getUpdates rate-limited; sleeping ${retryAfterMs}ms`
|
|
1016
|
-
);
|
|
1017
|
-
await sleep(retryAfterMs);
|
|
1018
|
-
continue;
|
|
1019
|
-
} else {
|
|
1020
|
-
this.consecutiveErrors++;
|
|
1021
|
-
this.log("warn", `getUpdates failed: ${message}`);
|
|
1022
|
-
}
|
|
1023
|
-
const backoffMs = Math.min(
|
|
1024
|
-
1500 * Math.pow(2, Math.max(0, this.consecutiveErrors - 1)),
|
|
1025
|
-
3e4
|
|
1026
|
-
);
|
|
1027
|
-
await sleep(backoffMs);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
async sendMessage(chatId, text, options = {}) {
|
|
1032
|
-
try {
|
|
1033
|
-
const params = { chat_id: chatId, text };
|
|
1034
|
-
if (options.parse_mode) params["parse_mode"] = options.parse_mode;
|
|
1035
|
-
if (options.reply_markup) params["reply_markup"] = options.reply_markup;
|
|
1036
|
-
if (options.message_thread_id !== void 0)
|
|
1037
|
-
params["message_thread_id"] = options.message_thread_id;
|
|
1038
|
-
const result = await this.call("sendMessage", params);
|
|
1039
|
-
return result;
|
|
1040
|
-
} catch (err) {
|
|
1041
|
-
this.log("warn", `sendMessage failed: ${this.redact(errorMessage(err))}`);
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
async createForumTopic(chatId, name) {
|
|
1046
|
-
try {
|
|
1047
|
-
return await this.call("createForumTopic", {
|
|
1048
|
-
chat_id: chatId,
|
|
1049
|
-
name
|
|
1050
|
-
});
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
this.log(
|
|
1053
|
-
"warn",
|
|
1054
|
-
`createForumTopic failed: ${this.redact(errorMessage(err))}`
|
|
1055
|
-
);
|
|
1056
|
-
return null;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
async editForumTopic(chatId, messageThreadId, name) {
|
|
1060
|
-
try {
|
|
1061
|
-
await this.call("editForumTopic", {
|
|
1062
|
-
chat_id: chatId,
|
|
1063
|
-
message_thread_id: messageThreadId,
|
|
1064
|
-
name
|
|
1065
|
-
});
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
this.log(
|
|
1068
|
-
"debug",
|
|
1069
|
-
`editForumTopic failed: ${this.redact(errorMessage(err))}`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
async closeForumTopic(chatId, messageThreadId) {
|
|
1074
|
-
try {
|
|
1075
|
-
await this.call("closeForumTopic", {
|
|
1076
|
-
chat_id: chatId,
|
|
1077
|
-
message_thread_id: messageThreadId
|
|
1078
|
-
});
|
|
1079
|
-
} catch (err) {
|
|
1080
|
-
this.log(
|
|
1081
|
-
"debug",
|
|
1082
|
-
`closeForumTopic failed: ${this.redact(errorMessage(err))}`
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
async editMessageText(chatId, messageId, text, options = {}) {
|
|
1087
|
-
try {
|
|
1088
|
-
const params = {
|
|
1089
|
-
chat_id: chatId,
|
|
1090
|
-
message_id: messageId,
|
|
1091
|
-
text
|
|
1092
|
-
};
|
|
1093
|
-
if (options.parse_mode) params["parse_mode"] = options.parse_mode;
|
|
1094
|
-
if (options.reply_markup) params["reply_markup"] = options.reply_markup;
|
|
1095
|
-
await this.call("editMessageText", params);
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
this.log(
|
|
1098
|
-
"debug",
|
|
1099
|
-
`editMessageText failed: ${this.redact(errorMessage(err))}`
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
async editMessageReplyMarkup(chatId, messageId, replyMarkup) {
|
|
1104
|
-
try {
|
|
1105
|
-
await this.call("editMessageReplyMarkup", {
|
|
1106
|
-
chat_id: chatId,
|
|
1107
|
-
message_id: messageId,
|
|
1108
|
-
reply_markup: replyMarkup ?? { inline_keyboard: [] }
|
|
1109
|
-
});
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
this.log(
|
|
1112
|
-
"debug",
|
|
1113
|
-
`editMessageReplyMarkup failed: ${this.redact(errorMessage(err))}`
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
async answerCallbackQuery(callbackQueryId, text) {
|
|
1118
|
-
try {
|
|
1119
|
-
const params = {
|
|
1120
|
-
callback_query_id: callbackQueryId
|
|
1121
|
-
};
|
|
1122
|
-
if (text) params["text"] = text;
|
|
1123
|
-
await this.call("answerCallbackQuery", params);
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
this.log(
|
|
1126
|
-
"debug",
|
|
1127
|
-
`answerCallbackQuery failed: ${this.redact(errorMessage(err))}`
|
|
1128
|
-
);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
async setMyCommands(commands) {
|
|
1132
|
-
try {
|
|
1133
|
-
await this.call("setMyCommands", { commands });
|
|
1134
|
-
} catch (err) {
|
|
1135
|
-
this.log(
|
|
1136
|
-
"debug",
|
|
1137
|
-
`setMyCommands failed: ${this.redact(errorMessage(err))}`
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
async getUpdates() {
|
|
1142
|
-
const result = await this.call("getUpdates", {
|
|
1143
|
-
offset: this.offset,
|
|
1144
|
-
timeout: this.pollTimeoutSec,
|
|
1145
|
-
allowed_updates: ["message", "callback_query"]
|
|
1146
|
-
});
|
|
1147
|
-
return result;
|
|
1148
|
-
}
|
|
1149
|
-
async call(method, params) {
|
|
1150
|
-
const url = `${this.apiBase}/bot${this.token}/${method}`;
|
|
1151
|
-
const ctrl = new AbortController();
|
|
1152
|
-
const timeoutMs = (this.pollTimeoutSec + 5) * 1e3 * 2;
|
|
1153
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
1154
|
-
let res;
|
|
1155
|
-
try {
|
|
1156
|
-
res = await fetch(url, {
|
|
1157
|
-
method: "POST",
|
|
1158
|
-
headers: { "Content-Type": "application/json" },
|
|
1159
|
-
body: JSON.stringify(params),
|
|
1160
|
-
signal: ctrl.signal
|
|
1161
|
-
});
|
|
1162
|
-
} finally {
|
|
1163
|
-
clearTimeout(timer);
|
|
1164
|
-
}
|
|
1165
|
-
if (!res.ok) {
|
|
1166
|
-
let retryAfterSec;
|
|
1167
|
-
try {
|
|
1168
|
-
const body = await res.json();
|
|
1169
|
-
retryAfterSec = body.parameters?.retry_after;
|
|
1170
|
-
} catch {
|
|
1171
|
-
}
|
|
1172
|
-
throw new TelegramApiError(
|
|
1173
|
-
res.status,
|
|
1174
|
-
`HTTP ${res.status}`,
|
|
1175
|
-
typeof retryAfterSec === "number" ? retryAfterSec * 1e3 : void 0
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
const json = await res.json();
|
|
1179
|
-
if (!json.ok) {
|
|
1180
|
-
throw new Error(json.description ?? "telegram api returned ok=false");
|
|
1181
|
-
}
|
|
1182
|
-
return json.result;
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
function sleep(ms) {
|
|
1186
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// src/shared/telegram/markdown.ts
|
|
1190
|
-
function escapeMarkdownV2(text) {
|
|
1191
|
-
return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
1192
|
-
}
|
|
1193
|
-
function escapeMarkdownV2CodeBlock(text) {
|
|
1194
|
-
return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// src/gateway/adapters/telegram/verdict.ts
|
|
1198
|
-
var CB_PERMISSION = "v";
|
|
1199
|
-
var CB_QUESTION = "q";
|
|
1200
|
-
var CB_ALLOW = "a";
|
|
1201
|
-
var CB_DENY = "d";
|
|
1202
|
-
var CB_VERDICT = {
|
|
1203
|
-
allow: CB_ALLOW,
|
|
1204
|
-
deny: CB_DENY
|
|
1205
|
-
};
|
|
1206
|
-
var ID_PATTERN = CHANNEL_REQUEST_ID_REGEX.source.replace(/^\^|\$$/g, "");
|
|
1207
|
-
var VERDICT_RE = new RegExp(`^\\s*(y|yes|n|no)\\s+(${ID_PATTERN})\\s*$`, "i");
|
|
1208
|
-
var ANSWER_RE = new RegExp(
|
|
1209
|
-
`^\\s*(a|answer)\\s+(${ID_PATTERN})\\s+([\\s\\S]+?)\\s*$`,
|
|
1210
|
-
"i"
|
|
1211
|
-
);
|
|
1212
|
-
var ANSWER_ID_RE = new RegExp(`^\\s*(a|answer)\\s+(${ID_PATTERN})\\s+`, "i");
|
|
1213
|
-
var NON_NEG_INT_RE = /^\d+$/;
|
|
1214
|
-
var MAX_JSON_ANSWER_BYTES = 8 * 1024;
|
|
1215
|
-
function parseVerdict(text) {
|
|
1216
|
-
const m = VERDICT_RE.exec(text);
|
|
1217
|
-
if (!m) return null;
|
|
1218
|
-
const verdictWord = m[1].toLowerCase();
|
|
1219
|
-
const id = m[2].toLowerCase();
|
|
1220
|
-
return {
|
|
1221
|
-
channelRequestId: id,
|
|
1222
|
-
behavior: verdictWord.startsWith("y") ? "allow" : "deny"
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
function parseQuestionAnswer(text, questionKeys) {
|
|
1226
|
-
const m = ANSWER_RE.exec(text);
|
|
1227
|
-
if (!m) return null;
|
|
1228
|
-
const channelRequestId = m[2].toLowerCase();
|
|
1229
|
-
const rawAnswer = m[3].trim();
|
|
1230
|
-
if (rawAnswer.length === 0) return null;
|
|
1231
|
-
if (rawAnswer.startsWith("{")) {
|
|
1232
|
-
if (rawAnswer.length > MAX_JSON_ANSWER_BYTES) return null;
|
|
1233
|
-
try {
|
|
1234
|
-
const parsed = JSON.parse(rawAnswer);
|
|
1235
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
const answers = {};
|
|
1239
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
1240
|
-
if (typeof value !== "string") return null;
|
|
1241
|
-
answers[key] = value;
|
|
1242
|
-
}
|
|
1243
|
-
return { channelRequestId, answers };
|
|
1244
|
-
} catch {
|
|
1245
|
-
return null;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
const firstKey = questionKeys[0];
|
|
1249
|
-
if (!firstKey) return null;
|
|
1250
|
-
return {
|
|
1251
|
-
channelRequestId,
|
|
1252
|
-
answers: { [firstKey]: rawAnswer }
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
function parseQuestionAnswerId(text) {
|
|
1256
|
-
const m = ANSWER_ID_RE.exec(text);
|
|
1257
|
-
return m ? m[2].toLowerCase() : null;
|
|
1258
|
-
}
|
|
1259
|
-
function buildPlainTextQuestionAnswer(channelRequestId, text, questionKeys) {
|
|
1260
|
-
const answer = text.trim();
|
|
1261
|
-
const firstKey = questionKeys[0];
|
|
1262
|
-
if (!firstKey || answer.length === 0) return null;
|
|
1263
|
-
return {
|
|
1264
|
-
channelRequestId,
|
|
1265
|
-
answers: { [firstKey]: answer }
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
function parseCallbackData(data) {
|
|
1269
|
-
const parts = data.split(":");
|
|
1270
|
-
const kind = parts[0];
|
|
1271
|
-
const id = parts[1];
|
|
1272
|
-
if (!id || !isValidChannelRequestId(id)) return null;
|
|
1273
|
-
if (kind === CB_PERMISSION && parts.length === 3) {
|
|
1274
|
-
const verb = parts[2];
|
|
1275
|
-
if (verb !== CB_ALLOW && verb !== CB_DENY) return null;
|
|
1276
|
-
return {
|
|
1277
|
-
kind: "permission",
|
|
1278
|
-
channelRequestId: id,
|
|
1279
|
-
behavior: verb === CB_ALLOW ? "allow" : "deny"
|
|
1280
|
-
};
|
|
1281
|
-
}
|
|
1282
|
-
if (kind === CB_QUESTION && parts.length === 3) {
|
|
1283
|
-
const idxStr = parts[2];
|
|
1284
|
-
if (!NON_NEG_INT_RE.test(idxStr)) return null;
|
|
1285
|
-
const optionIndex = Number.parseInt(idxStr, 10);
|
|
1286
|
-
return { kind: "question", channelRequestId: id, optionIndex };
|
|
1287
|
-
}
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
function buildPermissionCallbackData(channelRequestId, behavior) {
|
|
1291
|
-
return `${CB_PERMISSION}:${channelRequestId}:${CB_VERDICT[behavior]}`;
|
|
1292
|
-
}
|
|
1293
|
-
function buildQuestionCallbackData(channelRequestId, optionIndex) {
|
|
1294
|
-
return `${CB_QUESTION}:${channelRequestId}:${optionIndex}`;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// src/gateway/adapters/telegram/relay.ts
|
|
1298
|
-
var MD_OPTIONS = {
|
|
1299
|
-
parse_mode: "MarkdownV2"
|
|
1300
|
-
};
|
|
1301
|
-
var TELEGRAM_MAX_TEXT = 4096;
|
|
1302
|
-
var TELEGRAM_TEXT_SAFE_MARGIN = 96;
|
|
1303
|
-
var TelegramRelay = class {
|
|
1304
|
-
pending = /* @__PURE__ */ new Map();
|
|
1305
|
-
resolveTarget;
|
|
1306
|
-
log;
|
|
1307
|
-
bot = null;
|
|
1308
|
-
constructor(opts) {
|
|
1309
|
-
this.resolveTarget = opts.resolveTarget;
|
|
1310
|
-
this.log = opts.log;
|
|
1311
|
-
}
|
|
1312
|
-
bindBot(bot) {
|
|
1313
|
-
this.bot = bot;
|
|
1314
|
-
}
|
|
1315
|
-
async requestPermission(req, signal) {
|
|
1316
|
-
const bot = this.bot;
|
|
1317
|
-
const target = this.resolveTarget();
|
|
1318
|
-
if (!bot || !target) {
|
|
1319
|
-
return { kind: "no_relay" };
|
|
1320
|
-
}
|
|
1321
|
-
if (signal.aborted) {
|
|
1322
|
-
return { kind: "cancelled", reason: "auto_resolved" };
|
|
1323
|
-
}
|
|
1324
|
-
const headline = `${req.toolName} \u2014 ${req.description}`;
|
|
1325
|
-
const text = buildPromptMarkdown(
|
|
1326
|
-
req.toolName,
|
|
1327
|
-
req.description,
|
|
1328
|
-
req.inputPreview,
|
|
1329
|
-
req.channelRequestId
|
|
1330
|
-
);
|
|
1331
|
-
const reply_markup = buildPermissionKeyboard(req.channelRequestId);
|
|
1332
|
-
const sent = await bot.sendMessage(target.chatId, text, {
|
|
1333
|
-
...MD_OPTIONS,
|
|
1334
|
-
reply_markup,
|
|
1335
|
-
...target.threadId !== void 0 ? { message_thread_id: target.threadId } : {}
|
|
1336
|
-
});
|
|
1337
|
-
if (!sent) {
|
|
1338
|
-
return { kind: "no_relay" };
|
|
1339
|
-
}
|
|
1340
|
-
return new Promise((resolve) => {
|
|
1341
|
-
const abortListener = () => {
|
|
1342
|
-
const entry = this.pending.get(req.channelRequestId);
|
|
1343
|
-
if (!entry || entry.kind !== "permission") return;
|
|
1344
|
-
this.pending.delete(req.channelRequestId);
|
|
1345
|
-
void this.editToCancelled(entry);
|
|
1346
|
-
resolve({ kind: "cancelled", reason: "resolved_by_other_channel" });
|
|
1347
|
-
};
|
|
1348
|
-
signal.addEventListener("abort", abortListener);
|
|
1349
|
-
this.pending.set(req.channelRequestId, {
|
|
1350
|
-
kind: "permission",
|
|
1351
|
-
channelRequestId: req.channelRequestId,
|
|
1352
|
-
chatId: sent.chat.id,
|
|
1353
|
-
messageId: sent.message_id,
|
|
1354
|
-
headline,
|
|
1355
|
-
resolve,
|
|
1356
|
-
abortListener,
|
|
1357
|
-
signal
|
|
1358
|
-
});
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
async requestQuestion(req, signal) {
|
|
1362
|
-
const bot = this.bot;
|
|
1363
|
-
const target = this.resolveTarget();
|
|
1364
|
-
if (!bot || !target) {
|
|
1365
|
-
return { kind: "no_relay" };
|
|
1366
|
-
}
|
|
1367
|
-
if (signal.aborted) {
|
|
1368
|
-
return { kind: "cancelled", reason: "auto_resolved" };
|
|
1369
|
-
}
|
|
1370
|
-
const headline = req.title.trim() || "Question";
|
|
1371
|
-
const keyboard = buildQuestionKeyboard(req.channelRequestId, req.questions);
|
|
1372
|
-
const text = buildQuestionMarkdown(
|
|
1373
|
-
headline,
|
|
1374
|
-
req.questions,
|
|
1375
|
-
req.channelRequestId,
|
|
1376
|
-
keyboard !== null
|
|
1377
|
-
);
|
|
1378
|
-
const sent = await bot.sendMessage(target.chatId, text, {
|
|
1379
|
-
...MD_OPTIONS,
|
|
1380
|
-
...keyboard !== null ? { reply_markup: keyboard.markup } : {},
|
|
1381
|
-
...target.threadId !== void 0 ? { message_thread_id: target.threadId } : {}
|
|
1382
|
-
});
|
|
1383
|
-
if (!sent) {
|
|
1384
|
-
return { kind: "no_relay" };
|
|
1385
|
-
}
|
|
1386
|
-
return new Promise((resolve) => {
|
|
1387
|
-
const abortListener = () => {
|
|
1388
|
-
const entry = this.pending.get(req.channelRequestId);
|
|
1389
|
-
if (!entry || entry.kind !== "question") return;
|
|
1390
|
-
this.pending.delete(req.channelRequestId);
|
|
1391
|
-
void this.editToCancelled(entry);
|
|
1392
|
-
resolve({ kind: "cancelled", reason: "resolved_by_other_channel" });
|
|
1393
|
-
};
|
|
1394
|
-
signal.addEventListener("abort", abortListener);
|
|
1395
|
-
this.pending.set(req.channelRequestId, {
|
|
1396
|
-
kind: "question",
|
|
1397
|
-
channelRequestId: req.channelRequestId,
|
|
1398
|
-
chatId: sent.chat.id,
|
|
1399
|
-
messageId: sent.message_id,
|
|
1400
|
-
headline,
|
|
1401
|
-
questionKeys: req.questions.map((q) => q.key),
|
|
1402
|
-
buttonOptions: keyboard?.options ?? null,
|
|
1403
|
-
resolve,
|
|
1404
|
-
abortListener,
|
|
1405
|
-
signal
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
/** Returns true if the update was consumed by a relay (verdict/answer). */
|
|
1410
|
-
handleUpdate(update) {
|
|
1411
|
-
const bot = this.bot;
|
|
1412
|
-
if (!bot) return false;
|
|
1413
|
-
const cb = update.callback_query;
|
|
1414
|
-
if (cb?.data) {
|
|
1415
|
-
const parsed = parseCallbackData(cb.data);
|
|
1416
|
-
if (!parsed) return false;
|
|
1417
|
-
const entry = this.pending.get(parsed.channelRequestId);
|
|
1418
|
-
if (!entry) {
|
|
1419
|
-
void bot.answerCallbackQuery(cb.id, "request expired");
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
if (parsed.kind === "permission" && entry.kind === "permission") {
|
|
1423
|
-
this.settlePermission(entry, parsed.behavior);
|
|
1424
|
-
void bot.answerCallbackQuery(cb.id, parsed.behavior);
|
|
1425
|
-
return true;
|
|
1426
|
-
}
|
|
1427
|
-
if (parsed.kind === "question" && entry.kind === "question") {
|
|
1428
|
-
const opt = entry.buttonOptions?.[parsed.optionIndex];
|
|
1429
|
-
if (!opt) {
|
|
1430
|
-
void bot.answerCallbackQuery(cb.id, "unknown option");
|
|
1431
|
-
return true;
|
|
1432
|
-
}
|
|
1433
|
-
this.settleQuestion(entry, { [opt.key]: opt.label });
|
|
1434
|
-
void bot.answerCallbackQuery(cb.id, opt.label);
|
|
1435
|
-
return true;
|
|
1436
|
-
}
|
|
1437
|
-
return false;
|
|
1438
|
-
}
|
|
1439
|
-
const message = update.message ?? update.edited_message;
|
|
1440
|
-
const text = message?.text;
|
|
1441
|
-
if (typeof text !== "string" || text.length === 0) return false;
|
|
1442
|
-
const verdict = parseVerdict(text);
|
|
1443
|
-
if (verdict) {
|
|
1444
|
-
const entry = this.pending.get(verdict.channelRequestId);
|
|
1445
|
-
if (entry?.kind === "permission") {
|
|
1446
|
-
this.settlePermission(entry, verdict.behavior);
|
|
1447
|
-
return true;
|
|
1448
|
-
}
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
const answerId = parseQuestionAnswerId(text);
|
|
1452
|
-
if (answerId) {
|
|
1453
|
-
const entry = this.pending.get(answerId);
|
|
1454
|
-
if (entry?.kind === "question") {
|
|
1455
|
-
const parsed = parseQuestionAnswer(text, entry.questionKeys);
|
|
1456
|
-
if (!parsed) return false;
|
|
1457
|
-
this.settleQuestion(entry, parsed.answers);
|
|
1458
|
-
return true;
|
|
1459
|
-
}
|
|
1460
|
-
return false;
|
|
1461
|
-
}
|
|
1462
|
-
if (message && "reply_to_message" in message && message.reply_to_message) {
|
|
1463
|
-
const replyTo = message.reply_to_message.message_id;
|
|
1464
|
-
for (const entry of this.pending.values()) {
|
|
1465
|
-
if (entry.kind !== "question") continue;
|
|
1466
|
-
if (entry.messageId !== replyTo) continue;
|
|
1467
|
-
const parsed = buildPlainTextQuestionAnswer(
|
|
1468
|
-
entry.channelRequestId,
|
|
1469
|
-
text,
|
|
1470
|
-
entry.questionKeys
|
|
1471
|
-
);
|
|
1472
|
-
if (!parsed) return false;
|
|
1473
|
-
this.settleQuestion(entry, parsed.answers);
|
|
1474
|
-
return true;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
disposeAll() {
|
|
1480
|
-
for (const entry of [...this.pending.values()]) {
|
|
1481
|
-
this.pending.delete(entry.channelRequestId);
|
|
1482
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
1483
|
-
if (entry.kind === "permission") {
|
|
1484
|
-
entry.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
1485
|
-
} else {
|
|
1486
|
-
entry.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
settlePermission(entry, behavior) {
|
|
1491
|
-
this.pending.delete(entry.channelRequestId);
|
|
1492
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
1493
|
-
void this.editToResolved(
|
|
1494
|
-
entry,
|
|
1495
|
-
behavior === "allow" ? "Allowed" : "Denied"
|
|
1496
|
-
);
|
|
1497
|
-
entry.resolve({ kind: "verdict", behavior, channelId: "telegram" });
|
|
1498
|
-
}
|
|
1499
|
-
settleQuestion(entry, answers) {
|
|
1500
|
-
this.pending.delete(entry.channelRequestId);
|
|
1501
|
-
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
1502
|
-
const summary = Object.values(answers).join(", ").slice(0, 120);
|
|
1503
|
-
void this.editToResolved(entry, summary || "Answered");
|
|
1504
|
-
entry.resolve({ kind: "answer", answers, channelId: "telegram" });
|
|
1505
|
-
}
|
|
1506
|
-
async editToResolved(entry, label) {
|
|
1507
|
-
const bot = this.bot;
|
|
1508
|
-
if (!bot) return;
|
|
1509
|
-
try {
|
|
1510
|
-
await bot.editMessageText(
|
|
1511
|
-
entry.chatId,
|
|
1512
|
-
entry.messageId,
|
|
1513
|
-
buildResolvedText(entry.headline, label),
|
|
1514
|
-
MD_OPTIONS
|
|
1515
|
-
);
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
this.log(
|
|
1518
|
-
"debug",
|
|
1519
|
-
`telegram relay: edit-to-resolved failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
async editToCancelled(entry) {
|
|
1524
|
-
const bot = this.bot;
|
|
1525
|
-
if (!bot) return;
|
|
1526
|
-
try {
|
|
1527
|
-
await bot.editMessageText(
|
|
1528
|
-
entry.chatId,
|
|
1529
|
-
entry.messageId,
|
|
1530
|
-
buildCancelText("resolved elsewhere"),
|
|
1531
|
-
MD_OPTIONS
|
|
1532
|
-
);
|
|
1533
|
-
} catch (err) {
|
|
1534
|
-
this.log(
|
|
1535
|
-
"debug",
|
|
1536
|
-
`telegram relay: edit-to-cancelled failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
};
|
|
1541
|
-
function clampToTelegramLimit(text) {
|
|
1542
|
-
if (text.length <= TELEGRAM_MAX_TEXT) return text;
|
|
1543
|
-
return text.slice(0, TELEGRAM_MAX_TEXT - 1) + "\u2026";
|
|
1544
|
-
}
|
|
1545
|
-
function buildPromptMarkdown(toolName, description, inputPreview, channelRequestId) {
|
|
1546
|
-
const trimmedPreview = inputPreview.trim();
|
|
1547
|
-
const render = (preview) => {
|
|
1548
|
-
const lines = [
|
|
1549
|
-
`*${escapeMarkdownV2(toolName)}* \u2014 ${escapeMarkdownV2(description)}`
|
|
1550
|
-
];
|
|
1551
|
-
if (preview.length > 0) {
|
|
1552
|
-
lines.push("", "```", escapeMarkdownV2CodeBlock(preview), "```");
|
|
1553
|
-
}
|
|
1554
|
-
lines.push(
|
|
1555
|
-
"",
|
|
1556
|
-
escapeMarkdownV2(
|
|
1557
|
-
`Tap a button below, or reply "yes ${channelRequestId}" / "no ${channelRequestId}".`
|
|
1558
|
-
)
|
|
1559
|
-
);
|
|
1560
|
-
return lines.join("\n");
|
|
1561
|
-
};
|
|
1562
|
-
const first = render(trimmedPreview);
|
|
1563
|
-
const budget = TELEGRAM_MAX_TEXT - TELEGRAM_TEXT_SAFE_MARGIN;
|
|
1564
|
-
if (first.length <= budget) return first;
|
|
1565
|
-
const overflow = first.length - budget;
|
|
1566
|
-
const safePreview = trimmedPreview.length > overflow ? trimmedPreview.slice(0, Math.max(0, trimmedPreview.length - overflow)) + "\u2026" : "";
|
|
1567
|
-
return clampToTelegramLimit(render(safePreview));
|
|
1568
|
-
}
|
|
1569
|
-
function buildPermissionKeyboard(channelRequestId) {
|
|
1570
|
-
return {
|
|
1571
|
-
inline_keyboard: [
|
|
1572
|
-
[
|
|
1573
|
-
{
|
|
1574
|
-
text: "\u2705 Allow",
|
|
1575
|
-
callback_data: buildPermissionCallbackData(channelRequestId, "allow")
|
|
1576
|
-
},
|
|
1577
|
-
{
|
|
1578
|
-
text: "\u274C Deny",
|
|
1579
|
-
callback_data: buildPermissionCallbackData(channelRequestId, "deny")
|
|
1580
|
-
}
|
|
1581
|
-
]
|
|
1582
|
-
]
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
function buildQuestionMarkdown(title, questions, channelRequestId, hasButtons) {
|
|
1586
|
-
const lines = [`*${escapeMarkdownV2(title)}*`];
|
|
1587
|
-
for (const [index, q] of questions.entries()) {
|
|
1588
|
-
lines.push("");
|
|
1589
|
-
lines.push(
|
|
1590
|
-
`${index + 1}\\. *${escapeMarkdownV2(q.header)}*: ${escapeMarkdownV2(q.question)}`
|
|
1591
|
-
);
|
|
1592
|
-
if (q.options.length > 0 && !hasButtons) {
|
|
1593
|
-
for (const option of q.options) {
|
|
1594
|
-
const suffix = option.description ? ` \u2014 ${option.description}` : "";
|
|
1595
|
-
lines.push(` \u2022 ${escapeMarkdownV2(option.label + suffix)}`);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
const trailer = hasButtons ? escapeMarkdownV2("Tap an option below.") : questions.length <= 1 ? escapeMarkdownV2(
|
|
1600
|
-
`Reply with your answer, or "answer ${channelRequestId} your response".`
|
|
1601
|
-
) : escapeMarkdownV2(
|
|
1602
|
-
`Reply 'answer ${channelRequestId} {"Question":"Answer"}' to respond.`
|
|
1603
|
-
);
|
|
1604
|
-
lines.push("", trailer);
|
|
1605
|
-
return clampToTelegramLimit(lines.join("\n"));
|
|
1606
|
-
}
|
|
1607
|
-
function buildQuestionKeyboard(channelRequestId, questions) {
|
|
1608
|
-
if (questions.length !== 1) return null;
|
|
1609
|
-
const q = questions[0];
|
|
1610
|
-
if (q.multi_select || q.options.length === 0) return null;
|
|
1611
|
-
const rows = [];
|
|
1612
|
-
const options = [];
|
|
1613
|
-
for (const [optIdx, option] of q.options.entries()) {
|
|
1614
|
-
const data = buildQuestionCallbackData(channelRequestId, optIdx);
|
|
1615
|
-
if (Buffer.byteLength(data, "utf8") > 64) return null;
|
|
1616
|
-
rows.push([{ text: option.label, callback_data: data }]);
|
|
1617
|
-
options.push({ key: q.key, label: option.label });
|
|
1618
|
-
}
|
|
1619
|
-
return { markup: { inline_keyboard: rows }, options };
|
|
1620
|
-
}
|
|
1621
|
-
function buildResolvedText(headline, label) {
|
|
1622
|
-
return `*${escapeMarkdownV2(headline)}*
|
|
1623
|
-
|
|
1624
|
-
${escapeMarkdownV2(`\u2713 ${label}`)}`;
|
|
1625
|
-
}
|
|
1626
|
-
function buildCancelText(reason) {
|
|
1627
|
-
return escapeMarkdownV2(`~ resolved (${reason}) ~`);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// src/gateway/adapters/telegram/adapter.ts
|
|
1631
|
-
var TELEGRAM_DEFAULT_ID = "telegram";
|
|
1632
|
-
var TelegramAdapter = class {
|
|
1633
|
-
id;
|
|
1634
|
-
capabilities = {
|
|
1635
|
-
chat: true,
|
|
1636
|
-
threads: true,
|
|
1637
|
-
relayPermission: true,
|
|
1638
|
-
relayQuestion: true,
|
|
1639
|
-
// Telegram caps text at 4096 chars; we leave chunking to the manager.
|
|
1640
|
-
maxMessageBytes: 4096
|
|
1641
|
-
};
|
|
1642
|
-
bot = null;
|
|
1643
|
-
opts;
|
|
1644
|
-
relay;
|
|
1645
|
-
pollTask = null;
|
|
1646
|
-
lastInboundAt;
|
|
1647
|
-
lastTransportOk = true;
|
|
1648
|
-
ctx = null;
|
|
1649
|
-
constructor(opts, id = TELEGRAM_DEFAULT_ID) {
|
|
1650
|
-
this.opts = opts;
|
|
1651
|
-
this.id = id;
|
|
1652
|
-
this.relay = new TelegramRelay({
|
|
1653
|
-
resolveTarget: () => {
|
|
1654
|
-
if (this.opts.defaultChatId === void 0) return null;
|
|
1655
|
-
return {
|
|
1656
|
-
chatId: this.opts.defaultChatId,
|
|
1657
|
-
...this.opts.defaultThreadId !== void 0 ? { threadId: this.opts.defaultThreadId } : {}
|
|
1658
|
-
};
|
|
1659
|
-
},
|
|
1660
|
-
log: (level, msg) => this.ctx?.log(level, msg)
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
requestPermissionVerdict(req, signal) {
|
|
1664
|
-
return this.relay.requestPermission(req, signal);
|
|
1665
|
-
}
|
|
1666
|
-
requestQuestionAnswer(req, signal) {
|
|
1667
|
-
return this.relay.requestQuestion(req, signal);
|
|
1668
|
-
}
|
|
1669
|
-
async start(ctx) {
|
|
1670
|
-
if (this.bot) {
|
|
1671
|
-
throw new Error("telegram adapter already started");
|
|
1672
|
-
}
|
|
1673
|
-
this.ctx = ctx;
|
|
1674
|
-
const factory = this.opts.botFactory ?? ((o) => new TelegramBot(
|
|
1675
|
-
{
|
|
1676
|
-
token: o.token,
|
|
1677
|
-
apiBase: o.apiBase,
|
|
1678
|
-
pollTimeoutSec: o.pollTimeoutSec
|
|
1679
|
-
},
|
|
1680
|
-
o.log
|
|
1681
|
-
));
|
|
1682
|
-
this.bot = factory({
|
|
1683
|
-
token: this.opts.token,
|
|
1684
|
-
apiBase: this.opts.apiBase,
|
|
1685
|
-
pollTimeoutSec: this.opts.pollTimeoutSec,
|
|
1686
|
-
log: ctx.log
|
|
1687
|
-
});
|
|
1688
|
-
this.relay.bindBot(this.bot);
|
|
1689
|
-
this.pollTask = this.runPollLoop();
|
|
1690
|
-
ctx.signal.addEventListener("abort", () => {
|
|
1691
|
-
this.bot?.stop();
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
async stop(_reason) {
|
|
1695
|
-
this.relay.disposeAll();
|
|
1696
|
-
this.bot?.stop();
|
|
1697
|
-
const task = this.pollTask;
|
|
1698
|
-
this.pollTask = null;
|
|
1699
|
-
if (task) {
|
|
1700
|
-
try {
|
|
1701
|
-
await task;
|
|
1702
|
-
} catch {
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
this.relay.bindBot(null);
|
|
1706
|
-
this.bot = null;
|
|
1707
|
-
this.ctx = null;
|
|
1708
|
-
}
|
|
1709
|
-
async send(msg) {
|
|
1710
|
-
const bot = this.bot;
|
|
1711
|
-
if (!bot) {
|
|
1712
|
-
throw new Error("telegram adapter: send called before start");
|
|
1713
|
-
}
|
|
1714
|
-
const chatId = msg.location.peer?.id ?? msg.location.room?.id;
|
|
1715
|
-
if (!chatId) {
|
|
1716
|
-
throw new Error(
|
|
1717
|
-
"telegram adapter: outbound location has no peer or room"
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
const threadId = msg.location.thread?.id ? Number(msg.location.thread.id) : void 0;
|
|
1721
|
-
const result = await bot.sendMessage(chatId, msg.text, {
|
|
1722
|
-
...threadId !== void 0 && Number.isFinite(threadId) ? { message_thread_id: threadId } : {}
|
|
1723
|
-
});
|
|
1724
|
-
if (!result) {
|
|
1725
|
-
throw new Error("telegram adapter: sendMessage returned null");
|
|
1726
|
-
}
|
|
1727
|
-
return {
|
|
1728
|
-
providerMessageId: String(result.message_id),
|
|
1729
|
-
deliveredAt: Date.now()
|
|
1730
|
-
};
|
|
1731
|
-
}
|
|
1732
|
-
async probe() {
|
|
1733
|
-
return {
|
|
1734
|
-
ok: this.lastTransportOk,
|
|
1735
|
-
detail: this.lastTransportOk ? "long-poll healthy" : "long-poll erroring",
|
|
1736
|
-
checkedAt: Date.now()
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
async runPollLoop() {
|
|
1740
|
-
const bot = this.bot;
|
|
1741
|
-
const ctx = this.ctx;
|
|
1742
|
-
if (!bot || !ctx) return;
|
|
1743
|
-
const allow = this.opts.allowedUserIds.length === 0 ? null : new Set(this.opts.allowedUserIds.map(String));
|
|
1744
|
-
try {
|
|
1745
|
-
for await (const update of bot.poll()) {
|
|
1746
|
-
if (this.relay.handleUpdate(update)) {
|
|
1747
|
-
this.markHealth(true);
|
|
1748
|
-
continue;
|
|
1749
|
-
}
|
|
1750
|
-
const inbound = normalizeInbound2(update, allow, this.id);
|
|
1751
|
-
if (!inbound) continue;
|
|
1752
|
-
this.lastInboundAt = inbound.receivedAt;
|
|
1753
|
-
this.markHealth(true);
|
|
1754
|
-
try {
|
|
1755
|
-
ctx.emitInbound(inbound);
|
|
1756
|
-
} catch (err) {
|
|
1757
|
-
ctx.log(
|
|
1758
|
-
"warn",
|
|
1759
|
-
`telegram emitInbound threw: ${err instanceof Error ? err.message : String(err)}`
|
|
1760
|
-
);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
} catch (err) {
|
|
1764
|
-
this.markHealth(false, err instanceof Error ? err.message : String(err));
|
|
1765
|
-
ctx.log(
|
|
1766
|
-
"error",
|
|
1767
|
-
`telegram poll loop terminated: ${err instanceof Error ? err.message : String(err)}`
|
|
1768
|
-
);
|
|
1769
|
-
throw err;
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
markHealth(ok2, note) {
|
|
1773
|
-
this.lastTransportOk = ok2;
|
|
1774
|
-
const ctx = this.ctx;
|
|
1775
|
-
if (!ctx) return;
|
|
1776
|
-
const sample = {
|
|
1777
|
-
at: Date.now(),
|
|
1778
|
-
transportOk: ok2,
|
|
1779
|
-
...this.lastInboundAt !== void 0 ? { lastInboundAt: this.lastInboundAt } : {},
|
|
1780
|
-
...note !== void 0 ? { note } : {}
|
|
1781
|
-
};
|
|
1782
|
-
try {
|
|
1783
|
-
ctx.emitHealth(sample);
|
|
1784
|
-
} catch {
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
};
|
|
1788
|
-
function normalizeInbound2(update, allow, channelId) {
|
|
1789
|
-
const message = update.message ?? update.edited_message;
|
|
1790
|
-
if (!message) return null;
|
|
1791
|
-
const text = message.text;
|
|
1792
|
-
if (typeof text !== "string" || text.length === 0) return null;
|
|
1793
|
-
const sender = message.from;
|
|
1794
|
-
if (!sender) return null;
|
|
1795
|
-
const senderId = String(sender.id);
|
|
1796
|
-
if (allow && !allow.has(senderId)) return null;
|
|
1797
|
-
const accountId = String(sender.is_bot ? `bot:${sender.id}` : "user");
|
|
1798
|
-
const chatId = String(message.chat.id);
|
|
1799
|
-
const isPrivate = message.chat.type === "private";
|
|
1800
|
-
const threadId = typeof message.message_thread_id === "number" ? String(message.message_thread_id) : void 0;
|
|
1801
|
-
return {
|
|
1802
|
-
location: {
|
|
1803
|
-
channelId,
|
|
1804
|
-
accountId,
|
|
1805
|
-
...isPrivate ? { peer: { id: chatId, kind: "user" } } : {
|
|
1806
|
-
room: {
|
|
1807
|
-
id: chatId,
|
|
1808
|
-
kind: message.chat.type === "channel" ? "channel" : "group"
|
|
1809
|
-
}
|
|
1810
|
-
},
|
|
1811
|
-
...threadId !== void 0 ? { thread: { id: threadId } } : {}
|
|
1812
|
-
},
|
|
1813
|
-
sender: {
|
|
1814
|
-
id: senderId,
|
|
1815
|
-
...sender.username !== void 0 ? { displayName: sender.username } : sender.first_name !== void 0 ? { displayName: sender.first_name } : {}
|
|
1816
|
-
},
|
|
1817
|
-
text,
|
|
1818
|
-
receivedAt: Date.now(),
|
|
1819
|
-
idempotencyKey: `tg:${update.update_id}`,
|
|
1820
|
-
providerMessageId: String(message.message_id)
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// src/gateway/adapters/telegram/module.ts
|
|
1825
|
-
var telegramModule = {
|
|
1826
|
-
name: "telegram",
|
|
1827
|
-
parseConfig({ options, allowedUserIds }) {
|
|
1828
|
-
const token = options["bot_token"];
|
|
1829
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
1830
|
-
return { ok: false, reason: "bot_token missing" };
|
|
1831
|
-
}
|
|
1832
|
-
const defaultChatRaw = options["default_chat_id"];
|
|
1833
|
-
const defaultThreadRaw = options["default_thread_id"];
|
|
1834
|
-
const apiBaseRaw = options["api_base"];
|
|
1835
|
-
const pollTimeoutRaw = options["poll_timeout_sec"];
|
|
1836
|
-
if (defaultChatRaw !== void 0 && typeof defaultChatRaw !== "string" && typeof defaultChatRaw !== "number") {
|
|
1837
|
-
return { ok: false, reason: "default_chat_id must be string or number" };
|
|
1838
|
-
}
|
|
1839
|
-
if (defaultThreadRaw !== void 0 && typeof defaultThreadRaw !== "number") {
|
|
1840
|
-
return { ok: false, reason: "default_thread_id must be number" };
|
|
1841
|
-
}
|
|
1842
|
-
if (apiBaseRaw !== void 0 && typeof apiBaseRaw !== "string") {
|
|
1843
|
-
return { ok: false, reason: "api_base must be string" };
|
|
1844
|
-
}
|
|
1845
|
-
if (pollTimeoutRaw !== void 0 && typeof pollTimeoutRaw !== "number") {
|
|
1846
|
-
return { ok: false, reason: "poll_timeout_sec must be number" };
|
|
1847
|
-
}
|
|
1848
|
-
const config = {
|
|
1849
|
-
token,
|
|
1850
|
-
allowedUserIds,
|
|
1851
|
-
...defaultChatRaw !== void 0 ? { defaultChatId: defaultChatRaw } : {},
|
|
1852
|
-
...defaultThreadRaw !== void 0 ? { defaultThreadId: defaultThreadRaw } : {},
|
|
1853
|
-
...apiBaseRaw !== void 0 ? { apiBase: apiBaseRaw } : {},
|
|
1854
|
-
...pollTimeoutRaw !== void 0 ? { pollTimeoutSec: pollTimeoutRaw } : {}
|
|
1855
|
-
};
|
|
1856
|
-
return { ok: true, config };
|
|
1857
|
-
},
|
|
1858
|
-
create(config, instanceId) {
|
|
1859
|
-
return new TelegramAdapter(config, instanceId);
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
// src/gateway/adapters/registry.ts
|
|
1864
|
-
var BUILTIN_MODULES = [
|
|
1865
|
-
telegramModule,
|
|
1866
|
-
consoleModule
|
|
1867
|
-
];
|
|
1868
|
-
function findAdapterModule(name) {
|
|
1869
|
-
return BUILTIN_MODULES.find((m) => m.name === name);
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
// src/gateway/adapters/factory.ts
|
|
1873
|
-
function instantiateAdapter(sidecar) {
|
|
1874
|
-
const module = findAdapterModule(sidecar.kind);
|
|
1875
|
-
if (!module) {
|
|
1876
|
-
return { ok: false, reason: `unknown channel kind: ${sidecar.kind}` };
|
|
1877
|
-
}
|
|
1878
|
-
const parsed = module.parseConfig({
|
|
1879
|
-
options: sidecar.options,
|
|
1880
|
-
allowedUserIds: sidecar.allowedUserIds
|
|
1881
|
-
});
|
|
1882
|
-
if (!parsed.ok) {
|
|
1883
|
-
return { ok: false, reason: `${sidecar.instanceId}: ${parsed.reason}` };
|
|
1884
|
-
}
|
|
1885
|
-
return { ok: true, adapter: module.create(parsed.config, sidecar.instanceId) };
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
// src/gateway/channelManager.ts
|
|
1889
|
-
var DEFAULT_DEDUP_WINDOW = 1024;
|
|
1890
|
-
var DuplicateChannelError = class extends Error {
|
|
1891
|
-
constructor(id) {
|
|
1892
|
-
super(`channel ${id} already registered`);
|
|
1893
|
-
this.name = "DuplicateChannelError";
|
|
1894
|
-
}
|
|
1895
|
-
};
|
|
1896
|
-
var UnknownChannelError = class extends Error {
|
|
1897
|
-
constructor(id) {
|
|
1898
|
-
super(`channel ${id} not registered`);
|
|
1899
|
-
this.name = "UnknownChannelError";
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
var ChannelManager = class {
|
|
1903
|
-
entries = /* @__PURE__ */ new Map();
|
|
1904
|
-
dedup = [];
|
|
1905
|
-
dedupSet = /* @__PURE__ */ new Set();
|
|
1906
|
-
dedupMax;
|
|
1907
|
-
log;
|
|
1908
|
-
inboundSink = null;
|
|
1909
|
-
healthSink = null;
|
|
1910
|
-
stopped = false;
|
|
1911
|
-
constructor(opts = {}) {
|
|
1912
|
-
this.dedupMax = opts.dedupWindow ?? DEFAULT_DEDUP_WINDOW;
|
|
1913
|
-
this.log = opts.log;
|
|
1914
|
-
}
|
|
1915
|
-
/** Register the single inbound dispatch target. M5 wires the router here. */
|
|
1916
|
-
setInboundSink(sink) {
|
|
1917
|
-
this.inboundSink = sink;
|
|
1918
|
-
}
|
|
1919
|
-
/**
|
|
1920
|
-
* Returns the attachmentId associated with `channelId` at registration
|
|
1921
|
-
* time, or undefined if the channel is unknown or registered without one.
|
|
1922
|
-
*/
|
|
1923
|
-
getAttachmentId(channelId) {
|
|
1924
|
-
return this.entries.get(channelId)?.attachmentId;
|
|
1925
|
-
}
|
|
1926
|
-
setHealthSink(sink) {
|
|
1927
|
-
this.healthSink = sink;
|
|
1928
|
-
}
|
|
1929
|
-
listChannels() {
|
|
1930
|
-
return [...this.entries.values()].map((e) => ({
|
|
1931
|
-
id: e.adapter.id,
|
|
1932
|
-
health: e.lastHealth
|
|
1933
|
-
}));
|
|
1934
|
-
}
|
|
1935
|
-
/** Snapshot of currently registered adapters; used by the relay coordinator. */
|
|
1936
|
-
listAdapters() {
|
|
1937
|
-
return [...this.entries.values()].map((e) => e.adapter);
|
|
1938
|
-
}
|
|
1939
|
-
async register(adapter, opts = {}) {
|
|
1940
|
-
if (this.stopped) {
|
|
1941
|
-
throw new Error("channel manager already stopped");
|
|
1942
|
-
}
|
|
1943
|
-
if (this.entries.has(adapter.id)) {
|
|
1944
|
-
throw new DuplicateChannelError(adapter.id);
|
|
1945
|
-
}
|
|
1946
|
-
const abort = new AbortController();
|
|
1947
|
-
const inboundListener = (msg) => this.handleInbound(adapter.id, msg);
|
|
1948
|
-
const healthListener = (sample) => {
|
|
1949
|
-
const entry2 = this.entries.get(adapter.id);
|
|
1950
|
-
if (entry2) entry2.lastHealth = sample;
|
|
1951
|
-
this.healthSink?.(sample);
|
|
1952
|
-
};
|
|
1953
|
-
const startPromise = adapter.start({
|
|
1954
|
-
log: (level, msg) => this.log?.(level, `[${adapter.id}] ${msg}`),
|
|
1955
|
-
signal: abort.signal,
|
|
1956
|
-
emitInbound: inboundListener,
|
|
1957
|
-
emitHealth: healthListener
|
|
1958
|
-
});
|
|
1959
|
-
const entry = {
|
|
1960
|
-
adapter,
|
|
1961
|
-
abort,
|
|
1962
|
-
startPromise
|
|
1963
|
-
};
|
|
1964
|
-
if (opts.attachmentId !== void 0) entry.attachmentId = opts.attachmentId;
|
|
1965
|
-
this.entries.set(adapter.id, entry);
|
|
1966
|
-
try {
|
|
1967
|
-
await startPromise;
|
|
1968
|
-
} catch (err) {
|
|
1969
|
-
this.entries.delete(adapter.id);
|
|
1970
|
-
throw err;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
async unregister(id, reason) {
|
|
1974
|
-
const entry = this.entries.get(id);
|
|
1975
|
-
if (!entry) {
|
|
1976
|
-
throw new UnknownChannelError(id);
|
|
1977
|
-
}
|
|
1978
|
-
this.entries.delete(id);
|
|
1979
|
-
entry.abort.abort();
|
|
1980
|
-
await entry.adapter.stop(reason);
|
|
1981
|
-
}
|
|
1982
|
-
async send(channelId, msg) {
|
|
1983
|
-
const entry = this.entries.get(channelId);
|
|
1984
|
-
if (!entry) {
|
|
1985
|
-
throw new UnknownChannelError(channelId);
|
|
1986
|
-
}
|
|
1987
|
-
return entry.adapter.send(msg);
|
|
1988
|
-
}
|
|
1989
|
-
async probe(channelId) {
|
|
1990
|
-
const entry = this.entries.get(channelId);
|
|
1991
|
-
if (!entry) {
|
|
1992
|
-
throw new UnknownChannelError(channelId);
|
|
1993
|
-
}
|
|
1994
|
-
return entry.adapter.probe();
|
|
1995
|
-
}
|
|
1996
|
-
async stop(reason = "shutdown") {
|
|
1997
|
-
if (this.stopped) return;
|
|
1998
|
-
this.stopped = true;
|
|
1999
|
-
const ids = [...this.entries.keys()];
|
|
2000
|
-
for (const id of ids.reverse()) {
|
|
2001
|
-
try {
|
|
2002
|
-
await this.unregister(id, reason);
|
|
2003
|
-
} catch (err) {
|
|
2004
|
-
this.log?.(
|
|
2005
|
-
"warn",
|
|
2006
|
-
`channel ${id} stop failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2007
|
-
);
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
handleInbound(channelId, msg) {
|
|
2012
|
-
if (this.dedupSet.has(msg.idempotencyKey)) {
|
|
2013
|
-
this.log?.("debug", `dropping duplicate inbound ${msg.idempotencyKey}`);
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
|
-
this.dedupSet.add(msg.idempotencyKey);
|
|
2017
|
-
this.dedup.push(msg.idempotencyKey);
|
|
2018
|
-
while (this.dedup.length > this.dedupMax) {
|
|
2019
|
-
const evicted = this.dedup.shift();
|
|
2020
|
-
if (evicted !== void 0) this.dedupSet.delete(evicted);
|
|
2021
|
-
}
|
|
2022
|
-
const sink = this.inboundSink;
|
|
2023
|
-
if (!sink) {
|
|
2024
|
-
this.log?.(
|
|
2025
|
-
"debug",
|
|
2026
|
-
`no inbound sink registered; dropping ${msg.idempotencyKey}`
|
|
2027
|
-
);
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
try {
|
|
2031
|
-
sink(msg, { attachmentId: this.entries.get(channelId)?.attachmentId });
|
|
2032
|
-
} catch (err) {
|
|
2033
|
-
this.log?.(
|
|
2034
|
-
"warn",
|
|
2035
|
-
`inbound sink threw: ${err instanceof Error ? err.message : String(err)}`
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
};
|
|
2040
|
-
|
|
2041
|
-
// src/gateway/control/handlers.ts
|
|
2042
|
-
import { createRequire } from "module";
|
|
2043
|
-
|
|
2044
|
-
// src/gateway/runtimeBindingStore.ts
|
|
2045
|
-
var AlreadyRegisteredError = class extends Error {
|
|
2046
|
-
code = "already_registered";
|
|
2047
|
-
constructor(existing) {
|
|
2048
|
-
super(
|
|
2049
|
-
`gateway already has a registered runtime (pid=${existing.pid}, runtimeId=${existing.runtimeId})`
|
|
2050
|
-
);
|
|
2051
|
-
this.name = "AlreadyRegisteredError";
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2054
|
-
var NotRegisteredError = class extends Error {
|
|
2055
|
-
code = "not_registered";
|
|
2056
|
-
constructor() {
|
|
2057
|
-
super("no runtime registered with gateway");
|
|
2058
|
-
this.name = "NotRegisteredError";
|
|
2059
|
-
}
|
|
2060
|
-
};
|
|
2061
|
-
function maybeLastRebindAt(value) {
|
|
2062
|
-
return value !== void 0 ? { lastRebindAt: value } : {};
|
|
2063
|
-
}
|
|
2064
|
-
var RuntimeBindingStore = class {
|
|
2065
|
-
slots = /* @__PURE__ */ new Map();
|
|
2066
|
-
gracePeriodMs;
|
|
2067
|
-
observers;
|
|
2068
|
-
now;
|
|
2069
|
-
constructor(opts = {}) {
|
|
2070
|
-
this.gracePeriodMs = opts.gracePeriodMs ?? 0;
|
|
2071
|
-
this.observers = opts.observers ?? {};
|
|
2072
|
-
this.now = opts.now ?? Date.now;
|
|
2073
|
-
}
|
|
2074
|
-
// ── lifecycle ─────────────────────────────────────────────
|
|
2075
|
-
/** Register a runtime and bind its connection in one atomic step. */
|
|
2076
|
-
bind(input) {
|
|
2077
|
-
const key = input.attachmentId;
|
|
2078
|
-
const existing = this.slots.get(key);
|
|
2079
|
-
const previousBinding = existing?.binding ?? null;
|
|
2080
|
-
const wasStale = previousBinding?.state === "stale";
|
|
2081
|
-
const staleSince = wasStale ? previousBinding.staleSince : null;
|
|
2082
|
-
let runtime;
|
|
2083
|
-
if (!existing) {
|
|
2084
|
-
runtime = {
|
|
2085
|
-
runtimeId: input.runtimeId,
|
|
2086
|
-
defaultAgentId: input.defaultAgentId,
|
|
2087
|
-
pid: input.pid,
|
|
2088
|
-
registeredAt: this.now(),
|
|
2089
|
-
...input.attachmentId !== void 0 ? { attachmentId: input.attachmentId } : {}
|
|
2090
|
-
};
|
|
2091
|
-
} else if (existing.runtime.runtimeId === input.runtimeId) {
|
|
2092
|
-
runtime = {
|
|
2093
|
-
...existing.runtime,
|
|
2094
|
-
defaultAgentId: input.defaultAgentId,
|
|
2095
|
-
pid: input.pid
|
|
2096
|
-
};
|
|
2097
|
-
} else {
|
|
2098
|
-
throw new AlreadyRegisteredError(existing.runtime);
|
|
2099
|
-
}
|
|
2100
|
-
const now = this.now();
|
|
2101
|
-
const isRebind = previousBinding !== null && (previousBinding.state === "stale" || previousBinding.connectionId !== input.connectionId);
|
|
2102
|
-
const lastRebindAt = isRebind ? now : previousBinding?.lastRebindAt;
|
|
2103
|
-
const epoch = previousBinding ? previousBinding.epoch + (isRebind ? 1 : 0) : 1;
|
|
2104
|
-
const newBinding = {
|
|
2105
|
-
state: "active",
|
|
2106
|
-
connectionId: input.connectionId,
|
|
2107
|
-
boundAt: now,
|
|
2108
|
-
epoch,
|
|
2109
|
-
...maybeLastRebindAt(lastRebindAt)
|
|
2110
|
-
};
|
|
2111
|
-
const slot = existing ? { ...existing, runtime, binding: newBinding } : { runtime, binding: newBinding, staleTimer: null, staleSince: null };
|
|
2112
|
-
this.clearStaleTimerForSlot(slot);
|
|
2113
|
-
this.slots.set(key, slot);
|
|
2114
|
-
if (wasStale && staleSince !== null) {
|
|
2115
|
-
this.observers.onRuntimeRebind?.({
|
|
2116
|
-
runtimeId: input.runtimeId,
|
|
2117
|
-
gapMs: now - staleSince,
|
|
2118
|
-
epoch: newBinding.epoch
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
return { registeredAt: runtime.registeredAt };
|
|
2122
|
-
}
|
|
2123
|
-
/** Fully unregister a runtime. Throws NotRegisteredError if id does not match. */
|
|
2124
|
-
unbind(runtimeId) {
|
|
2125
|
-
const entry = this.findSlotByRuntimeId(runtimeId);
|
|
2126
|
-
if (!entry) {
|
|
2127
|
-
throw new NotRegisteredError();
|
|
2128
|
-
}
|
|
2129
|
-
this.clearStaleTimerForSlot(entry.slot);
|
|
2130
|
-
this.slots.delete(entry.key);
|
|
2131
|
-
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: true });
|
|
2132
|
-
}
|
|
2133
|
-
/**
|
|
2134
|
-
* Called when the transport connection closes.
|
|
2135
|
-
* Returns the runtimeId if the close was for a current binding (caller should
|
|
2136
|
-
* clear the push handle); returns null if the connectionId was not recognised.
|
|
2137
|
-
*/
|
|
2138
|
-
notifyConnectionClosed(connectionId) {
|
|
2139
|
-
const entry = this.findSlotByConnectionId(connectionId);
|
|
2140
|
-
if (!entry) return null;
|
|
2141
|
-
const { key, slot } = entry;
|
|
2142
|
-
const runtimeId = slot.runtime.runtimeId;
|
|
2143
|
-
const now = this.now();
|
|
2144
|
-
const previousBinding = slot.binding;
|
|
2145
|
-
slot.binding = {
|
|
2146
|
-
state: "stale",
|
|
2147
|
-
connectionId,
|
|
2148
|
-
staleSince: now,
|
|
2149
|
-
epoch: previousBinding.epoch,
|
|
2150
|
-
...maybeLastRebindAt(previousBinding.lastRebindAt)
|
|
2151
|
-
};
|
|
2152
|
-
if (this.gracePeriodMs <= 0) {
|
|
2153
|
-
this.slots.delete(key);
|
|
2154
|
-
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2155
|
-
return runtimeId;
|
|
2156
|
-
}
|
|
2157
|
-
slot.staleSince = now;
|
|
2158
|
-
slot.staleTimer = setTimeout(() => {
|
|
2159
|
-
this.expireStaleBinding(key, runtimeId);
|
|
2160
|
-
}, this.gracePeriodMs);
|
|
2161
|
-
return runtimeId;
|
|
2162
|
-
}
|
|
2163
|
-
stop() {
|
|
2164
|
-
for (const slot of this.slots.values()) {
|
|
2165
|
-
this.clearStaleTimerForSlot(slot);
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
// ── reads ─────────────────────────────────────────────────
|
|
2169
|
-
hasActiveBinding(runtimeId) {
|
|
2170
|
-
const slot = this.slots.get(void 0);
|
|
2171
|
-
if (!slot || !slot.binding || slot.binding.state !== "active") return false;
|
|
2172
|
-
return runtimeId === void 0 || slot.runtime.runtimeId === runtimeId;
|
|
2173
|
-
}
|
|
2174
|
-
hasActiveBindingForAttachment(key) {
|
|
2175
|
-
const slot = this.slots.get(key);
|
|
2176
|
-
return !!slot && !!slot.binding && slot.binding.state === "active";
|
|
2177
|
-
}
|
|
2178
|
-
getCurrent() {
|
|
2179
|
-
return this.slots.get(void 0)?.runtime ?? null;
|
|
2180
|
-
}
|
|
2181
|
-
getCurrentByAttachment(attachmentId) {
|
|
2182
|
-
return this.slots.get(attachmentId)?.runtime ?? null;
|
|
2183
|
-
}
|
|
2184
|
-
getBinding() {
|
|
2185
|
-
return this.slots.get(void 0)?.binding ?? null;
|
|
2186
|
-
}
|
|
2187
|
-
getRuntimeIdByConnection(connectionId) {
|
|
2188
|
-
const entry = this.findSlotByConnectionId(connectionId);
|
|
2189
|
-
return entry ? entry.slot.runtime.runtimeId : null;
|
|
2190
|
-
}
|
|
2191
|
-
/**
|
|
2192
|
-
* Returns the attachment slot key (or `undefined` for the legacy slot) that
|
|
2193
|
-
* holds the given runtime, or `null` if no slot does. Lets callers
|
|
2194
|
-
* route per-attachment side-state (like push handles) keyed the same way
|
|
2195
|
-
* as the binding map.
|
|
2196
|
-
*/
|
|
2197
|
-
getAttachmentKeyByRuntimeId(runtimeId) {
|
|
2198
|
-
const entry = this.findSlotByRuntimeId(runtimeId);
|
|
2199
|
-
return entry ? { key: entry.key, runtime: entry.slot.runtime } : null;
|
|
2200
|
-
}
|
|
2201
|
-
// ── private ───────────────────────────────────────────────
|
|
2202
|
-
expireStaleBinding(key, runtimeId) {
|
|
2203
|
-
const slot = this.slots.get(key);
|
|
2204
|
-
if (!slot) return;
|
|
2205
|
-
slot.staleTimer = null;
|
|
2206
|
-
const since = slot.staleSince;
|
|
2207
|
-
slot.staleSince = null;
|
|
2208
|
-
if (slot.runtime.runtimeId !== runtimeId) return;
|
|
2209
|
-
if (slot.binding?.state === "active") return;
|
|
2210
|
-
this.slots.delete(key);
|
|
2211
|
-
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2212
|
-
if (since !== null) {
|
|
2213
|
-
this.observers.onRuntimeExpired?.({ runtimeId, gapMs: this.now() - since });
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
clearStaleTimerForSlot(slot) {
|
|
2217
|
-
if (slot.staleTimer) {
|
|
2218
|
-
clearTimeout(slot.staleTimer);
|
|
2219
|
-
slot.staleTimer = null;
|
|
2220
|
-
}
|
|
2221
|
-
slot.staleSince = null;
|
|
2222
|
-
}
|
|
2223
|
-
findSlotByRuntimeId(runtimeId) {
|
|
2224
|
-
for (const [key, slot] of this.slots) {
|
|
2225
|
-
if (slot.runtime.runtimeId === runtimeId) return { key, slot };
|
|
2226
|
-
}
|
|
2227
|
-
return null;
|
|
2228
|
-
}
|
|
2229
|
-
findSlotByConnectionId(connectionId) {
|
|
2230
|
-
for (const [key, slot] of this.slots) {
|
|
2231
|
-
if (slot.binding?.connectionId === connectionId) return { key, slot };
|
|
2232
|
-
}
|
|
2233
|
-
return null;
|
|
2234
|
-
}
|
|
2235
|
-
};
|
|
2236
|
-
|
|
2237
|
-
// src/gateway/control/handlers.ts
|
|
2238
|
-
var require_ = createRequire(import.meta.url);
|
|
2239
|
-
var cachedVersion = null;
|
|
2240
|
-
function readVersion() {
|
|
2241
|
-
if (cachedVersion !== null) return cachedVersion;
|
|
2242
|
-
try {
|
|
2243
|
-
const injected = "0.5.1";
|
|
2244
|
-
if (typeof injected === "string" && injected.length > 0) {
|
|
2245
|
-
cachedVersion = injected;
|
|
2246
|
-
return cachedVersion;
|
|
2247
|
-
}
|
|
2248
|
-
} catch {
|
|
2249
|
-
}
|
|
2250
|
-
try {
|
|
2251
|
-
const pkg = require_("../../../package.json");
|
|
2252
|
-
cachedVersion = pkg.version ?? "0.0.0";
|
|
2253
|
-
} catch {
|
|
2254
|
-
cachedVersion = "0.0.0";
|
|
2255
|
-
}
|
|
2256
|
-
return cachedVersion;
|
|
2257
|
-
}
|
|
2258
|
-
var PING = {
|
|
2259
|
-
kind: "ping",
|
|
2260
|
-
handle: (_envelope, { ts, deps }) => {
|
|
2261
|
-
const payload = {
|
|
2262
|
-
pong: true,
|
|
2263
|
-
daemonPid: process.pid,
|
|
2264
|
-
uptimeMs: ts - deps.startedAt
|
|
2265
|
-
};
|
|
2266
|
-
return payload;
|
|
2267
|
-
}
|
|
2268
|
-
};
|
|
2269
|
-
var STATUS = {
|
|
2270
|
-
kind: "status",
|
|
2271
|
-
handle: (_envelope, { ts, deps }) => {
|
|
2272
|
-
const channels = (deps.channelManager?.listChannels() ?? []).map((c) => ({
|
|
2273
|
-
id: c.id,
|
|
2274
|
-
state: c.health?.transportOk === false ? "degraded" : "running",
|
|
2275
|
-
...c.health?.at !== void 0 ? { lastHealthAt: c.health.at } : {},
|
|
2276
|
-
...c.health?.note !== void 0 ? { note: c.health.note } : {}
|
|
2277
|
-
}));
|
|
2278
|
-
const payload = {
|
|
2279
|
-
daemonPid: process.pid,
|
|
2280
|
-
startedAt: deps.startedAt,
|
|
2281
|
-
uptimeMs: ts - deps.startedAt,
|
|
2282
|
-
version: readVersion(),
|
|
2283
|
-
listener: deps.getListener?.() ?? {
|
|
2284
|
-
kind: "uds",
|
|
2285
|
-
socketPath: "<unknown>"
|
|
2286
|
-
},
|
|
2287
|
-
channels,
|
|
2288
|
-
runtimes: runtimeStatusEntries(deps.pipeline)
|
|
2289
|
-
};
|
|
2290
|
-
return payload;
|
|
2291
|
-
}
|
|
2292
|
-
};
|
|
2293
|
-
var CHANNELS_RELOAD = {
|
|
2294
|
-
kind: "channels.reload",
|
|
2295
|
-
requires: ["reloadChannels"],
|
|
2296
|
-
unsupportedMessage: "channel reload not configured",
|
|
2297
|
-
handle: async (_envelope, { deps }) => {
|
|
2298
|
-
const payload = await deps.reloadChannels();
|
|
2299
|
-
return payload;
|
|
2300
|
-
}
|
|
2301
|
-
};
|
|
2302
|
-
var SESSION_REGISTER = {
|
|
2303
|
-
kind: "session.register",
|
|
2304
|
-
requires: ["pipeline"],
|
|
2305
|
-
unsupportedMessage: "session.register not configured",
|
|
2306
|
-
handle: (envelope, { deps, connection }) => {
|
|
2307
|
-
const req = envelope.payload;
|
|
2308
|
-
const reg = deps.pipeline.registerRuntime({
|
|
2309
|
-
runtimeId: req.runtimeId,
|
|
2310
|
-
defaultAgentId: req.defaultAgentId,
|
|
2311
|
-
pid: req.pid,
|
|
2312
|
-
connectionId: connection.connectionId,
|
|
2313
|
-
push: connection.push,
|
|
2314
|
-
...req.attachmentId !== void 0 ? { attachmentId: req.attachmentId } : {}
|
|
2315
|
-
});
|
|
2316
|
-
const payload = {
|
|
2317
|
-
registeredAt: reg.registeredAt,
|
|
2318
|
-
gatewayStartedAt: deps.startedAt
|
|
2319
|
-
};
|
|
2320
|
-
return payload;
|
|
2321
|
-
}
|
|
2322
|
-
};
|
|
2323
|
-
var SESSION_UNREGISTER = {
|
|
2324
|
-
kind: "session.unregister",
|
|
2325
|
-
requires: ["pipeline"],
|
|
2326
|
-
unsupportedMessage: "session.unregister not configured",
|
|
2327
|
-
handle: (envelope, { deps, ts }) => {
|
|
2328
|
-
const req = envelope.payload;
|
|
2329
|
-
deps.pipeline.unregisterRuntime(req.runtimeId);
|
|
2330
|
-
const payload = {
|
|
2331
|
-
unregisteredAt: ts
|
|
2332
|
-
};
|
|
2333
|
-
return payload;
|
|
2334
|
-
}
|
|
2335
|
-
};
|
|
2336
|
-
var SESSION_TURN_COMPLETE = {
|
|
2337
|
-
kind: "session.turn.complete",
|
|
2338
|
-
requires: ["pipeline"],
|
|
2339
|
-
unsupportedMessage: "pipeline not configured",
|
|
2340
|
-
handle: async (envelope, { deps }) => {
|
|
2341
|
-
const req = envelope.payload;
|
|
2342
|
-
return await deps.pipeline.handleTurnComplete(req);
|
|
2343
|
-
}
|
|
2344
|
-
};
|
|
2345
|
-
var SESSION_RUN_EVENT = {
|
|
2346
|
-
kind: "session.run.event",
|
|
2347
|
-
requires: ["pipeline"],
|
|
2348
|
-
unsupportedMessage: "pipeline not configured",
|
|
2349
|
-
handle: async (envelope, { deps }) => {
|
|
2350
|
-
const req = envelope.payload;
|
|
2351
|
-
return await deps.pipeline.handleRunEvent(req);
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
var CHANNEL_SEND = {
|
|
2355
|
-
kind: "channel.send",
|
|
2356
|
-
requires: ["channelManager"],
|
|
2357
|
-
unsupportedMessage: "channel manager not configured",
|
|
2358
|
-
handle: async (envelope, { deps }) => {
|
|
2359
|
-
const req = envelope.payload;
|
|
2360
|
-
const result = await deps.channelManager.send(
|
|
2361
|
-
req.message.location.channelId,
|
|
2362
|
-
req.message
|
|
2363
|
-
);
|
|
2364
|
-
const payload = {
|
|
2365
|
-
providerMessageId: result.providerMessageId,
|
|
2366
|
-
deliveredAt: result.deliveredAt
|
|
2367
|
-
};
|
|
2368
|
-
return payload;
|
|
2369
|
-
}
|
|
2370
|
-
};
|
|
2371
|
-
var RELAY_PERMISSION_REQUEST = {
|
|
2372
|
-
kind: "relay.permission.request",
|
|
2373
|
-
requires: ["relayCoordinator"],
|
|
2374
|
-
unsupportedMessage: "relay coordinator not configured",
|
|
2375
|
-
requireRegisteredRuntime: true,
|
|
2376
|
-
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2377
|
-
const req = envelope.payload;
|
|
2378
|
-
const broadcast = deps.relayCoordinator.requestPermission({
|
|
2379
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2380
|
-
toolName: req.toolName,
|
|
2381
|
-
description: req.description,
|
|
2382
|
-
inputPreview: req.inputPreview,
|
|
2383
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2384
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2385
|
-
});
|
|
2386
|
-
const result = await broadcast.result;
|
|
2387
|
-
const payload = {
|
|
2388
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2389
|
-
result
|
|
2390
|
-
};
|
|
2391
|
-
return payload;
|
|
2392
|
-
}
|
|
2393
|
-
};
|
|
2394
|
-
var RELAY_PERMISSION_CANCEL = {
|
|
2395
|
-
kind: "relay.permission.cancel",
|
|
2396
|
-
requires: ["relayCoordinator"],
|
|
2397
|
-
unsupportedMessage: "relay coordinator not configured",
|
|
2398
|
-
requireRegisteredRuntime: true,
|
|
2399
|
-
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2400
|
-
const req = envelope.payload;
|
|
2401
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2402
|
-
req.channelRequestId,
|
|
2403
|
-
req.reason,
|
|
2404
|
-
callerRuntimeId
|
|
2405
|
-
);
|
|
2406
|
-
const payload = { cancelled };
|
|
2407
|
-
return payload;
|
|
2408
|
-
}
|
|
2409
|
-
};
|
|
2410
|
-
var RELAY_QUESTION_REQUEST = {
|
|
2411
|
-
kind: "relay.question.request",
|
|
2412
|
-
requires: ["relayCoordinator"],
|
|
2413
|
-
unsupportedMessage: "relay coordinator not configured",
|
|
2414
|
-
requireRegisteredRuntime: true,
|
|
2415
|
-
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2416
|
-
const req = envelope.payload;
|
|
2417
|
-
const broadcast = deps.relayCoordinator.requestQuestion({
|
|
2418
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2419
|
-
title: req.title,
|
|
2420
|
-
questions: req.questions,
|
|
2421
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2422
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2423
|
-
});
|
|
2424
|
-
const result = await broadcast.result;
|
|
2425
|
-
const payload = {
|
|
2426
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2427
|
-
result
|
|
2428
|
-
};
|
|
2429
|
-
return payload;
|
|
2430
|
-
}
|
|
2431
|
-
};
|
|
2432
|
-
var RELAY_QUESTION_CANCEL = {
|
|
2433
|
-
kind: "relay.question.cancel",
|
|
2434
|
-
requires: ["relayCoordinator"],
|
|
2435
|
-
unsupportedMessage: "relay coordinator not configured",
|
|
2436
|
-
requireRegisteredRuntime: true,
|
|
2437
|
-
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2438
|
-
const req = envelope.payload;
|
|
2439
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2440
|
-
req.channelRequestId,
|
|
2441
|
-
req.reason,
|
|
2442
|
-
callerRuntimeId
|
|
2443
|
-
);
|
|
2444
|
-
const payload = { cancelled };
|
|
2445
|
-
return payload;
|
|
2446
|
-
}
|
|
2447
|
-
};
|
|
2448
|
-
var HANDLERS = new Map(
|
|
2449
|
-
[
|
|
2450
|
-
PING,
|
|
2451
|
-
STATUS,
|
|
2452
|
-
CHANNELS_RELOAD,
|
|
2453
|
-
SESSION_REGISTER,
|
|
2454
|
-
SESSION_UNREGISTER,
|
|
2455
|
-
SESSION_TURN_COMPLETE,
|
|
2456
|
-
SESSION_RUN_EVENT,
|
|
2457
|
-
CHANNEL_SEND,
|
|
2458
|
-
RELAY_PERMISSION_REQUEST,
|
|
2459
|
-
RELAY_PERMISSION_CANCEL,
|
|
2460
|
-
RELAY_QUESTION_REQUEST,
|
|
2461
|
-
RELAY_QUESTION_CANCEL
|
|
2462
|
-
].map((spec) => [spec.kind, spec])
|
|
2463
|
-
);
|
|
2464
|
-
function createDispatcher(deps) {
|
|
2465
|
-
const handle = async (envelope, connection) => {
|
|
2466
|
-
const ts = Date.now();
|
|
2467
|
-
const spec = HANDLERS.get(envelope.kind);
|
|
2468
|
-
if (!spec) {
|
|
2469
|
-
return error(
|
|
2470
|
-
envelope,
|
|
2471
|
-
ts,
|
|
2472
|
-
"unknown_kind",
|
|
2473
|
-
`unknown kind: ${envelope.kind}`
|
|
2474
|
-
);
|
|
2475
|
-
}
|
|
2476
|
-
if (spec.requires) {
|
|
2477
|
-
for (const name of spec.requires) {
|
|
2478
|
-
if (deps[name] === void 0) {
|
|
2479
|
-
return error(
|
|
2480
|
-
envelope,
|
|
2481
|
-
ts,
|
|
2482
|
-
"unsupported",
|
|
2483
|
-
spec.unsupportedMessage ?? `${spec.kind} not configured`
|
|
2484
|
-
);
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
let callerRuntimeId;
|
|
2489
|
-
if (spec.requireRegisteredRuntime) {
|
|
2490
|
-
callerRuntimeId = deps.pipeline?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2491
|
-
if (deps.pipeline && callerRuntimeId === void 0) {
|
|
2492
|
-
return error(
|
|
2493
|
-
envelope,
|
|
2494
|
-
ts,
|
|
2495
|
-
"not_registered",
|
|
2496
|
-
`${spec.kind} requires a registered runtime connection`
|
|
2497
|
-
);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
try {
|
|
2501
|
-
const payload = await spec.handle(envelope, {
|
|
2502
|
-
deps,
|
|
2503
|
-
connection,
|
|
2504
|
-
callerRuntimeId,
|
|
2505
|
-
ts
|
|
2506
|
-
});
|
|
2507
|
-
return ok(envelope, Date.now(), payload);
|
|
2508
|
-
} catch (err) {
|
|
2509
|
-
if (err instanceof AlreadyRegisteredError || err instanceof NotRegisteredError) {
|
|
2510
|
-
return error(envelope, ts, err.code, err.message);
|
|
2511
|
-
}
|
|
2512
|
-
throw err;
|
|
2513
|
-
}
|
|
2514
|
-
};
|
|
2515
|
-
return handle;
|
|
2516
|
-
}
|
|
2517
|
-
function runtimeStatusEntries(pipeline) {
|
|
2518
|
-
const runtime = pipeline?.getCurrentRuntime();
|
|
2519
|
-
if (!runtime || !pipeline) return [];
|
|
2520
|
-
const binding = pipeline.getBinding();
|
|
2521
|
-
return [
|
|
2522
|
-
{
|
|
2523
|
-
runtimeId: runtime.runtimeId,
|
|
2524
|
-
defaultAgentId: runtime.defaultAgentId,
|
|
2525
|
-
pid: runtime.pid,
|
|
2526
|
-
registeredAt: runtime.registeredAt,
|
|
2527
|
-
binding: binding?.state === "active" ? {
|
|
2528
|
-
state: "active",
|
|
2529
|
-
boundAt: binding.boundAt,
|
|
2530
|
-
epoch: binding.epoch,
|
|
2531
|
-
...maybeLastRebindAt(binding.lastRebindAt)
|
|
2532
|
-
} : binding?.state === "stale" ? {
|
|
2533
|
-
state: "stale",
|
|
2534
|
-
staleSince: binding.staleSince,
|
|
2535
|
-
epoch: binding.epoch,
|
|
2536
|
-
...maybeLastRebindAt(binding.lastRebindAt)
|
|
2537
|
-
} : { state: "none" },
|
|
2538
|
-
pendingDispatchCount: pipeline.pendingDispatchCount()
|
|
2539
|
-
}
|
|
2540
|
-
];
|
|
2541
|
-
}
|
|
2542
|
-
function ok(envelope, ts, payload) {
|
|
2543
|
-
return { request_id: envelope.request_id, ts, ok: true, payload };
|
|
2544
|
-
}
|
|
2545
|
-
function error(envelope, ts, code, message) {
|
|
2546
|
-
return {
|
|
2547
|
-
request_id: envelope.request_id,
|
|
2548
|
-
ts,
|
|
2549
|
-
ok: false,
|
|
2550
|
-
error: { code, message }
|
|
2551
|
-
};
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
// src/gateway/control/server.ts
|
|
2555
|
-
var CONNECT_TIMEOUT_MS = 2e3;
|
|
2556
|
-
function isStringRecord(v) {
|
|
2557
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2558
|
-
}
|
|
2559
|
-
function nowError(requestId, code, message) {
|
|
2560
|
-
return {
|
|
2561
|
-
request_id: requestId,
|
|
2562
|
-
ts: Date.now(),
|
|
2563
|
-
ok: false,
|
|
2564
|
-
error: { code, message }
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
var connectionCounter = 0;
|
|
2568
|
-
function nextConnectionId() {
|
|
2569
|
-
connectionCounter = connectionCounter + 1 >>> 0;
|
|
2570
|
-
return `c${connectionCounter}-${process.pid}`;
|
|
2571
|
-
}
|
|
2572
|
-
async function startControlServer(opts) {
|
|
2573
|
-
const { socketPath, token, startedAt, handler } = opts;
|
|
2574
|
-
const logError = opts.logError ?? ((m) => process.stderr.write(m + "\n"));
|
|
2575
|
-
const transport = opts.transport ?? createUdsServerTransport({
|
|
2576
|
-
socketPath,
|
|
2577
|
-
logError
|
|
2578
|
-
});
|
|
2579
|
-
const listener = await transport.listen((connection) => {
|
|
2580
|
-
handleConnection(
|
|
2581
|
-
connection,
|
|
2582
|
-
token,
|
|
2583
|
-
startedAt,
|
|
2584
|
-
handler,
|
|
2585
|
-
logError,
|
|
2586
|
-
opts.onConnect,
|
|
2587
|
-
opts.onDisconnect
|
|
2588
|
-
);
|
|
2589
|
-
});
|
|
2590
|
-
return {
|
|
2591
|
-
close: () => listener.close()
|
|
2592
|
-
};
|
|
2593
|
-
}
|
|
2594
|
-
function handleConnection(connection, expectedToken, startedAt, handler, logError, onConnect, onDisconnect) {
|
|
2595
|
-
let authed = false;
|
|
2596
|
-
const connectTimer = setTimeout(() => {
|
|
2597
|
-
connection.close();
|
|
2598
|
-
}, CONNECT_TIMEOUT_MS);
|
|
2599
|
-
const ctx = {
|
|
2600
|
-
connectionId: nextConnectionId(),
|
|
2601
|
-
push: (env) => connection.send(env),
|
|
2602
|
-
disconnect: () => connection.close()
|
|
2603
|
-
};
|
|
2604
|
-
connection.onFrame((parsed) => {
|
|
2605
|
-
if (!isStringRecord(parsed)) {
|
|
2606
|
-
connection.close();
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
if (!authed) {
|
|
2610
|
-
if (parsed["kind"] !== "connect") {
|
|
2611
|
-
connection.close();
|
|
2612
|
-
return;
|
|
2613
|
-
}
|
|
2614
|
-
const tok = parsed["token"];
|
|
2615
|
-
if (typeof tok !== "string" || !timingSafeTokenEqual(tok, expectedToken)) {
|
|
2616
|
-
connection.send({
|
|
2617
|
-
ok: false,
|
|
2618
|
-
error: { code: "unauthorized", message: "invalid token" }
|
|
2619
|
-
});
|
|
2620
|
-
connection.close();
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
authed = true;
|
|
2624
|
-
clearTimeout(connectTimer);
|
|
2625
|
-
connection.send({
|
|
2626
|
-
ok: true,
|
|
2627
|
-
hello: { daemonPid: process.pid, startedAt }
|
|
2628
|
-
});
|
|
2629
|
-
onConnect?.(ctx);
|
|
2630
|
-
return;
|
|
2631
|
-
}
|
|
2632
|
-
const requestId = typeof parsed["request_id"] === "string" ? parsed["request_id"] : "";
|
|
2633
|
-
if (typeof parsed["kind"] !== "string" || typeof parsed["ts"] !== "number" || !("payload" in parsed) || requestId.length === 0) {
|
|
2634
|
-
connection.send(nowError(requestId, "bad_request", "malformed envelope"));
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
void Promise.resolve().then(() => handler(parsed, ctx)).then((res) => connection.send(res)).catch(
|
|
2638
|
-
(err) => connection.send(
|
|
2639
|
-
nowError(
|
|
2640
|
-
requestId,
|
|
2641
|
-
"handler_error",
|
|
2642
|
-
err instanceof Error ? err.message : String(err)
|
|
2643
|
-
)
|
|
2644
|
-
)
|
|
2645
|
-
);
|
|
2646
|
-
});
|
|
2647
|
-
connection.onError((err) => {
|
|
2648
|
-
const code = err.code;
|
|
2649
|
-
if (code !== "EPIPE" && code !== "ECONNRESET") {
|
|
2650
|
-
logError(`gateway: socket error: ${err.message}`);
|
|
2651
|
-
}
|
|
2652
|
-
});
|
|
2653
|
-
connection.onClose(() => {
|
|
2654
|
-
clearTimeout(connectTimer);
|
|
2655
|
-
if (authed) {
|
|
2656
|
-
onDisconnect?.(ctx);
|
|
2657
|
-
}
|
|
2658
|
-
});
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
// src/gateway/dispatchPipeline.ts
|
|
2662
|
-
import crypto from "crypto";
|
|
2663
|
-
|
|
2664
|
-
// src/gateway/outboundDispatcher.ts
|
|
2665
|
-
var DEFAULT_BACKOFF = [
|
|
2666
|
-
1e3,
|
|
2667
|
-
// 1s
|
|
2668
|
-
2e3,
|
|
2669
|
-
// 2s
|
|
2670
|
-
4e3,
|
|
2671
|
-
// 4s
|
|
2672
|
-
8e3,
|
|
2673
|
-
// 8s
|
|
2674
|
-
16e3,
|
|
2675
|
-
// 16s
|
|
2676
|
-
3e4
|
|
2677
|
-
// 30s
|
|
2678
|
-
];
|
|
2679
|
-
var DEFAULT_MAX_ATTEMPTS = 10;
|
|
2680
|
-
var DEFAULT_TICK_MS = 1e3;
|
|
2681
|
-
var DEFAULT_BATCH = 16;
|
|
2682
|
-
var OutboundDispatcher = class {
|
|
2683
|
-
outbox;
|
|
2684
|
-
send;
|
|
2685
|
-
backoff;
|
|
2686
|
-
maxAttempts;
|
|
2687
|
-
tickMs;
|
|
2688
|
-
batchSize;
|
|
2689
|
-
now;
|
|
2690
|
-
log;
|
|
2691
|
-
timer = null;
|
|
2692
|
-
draining = false;
|
|
2693
|
-
stopped = false;
|
|
2694
|
-
constructor(opts) {
|
|
2695
|
-
this.outbox = opts.outbox;
|
|
2696
|
-
this.send = opts.send;
|
|
2697
|
-
this.backoff = opts.backoffSchedule ?? DEFAULT_BACKOFF;
|
|
2698
|
-
this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2699
|
-
this.tickMs = opts.tickIntervalMs ?? DEFAULT_TICK_MS;
|
|
2700
|
-
this.batchSize = opts.drainBatchSize ?? DEFAULT_BATCH;
|
|
2701
|
-
this.now = opts.now ?? Date.now;
|
|
2702
|
-
this.log = opts.log;
|
|
2703
|
-
}
|
|
2704
|
-
start() {
|
|
2705
|
-
if (this.timer) return;
|
|
2706
|
-
this.timer = setInterval(() => {
|
|
2707
|
-
void this.drain();
|
|
2708
|
-
}, this.tickMs);
|
|
2709
|
-
this.timer.unref();
|
|
2710
|
-
}
|
|
2711
|
-
stop() {
|
|
2712
|
-
this.stopped = true;
|
|
2713
|
-
if (this.timer) {
|
|
2714
|
-
clearInterval(this.timer);
|
|
2715
|
-
this.timer = null;
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
async dispatch(channelId, msg) {
|
|
2719
|
-
try {
|
|
2720
|
-
const result = await this.send(channelId, msg);
|
|
2721
|
-
return { kind: "sent", result };
|
|
2722
|
-
} catch (err) {
|
|
2723
|
-
const error2 = err instanceof Error ? err.message : String(err);
|
|
2724
|
-
const nextAttemptAt = this.now() + this.backoffFor(0);
|
|
2725
|
-
const id = this.outbox.enqueue({
|
|
2726
|
-
channelId,
|
|
2727
|
-
message: msg,
|
|
2728
|
-
nextAttemptAt,
|
|
2729
|
-
lastError: error2
|
|
2730
|
-
});
|
|
2731
|
-
this.log?.(
|
|
2732
|
-
"warn",
|
|
2733
|
-
`send to ${channelId} failed; parked as outbox#${id}: ${error2}`
|
|
2734
|
-
);
|
|
2735
|
-
return { kind: "queued", outboxId: id, error: error2 };
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
/**
|
|
2739
|
-
* Drain due entries. Exposed for tests; the timer also calls this. Safe
|
|
2740
|
-
* to call concurrently — a re-entry guard short-circuits.
|
|
2741
|
-
*/
|
|
2742
|
-
async drain() {
|
|
2743
|
-
if (this.draining || this.stopped) {
|
|
2744
|
-
return { retried: 0, succeeded: 0, dropped: 0 };
|
|
2745
|
-
}
|
|
2746
|
-
this.draining = true;
|
|
2747
|
-
let retried = 0;
|
|
2748
|
-
let succeeded = 0;
|
|
2749
|
-
let dropped = 0;
|
|
2750
|
-
try {
|
|
2751
|
-
const due = this.outbox.peekDue(this.now(), this.batchSize);
|
|
2752
|
-
for (const row of due) {
|
|
2753
|
-
retried += 1;
|
|
2754
|
-
const outcome = await this.attempt(row);
|
|
2755
|
-
if (outcome === "succeeded") succeeded += 1;
|
|
2756
|
-
else if (outcome === "dropped") dropped += 1;
|
|
2757
|
-
}
|
|
2758
|
-
} finally {
|
|
2759
|
-
this.draining = false;
|
|
2760
|
-
}
|
|
2761
|
-
return { retried, succeeded, dropped };
|
|
2762
|
-
}
|
|
2763
|
-
async attempt(row) {
|
|
2764
|
-
try {
|
|
2765
|
-
await this.send(row.channelId, row.message);
|
|
2766
|
-
this.outbox.delete(row.id);
|
|
2767
|
-
this.log?.(
|
|
2768
|
-
"info",
|
|
2769
|
-
`outbox#${row.id} delivered to ${row.channelId} on attempt ${row.attempt + 1}`
|
|
2770
|
-
);
|
|
2771
|
-
return "succeeded";
|
|
2772
|
-
} catch (err) {
|
|
2773
|
-
const error2 = err instanceof Error ? err.message : String(err);
|
|
2774
|
-
const nextAttempt = row.attempt + 1;
|
|
2775
|
-
if (nextAttempt >= this.maxAttempts) {
|
|
2776
|
-
this.outbox.delete(row.id);
|
|
2777
|
-
this.log?.(
|
|
2778
|
-
"error",
|
|
2779
|
-
`outbox#${row.id} dropped after ${nextAttempt} attempts: ${error2}`
|
|
2780
|
-
);
|
|
2781
|
-
return "dropped";
|
|
2782
|
-
}
|
|
2783
|
-
const nextAttemptAt = this.now() + this.backoffFor(nextAttempt);
|
|
2784
|
-
this.outbox.recordFailure({
|
|
2785
|
-
id: row.id,
|
|
2786
|
-
nextAttemptAt,
|
|
2787
|
-
lastError: error2
|
|
2788
|
-
});
|
|
2789
|
-
return "requeued";
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
backoffFor(attempt) {
|
|
2793
|
-
if (this.backoff.length === 0) return 1e3;
|
|
2794
|
-
const idx = Math.min(attempt, this.backoff.length - 1);
|
|
2795
|
-
return this.backoff[idx];
|
|
2796
|
-
}
|
|
2797
|
-
};
|
|
2798
|
-
|
|
2799
|
-
// src/gateway/router/sessionKey.ts
|
|
2800
|
-
function deriveSessionKey(loc) {
|
|
2801
|
-
const c = loc.channelId;
|
|
2802
|
-
const a = loc.accountId;
|
|
2803
|
-
if (loc.peer?.id) {
|
|
2804
|
-
const peer = loc.peer.id;
|
|
2805
|
-
if (loc.thread?.id) {
|
|
2806
|
-
return `peer:${c}:${a}:${peer}:${loc.thread.id}`;
|
|
2807
|
-
}
|
|
2808
|
-
return `peer:${c}:${a}:${peer}`;
|
|
2809
|
-
}
|
|
2810
|
-
if (loc.room?.id) {
|
|
2811
|
-
const room = loc.room.id;
|
|
2812
|
-
if (loc.thread?.id) {
|
|
2813
|
-
return `room:${c}:${a}:${room}:${loc.thread.id}`;
|
|
2814
|
-
}
|
|
2815
|
-
return `room:${c}:${a}:${room}`;
|
|
2816
|
-
}
|
|
2817
|
-
return `default:${c}:${a}`;
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
// src/gateway/sessionRegistry.ts
|
|
2821
|
-
import { randomUUID } from "crypto";
|
|
2822
|
-
var UnknownDispatchError = class extends Error {
|
|
2823
|
-
code = "unknown_dispatch";
|
|
2824
|
-
constructor(id) {
|
|
2825
|
-
super(`unknown dispatchId: ${id}`);
|
|
2826
|
-
this.name = "UnknownDispatchError";
|
|
2827
|
-
}
|
|
2828
|
-
};
|
|
2829
|
-
var SessionRegistry = class {
|
|
2830
|
-
dispatches = /* @__PURE__ */ new Map();
|
|
2831
|
-
idFactory;
|
|
2832
|
-
now;
|
|
2833
|
-
constructor(opts = {}) {
|
|
2834
|
-
this.idFactory = opts.idFactory ?? randomUUID;
|
|
2835
|
-
this.now = opts.now ?? Date.now;
|
|
2836
|
-
}
|
|
2837
|
-
beginDispatch(input) {
|
|
2838
|
-
const dispatchId = this.idFactory();
|
|
2839
|
-
const entry = {
|
|
2840
|
-
dispatchId,
|
|
2841
|
-
sessionKey: input.sessionKey,
|
|
2842
|
-
agentId: input.agentId,
|
|
2843
|
-
location: input.location,
|
|
2844
|
-
createdAt: this.now()
|
|
2845
|
-
};
|
|
2846
|
-
this.dispatches.set(dispatchId, entry);
|
|
2847
|
-
return entry;
|
|
2848
|
-
}
|
|
2849
|
-
completeDispatch(dispatchId) {
|
|
2850
|
-
const entry = this.dispatches.get(dispatchId);
|
|
2851
|
-
if (!entry) {
|
|
2852
|
-
throw new UnknownDispatchError(dispatchId);
|
|
2853
|
-
}
|
|
2854
|
-
this.dispatches.delete(dispatchId);
|
|
2855
|
-
return entry;
|
|
2856
|
-
}
|
|
2857
|
-
pendingDispatchCount() {
|
|
2858
|
-
return this.dispatches.size;
|
|
2859
|
-
}
|
|
2860
|
-
clearDispatches() {
|
|
2861
|
-
this.dispatches.clear();
|
|
2862
|
-
}
|
|
2863
|
-
};
|
|
2864
|
-
|
|
2865
|
-
// src/gateway/state/inboundQueue.ts
|
|
2866
|
-
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2867
|
-
var InboundQueue = class {
|
|
2868
|
-
db;
|
|
2869
|
-
maxEntries;
|
|
2870
|
-
constructor(db, opts = {}) {
|
|
2871
|
-
this.db = db;
|
|
2872
|
-
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2873
|
-
}
|
|
2874
|
-
size() {
|
|
2875
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM inbound_queue").get();
|
|
2876
|
-
return row.n;
|
|
2877
|
-
}
|
|
2878
|
-
enqueue(inbound) {
|
|
2879
|
-
if (this.size() >= this.maxEntries) {
|
|
2880
|
-
return { kind: "rejected", reason: "queue_full" };
|
|
2881
|
-
}
|
|
2882
|
-
const stmt = this.db.prepare(
|
|
2883
|
-
`INSERT INTO inbound_queue
|
|
2884
|
-
(channel_id, account_id, idempotency_key, payload_json, created_at)
|
|
2885
|
-
VALUES (?, ?, ?, ?, ?)
|
|
2886
|
-
ON CONFLICT(channel_id, account_id, idempotency_key) DO NOTHING`
|
|
2887
|
-
);
|
|
2888
|
-
const result = stmt.run(
|
|
2889
|
-
inbound.location.channelId,
|
|
2890
|
-
inbound.location.accountId,
|
|
2891
|
-
inbound.idempotencyKey,
|
|
2892
|
-
JSON.stringify(inbound),
|
|
2893
|
-
Date.now()
|
|
2894
|
-
);
|
|
2895
|
-
if (result.changes === 0) {
|
|
2896
|
-
return { kind: "duplicate" };
|
|
2897
|
-
}
|
|
2898
|
-
return { kind: "queued", id: Number(result.lastInsertRowid) };
|
|
2899
|
-
}
|
|
2900
|
-
/** Atomically read and remove all parked entries in FIFO order. */
|
|
2901
|
-
drain() {
|
|
2902
|
-
return this.db.transaction(() => {
|
|
2903
|
-
const rows = this.db.prepare("SELECT id, payload_json FROM inbound_queue ORDER BY id ASC").all();
|
|
2904
|
-
if (rows.length > 0) {
|
|
2905
|
-
this.db.prepare("DELETE FROM inbound_queue").run();
|
|
2906
|
-
}
|
|
2907
|
-
return rows.map((r) => ({
|
|
2908
|
-
id: r.id,
|
|
2909
|
-
inbound: JSON.parse(r.payload_json)
|
|
2910
|
-
}));
|
|
2911
|
-
})();
|
|
2912
|
-
}
|
|
2913
|
-
};
|
|
2914
|
-
|
|
2915
|
-
// src/gateway/state/outbox.ts
|
|
2916
|
-
var Outbox = class {
|
|
2917
|
-
db;
|
|
2918
|
-
constructor(db) {
|
|
2919
|
-
this.db = db;
|
|
2920
|
-
}
|
|
2921
|
-
size() {
|
|
2922
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM channel_outbox").get();
|
|
2923
|
-
return row.n;
|
|
2924
|
-
}
|
|
2925
|
-
enqueue(input) {
|
|
2926
|
-
const result = this.db.prepare(
|
|
2927
|
-
`INSERT INTO channel_outbox
|
|
2928
|
-
(channel_id, payload_json, attempt, next_attempt_at, last_error, created_at)
|
|
2929
|
-
VALUES (?, ?, 0, ?, ?, ?)`
|
|
2930
|
-
).run(
|
|
2931
|
-
input.channelId,
|
|
2932
|
-
JSON.stringify(input.message),
|
|
2933
|
-
input.nextAttemptAt,
|
|
2934
|
-
input.lastError ?? null,
|
|
2935
|
-
Date.now()
|
|
2936
|
-
);
|
|
2937
|
-
return Number(result.lastInsertRowid);
|
|
2938
|
-
}
|
|
2939
|
-
/** Rows whose `next_attempt_at` is at or before `now`, oldest first. */
|
|
2940
|
-
peekDue(now, limit) {
|
|
2941
|
-
const rows = this.db.prepare(
|
|
2942
|
-
`SELECT id, channel_id, payload_json, attempt, next_attempt_at, last_error
|
|
2943
|
-
FROM channel_outbox
|
|
2944
|
-
WHERE next_attempt_at <= ?
|
|
2945
|
-
ORDER BY next_attempt_at ASC, id ASC
|
|
2946
|
-
LIMIT ?`
|
|
2947
|
-
).all(now, limit);
|
|
2948
|
-
return rows.map((r) => ({
|
|
2949
|
-
id: r.id,
|
|
2950
|
-
channelId: r.channel_id,
|
|
2951
|
-
message: JSON.parse(r.payload_json),
|
|
2952
|
-
attempt: r.attempt,
|
|
2953
|
-
nextAttemptAt: r.next_attempt_at,
|
|
2954
|
-
lastError: r.last_error
|
|
2955
|
-
}));
|
|
2956
|
-
}
|
|
2957
|
-
delete(id) {
|
|
2958
|
-
this.db.prepare("DELETE FROM channel_outbox WHERE id = ?").run(id);
|
|
2959
|
-
}
|
|
2960
|
-
recordFailure(input) {
|
|
2961
|
-
this.db.prepare(
|
|
2962
|
-
`UPDATE channel_outbox
|
|
2963
|
-
SET attempt = attempt + 1,
|
|
2964
|
-
next_attempt_at = ?,
|
|
2965
|
-
last_error = ?
|
|
2966
|
-
WHERE id = ?`
|
|
2967
|
-
).run(input.nextAttemptAt, input.lastError, input.id);
|
|
2968
|
-
}
|
|
2969
|
-
};
|
|
2970
|
-
|
|
2971
|
-
// src/gateway/dispatchPipeline.ts
|
|
2972
|
-
var DispatchPipeline = class {
|
|
2973
|
-
bindingStore;
|
|
2974
|
-
registry;
|
|
2975
|
-
inboundQueue;
|
|
2976
|
-
outbox;
|
|
2977
|
-
outboundDispatcher;
|
|
2978
|
-
resolveAgent;
|
|
2979
|
-
log;
|
|
2980
|
-
now;
|
|
2981
|
-
idFactory;
|
|
2982
|
-
pushes = /* @__PURE__ */ new Map();
|
|
2983
|
-
connectionToKey = /* @__PURE__ */ new Map();
|
|
2984
|
-
constructor(opts) {
|
|
2985
|
-
this.bindingStore = new RuntimeBindingStore({
|
|
2986
|
-
gracePeriodMs: opts.gracePeriodMs,
|
|
2987
|
-
observers: opts.observers,
|
|
2988
|
-
now: opts.now
|
|
2989
|
-
});
|
|
2990
|
-
this.registry = new SessionRegistry({
|
|
2991
|
-
now: opts.now ?? Date.now,
|
|
2992
|
-
...opts.idFactory ? { idFactory: opts.idFactory } : {}
|
|
2993
|
-
});
|
|
2994
|
-
this.inboundQueue = new InboundQueue(opts.stateDb, opts.inboundQueue ?? {});
|
|
2995
|
-
this.outbox = new Outbox(opts.stateDb);
|
|
2996
|
-
this.outboundDispatcher = new OutboundDispatcher({
|
|
2997
|
-
outbox: this.outbox,
|
|
2998
|
-
send: opts.send,
|
|
2999
|
-
...opts.outbox?.backoffSchedule ? { backoffSchedule: opts.outbox.backoffSchedule } : {},
|
|
3000
|
-
...opts.outbox?.maxAttempts !== void 0 ? { maxAttempts: opts.outbox.maxAttempts } : {},
|
|
3001
|
-
...opts.outbox?.tickIntervalMs !== void 0 ? { tickIntervalMs: opts.outbox.tickIntervalMs } : {},
|
|
3002
|
-
...opts.outbox?.drainBatchSize !== void 0 ? { drainBatchSize: opts.outbox.drainBatchSize } : {},
|
|
3003
|
-
...opts.now ? { now: opts.now } : {},
|
|
3004
|
-
...opts.log ? { log: opts.log } : {}
|
|
3005
|
-
});
|
|
3006
|
-
this.resolveAgent = opts.resolveAgent ?? ((input) => input.defaultAgentId);
|
|
3007
|
-
this.log = opts.log;
|
|
3008
|
-
this.now = opts.now ?? Date.now;
|
|
3009
|
-
this.idFactory = opts.idFactory ?? crypto.randomUUID;
|
|
3010
|
-
}
|
|
3011
|
-
// ── lifecycle ────────────────────────────────────────────
|
|
3012
|
-
start() {
|
|
3013
|
-
this.outboundDispatcher.start();
|
|
3014
|
-
}
|
|
3015
|
-
async stop() {
|
|
3016
|
-
this.outboundDispatcher.stop();
|
|
3017
|
-
this.bindingStore.stop();
|
|
3018
|
-
}
|
|
3019
|
-
// ── inbound (channel side) ───────────────────────────────
|
|
3020
|
-
handleInbound(inbound, options = {}) {
|
|
3021
|
-
const key = options.attachmentId;
|
|
3022
|
-
const current = this.bindingStore.getCurrentByAttachment(key);
|
|
3023
|
-
if (!current || !this.bindingStore.hasActiveBindingForAttachment(key)) {
|
|
3024
|
-
const result = this.inboundQueue.enqueue(inbound);
|
|
3025
|
-
if (result.kind === "queued") {
|
|
3026
|
-
this.log?.(
|
|
3027
|
-
"info",
|
|
3028
|
-
`no runtime registered; parked inbound ${inbound.idempotencyKey} as queue#${result.id}`
|
|
3029
|
-
);
|
|
3030
|
-
return { kind: "queued", queueId: result.id };
|
|
3031
|
-
}
|
|
3032
|
-
if (result.kind === "duplicate") {
|
|
3033
|
-
this.log?.(
|
|
3034
|
-
"debug",
|
|
3035
|
-
`inbound ${inbound.idempotencyKey} already parked; ignoring duplicate`
|
|
3036
|
-
);
|
|
3037
|
-
return { kind: "dropped", reason: "no_runtime" };
|
|
3038
|
-
}
|
|
3039
|
-
this.log?.(
|
|
3040
|
-
"warn",
|
|
3041
|
-
`inbound queue full (>=${this.inboundQueue.size()}); dropping ${inbound.idempotencyKey}`
|
|
3042
|
-
);
|
|
3043
|
-
return { kind: "dropped", reason: "queue_full" };
|
|
3044
|
-
}
|
|
3045
|
-
return this.dispatchInboundToRuntime(inbound, current, key);
|
|
3046
|
-
}
|
|
3047
|
-
dispatchInboundToRuntime(inbound, current, key) {
|
|
3048
|
-
const sessionKey = deriveSessionKey(inbound.location);
|
|
3049
|
-
const agentId = this.resolveAgent({
|
|
3050
|
-
sessionKey,
|
|
3051
|
-
channelId: inbound.location.channelId,
|
|
3052
|
-
defaultAgentId: current.defaultAgentId
|
|
3053
|
-
});
|
|
3054
|
-
if (!agentId) {
|
|
3055
|
-
this.log?.(
|
|
3056
|
-
"warn",
|
|
3057
|
-
`agent resolution returned empty for sessionKey=${sessionKey}`
|
|
3058
|
-
);
|
|
3059
|
-
return { kind: "dropped", reason: "no_default_agent" };
|
|
3060
|
-
}
|
|
3061
|
-
const entry = this.registry.beginDispatch({
|
|
3062
|
-
sessionKey,
|
|
3063
|
-
agentId,
|
|
3064
|
-
location: inbound.location
|
|
3065
|
-
});
|
|
3066
|
-
this.pushDispatch(key, {
|
|
3067
|
-
dispatchId: entry.dispatchId,
|
|
3068
|
-
sessionKey,
|
|
3069
|
-
agentId,
|
|
3070
|
-
inbound
|
|
3071
|
-
});
|
|
3072
|
-
return { kind: "dispatched", dispatchId: entry.dispatchId, sessionKey };
|
|
3073
|
-
}
|
|
3074
|
-
pushDispatch(key, payload) {
|
|
3075
|
-
const handle = this.pushes.get(key);
|
|
3076
|
-
if (!handle) return;
|
|
3077
|
-
handle.push({
|
|
3078
|
-
push_id: this.idFactory(),
|
|
3079
|
-
ts: this.now(),
|
|
3080
|
-
kind: "session.dispatch.turn",
|
|
3081
|
-
payload
|
|
3082
|
-
});
|
|
3083
|
-
}
|
|
3084
|
-
// ── runtime side ─────────────────────────────────────────
|
|
3085
|
-
registerRuntime(input) {
|
|
3086
|
-
const key = input.attachmentId;
|
|
3087
|
-
const result = this.bindingStore.bind({
|
|
3088
|
-
runtimeId: input.runtimeId,
|
|
3089
|
-
defaultAgentId: input.defaultAgentId,
|
|
3090
|
-
pid: input.pid,
|
|
3091
|
-
connectionId: input.connectionId,
|
|
3092
|
-
...input.attachmentId !== void 0 ? { attachmentId: input.attachmentId } : {}
|
|
3093
|
-
});
|
|
3094
|
-
const previous = this.pushes.get(key);
|
|
3095
|
-
if (previous && previous.connectionId !== input.connectionId) {
|
|
3096
|
-
this.connectionToKey.delete(previous.connectionId);
|
|
3097
|
-
}
|
|
3098
|
-
this.pushes.set(key, {
|
|
3099
|
-
connectionId: input.connectionId,
|
|
3100
|
-
push: input.push
|
|
3101
|
-
});
|
|
3102
|
-
this.connectionToKey.set(input.connectionId, key);
|
|
3103
|
-
writeGatewayTrace(
|
|
3104
|
-
`pipeline registered runtime runtimeId=${input.runtimeId} connectionId=${input.connectionId}`
|
|
3105
|
-
);
|
|
3106
|
-
this.drainPending(key);
|
|
3107
|
-
return { registeredAt: result.registeredAt };
|
|
3108
|
-
}
|
|
3109
|
-
unregisterRuntime(runtimeId) {
|
|
3110
|
-
const slot = this.findSlotByRuntimeId(runtimeId);
|
|
3111
|
-
this.bindingStore.unbind(runtimeId);
|
|
3112
|
-
this.registry.clearDispatches();
|
|
3113
|
-
if (slot) {
|
|
3114
|
-
const handle = this.pushes.get(slot.key);
|
|
3115
|
-
this.pushes.delete(slot.key);
|
|
3116
|
-
if (handle) this.connectionToKey.delete(handle.connectionId);
|
|
3117
|
-
}
|
|
3118
|
-
writeGatewayTrace(`pipeline unregistered runtime runtimeId=${runtimeId}`);
|
|
3119
|
-
}
|
|
3120
|
-
notifyConnectionClosed(connectionId) {
|
|
3121
|
-
const key = this.connectionToKey.get(connectionId);
|
|
3122
|
-
const runtimeId = this.bindingStore.notifyConnectionClosed(connectionId);
|
|
3123
|
-
if (runtimeId === null) return;
|
|
3124
|
-
writeGatewayTrace(
|
|
3125
|
-
`pipeline runtime connection closed runtimeId=${runtimeId} connectionId=${connectionId}`
|
|
3126
|
-
);
|
|
3127
|
-
if (key !== void 0 || this.pushes.has(key)) {
|
|
3128
|
-
this.pushes.delete(key);
|
|
3129
|
-
this.connectionToKey.delete(connectionId);
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
/**
|
|
3133
|
-
* Streaming run-event from a runner harness child to its outbound
|
|
3134
|
-
* adapter. Independent of dispatch state — `runId`/`seq` are the runner
|
|
3135
|
-
* protocol's own correlation, not the gateway's `dispatchId`. The
|
|
3136
|
-
* outbound text is the wire envelope shape RunnerAdapter expects on
|
|
3137
|
-
* `OutboundMessage.text`.
|
|
3138
|
-
*/
|
|
3139
|
-
async handleRunEvent(payload) {
|
|
3140
|
-
const slot = this.findSlotByRuntimeId(payload.runtimeId);
|
|
3141
|
-
if (!slot) {
|
|
3142
|
-
throw new Error("runtime mismatch on session.run.event");
|
|
3143
|
-
}
|
|
3144
|
-
const envelopeText = JSON.stringify({
|
|
3145
|
-
kind: "run_event",
|
|
3146
|
-
runId: payload.runId,
|
|
3147
|
-
seq: payload.seq,
|
|
3148
|
-
ts: payload.ts,
|
|
3149
|
-
eventKind: payload.kind,
|
|
3150
|
-
...payload.payload !== void 0 ? { payload: payload.payload } : {}
|
|
3151
|
-
});
|
|
3152
|
-
const out = {
|
|
3153
|
-
location: payload.location,
|
|
3154
|
-
text: envelopeText,
|
|
3155
|
-
idempotencyKey: `run_event:${payload.runId}:${payload.seq}`
|
|
3156
|
-
};
|
|
3157
|
-
try {
|
|
3158
|
-
await this.outboundDispatcher.dispatch(payload.location.channelId, out);
|
|
3159
|
-
} catch {
|
|
3160
|
-
return { delivered: false };
|
|
3161
|
-
}
|
|
3162
|
-
return { delivered: true };
|
|
3163
|
-
}
|
|
3164
|
-
async handleTurnComplete(payload) {
|
|
3165
|
-
const slot = this.findSlotByRuntimeId(payload.runtimeId);
|
|
3166
|
-
writeGatewayTrace(
|
|
3167
|
-
`pipeline turn.complete received runtimeId=${payload.runtimeId} dispatchId=${payload.dispatchId} channel=${payload.location.channelId} account=${payload.location.accountId} thread=${payload.location.thread?.id ?? ""} textLength=${payload.text.length}`
|
|
3168
|
-
);
|
|
3169
|
-
if (!slot) {
|
|
3170
|
-
throw new Error("runtime mismatch on session.turn.complete");
|
|
3171
|
-
}
|
|
3172
|
-
let entry;
|
|
3173
|
-
try {
|
|
3174
|
-
entry = this.registry.completeDispatch(payload.dispatchId);
|
|
3175
|
-
} catch (err) {
|
|
3176
|
-
if (err instanceof UnknownDispatchError) {
|
|
3177
|
-
writeGatewayTrace(
|
|
3178
|
-
`pipeline turn.complete unknown dispatchId=${payload.dispatchId}`
|
|
3179
|
-
);
|
|
3180
|
-
return { delivered: false };
|
|
3181
|
-
}
|
|
3182
|
-
throw err;
|
|
3183
|
-
}
|
|
3184
|
-
const result = await this.sendOutbound(entry.location, payload);
|
|
3185
|
-
writeGatewayTrace(
|
|
3186
|
-
`pipeline sendOutbound delivered dispatchId=${payload.dispatchId} providerMessageId=${result.providerMessageId}`
|
|
3187
|
-
);
|
|
3188
|
-
return { delivered: true, providerMessageId: result.providerMessageId };
|
|
3189
|
-
}
|
|
3190
|
-
async sendOutbound(_parkedLocation, payload) {
|
|
3191
|
-
const out = {
|
|
3192
|
-
location: payload.location,
|
|
3193
|
-
text: payload.text,
|
|
3194
|
-
idempotencyKey: payload.idempotencyKey
|
|
3195
|
-
};
|
|
3196
|
-
const result = await this.outboundDispatcher.dispatch(
|
|
3197
|
-
payload.location.channelId,
|
|
3198
|
-
out
|
|
3199
|
-
);
|
|
3200
|
-
if (result.kind === "sent") return result.result;
|
|
3201
|
-
return {
|
|
3202
|
-
providerMessageId: `outbox:${result.outboxId}`,
|
|
3203
|
-
deliveredAt: this.now()
|
|
3204
|
-
};
|
|
3205
|
-
}
|
|
3206
|
-
findSlotByRuntimeId(runtimeId) {
|
|
3207
|
-
return this.bindingStore.getAttachmentKeyByRuntimeId(runtimeId);
|
|
3208
|
-
}
|
|
3209
|
-
drainPending(key) {
|
|
3210
|
-
const current = this.bindingStore.getCurrentByAttachment(key);
|
|
3211
|
-
if (!current || !this.bindingStore.hasActiveBinding(current.runtimeId))
|
|
3212
|
-
return;
|
|
3213
|
-
const parked = this.inboundQueue.drain();
|
|
3214
|
-
let dispatched = 0;
|
|
3215
|
-
let dropped = 0;
|
|
3216
|
-
for (const { inbound } of parked) {
|
|
3217
|
-
const result = this.dispatchInboundToRuntime(inbound, current, key);
|
|
3218
|
-
if (result.kind === "dispatched") dispatched += 1;
|
|
3219
|
-
else dropped += 1;
|
|
3220
|
-
}
|
|
3221
|
-
if (dispatched > 0 || dropped > 0) {
|
|
3222
|
-
this.log?.(
|
|
3223
|
-
"info",
|
|
3224
|
-
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
// ── reads ────────────────────────────────────────────────
|
|
3229
|
-
getCurrentRuntime() {
|
|
3230
|
-
return this.bindingStore.getCurrent();
|
|
3231
|
-
}
|
|
3232
|
-
getCurrentRuntimeByAttachment(attachmentId) {
|
|
3233
|
-
return this.bindingStore.getCurrentByAttachment(attachmentId);
|
|
3234
|
-
}
|
|
3235
|
-
getBinding() {
|
|
3236
|
-
return this.bindingStore.getBinding();
|
|
3237
|
-
}
|
|
3238
|
-
hasActiveBinding(runtimeId) {
|
|
3239
|
-
return this.bindingStore.hasActiveBinding(runtimeId);
|
|
3240
|
-
}
|
|
3241
|
-
getRuntimeIdByConnection(connectionId) {
|
|
3242
|
-
return this.bindingStore.getRuntimeIdByConnection(connectionId);
|
|
3243
|
-
}
|
|
3244
|
-
pendingDispatchCount() {
|
|
3245
|
-
return this.registry.pendingDispatchCount();
|
|
3246
|
-
}
|
|
3247
|
-
pendingInboundCount() {
|
|
3248
|
-
return this.inboundQueue.size();
|
|
3249
|
-
}
|
|
3250
|
-
pendingOutboxCount() {
|
|
3251
|
-
return this.outbox.size();
|
|
3252
|
-
}
|
|
3253
|
-
};
|
|
3254
|
-
|
|
3255
|
-
// src/gateway/lock.ts
|
|
3256
|
-
import fs2 from "fs";
|
|
3257
|
-
import path2 from "path";
|
|
3258
|
-
var GatewayAlreadyRunningError = class extends Error {
|
|
3259
|
-
otherPid;
|
|
3260
|
-
constructor(otherPid) {
|
|
3261
|
-
super(`gateway already running (pid=${otherPid})`);
|
|
3262
|
-
this.name = "GatewayAlreadyRunningError";
|
|
3263
|
-
this.otherPid = otherPid;
|
|
3264
|
-
}
|
|
3265
|
-
};
|
|
3266
|
-
function isProcessAlive(pid) {
|
|
3267
|
-
try {
|
|
3268
|
-
process.kill(pid, 0);
|
|
3269
|
-
return true;
|
|
3270
|
-
} catch (err) {
|
|
3271
|
-
const code = err.code;
|
|
3272
|
-
return code === "EPERM";
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
function readPidFile(p) {
|
|
3276
|
-
try {
|
|
3277
|
-
const text = fs2.readFileSync(p, "utf-8").trim();
|
|
3278
|
-
const pid = Number.parseInt(text, 10);
|
|
3279
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
3280
|
-
} catch {
|
|
3281
|
-
return null;
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
function acquireLock(lockPath) {
|
|
3285
|
-
fs2.mkdirSync(path2.dirname(lockPath), { recursive: true, mode: 448 });
|
|
3286
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3287
|
-
try {
|
|
3288
|
-
const fd = fs2.openSync(lockPath, "wx", 384);
|
|
3289
|
-
fs2.writeSync(fd, String(process.pid) + "\n");
|
|
3290
|
-
fs2.closeSync(fd);
|
|
3291
|
-
return {
|
|
3292
|
-
path: lockPath,
|
|
3293
|
-
pid: process.pid,
|
|
3294
|
-
release: () => {
|
|
3295
|
-
try {
|
|
3296
|
-
const pidNow = readPidFile(lockPath);
|
|
3297
|
-
if (pidNow === process.pid) {
|
|
3298
|
-
fs2.unlinkSync(lockPath);
|
|
3299
|
-
}
|
|
3300
|
-
} catch {
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
};
|
|
3304
|
-
} catch (err) {
|
|
3305
|
-
const code = err.code;
|
|
3306
|
-
if (code !== "EEXIST") throw err;
|
|
3307
|
-
const otherPid = readPidFile(lockPath);
|
|
3308
|
-
if (otherPid !== null && isProcessAlive(otherPid)) {
|
|
3309
|
-
throw new GatewayAlreadyRunningError(otherPid);
|
|
3310
|
-
}
|
|
3311
|
-
try {
|
|
3312
|
-
fs2.unlinkSync(lockPath);
|
|
3313
|
-
} catch {
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
throw new Error(`failed to acquire gateway lock at ${lockPath}`);
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
// src/gateway/relay/pendingRegistry.ts
|
|
3321
|
-
var PendingRegistry = class {
|
|
3322
|
-
entries = /* @__PURE__ */ new Map();
|
|
3323
|
-
inspect(channelRequestId, kind, fingerprint, runtimeId) {
|
|
3324
|
-
const existing = this.entries.get(channelRequestId);
|
|
3325
|
-
if (!existing) return { kind: "absent" };
|
|
3326
|
-
if (existing.kind !== kind) return { kind: "collision", reason: "kind" };
|
|
3327
|
-
if (existing.fingerprint !== fingerprint) {
|
|
3328
|
-
return { kind: "collision", reason: "payload" };
|
|
3329
|
-
}
|
|
3330
|
-
if (existing.runtimeId !== runtimeId) {
|
|
3331
|
-
return { kind: "collision", reason: "owner" };
|
|
3332
|
-
}
|
|
3333
|
-
return { kind: "attach", entry: existing };
|
|
3334
|
-
}
|
|
3335
|
-
register(entry) {
|
|
3336
|
-
this.entries.set(entry.channelRequestId, entry);
|
|
3337
|
-
}
|
|
3338
|
-
settle(channelRequestId, result) {
|
|
3339
|
-
const entry = this.entries.get(channelRequestId);
|
|
3340
|
-
if (!entry || entry.settled) return false;
|
|
3341
|
-
entry.settled = true;
|
|
3342
|
-
this.entries.delete(channelRequestId);
|
|
3343
|
-
clearTimeout(entry.timer);
|
|
3344
|
-
for (const ctrl of entry.controllers) {
|
|
3345
|
-
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3346
|
-
}
|
|
3347
|
-
entry.resolve(result);
|
|
3348
|
-
return true;
|
|
3349
|
-
}
|
|
3350
|
-
cancel(channelRequestId, reason, expectedRuntimeId) {
|
|
3351
|
-
const entry = this.entries.get(channelRequestId);
|
|
3352
|
-
if (!entry) return false;
|
|
3353
|
-
if (expectedRuntimeId !== void 0 && entry.runtimeId !== void 0 && entry.runtimeId !== expectedRuntimeId) {
|
|
3354
|
-
return false;
|
|
3355
|
-
}
|
|
3356
|
-
return this.settle(channelRequestId, { kind: "cancelled", reason });
|
|
3357
|
-
}
|
|
3358
|
-
disposeAll(reason) {
|
|
3359
|
-
for (const id of [...this.entries.keys()]) {
|
|
3360
|
-
this.cancel(id, reason, void 0);
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
count() {
|
|
3364
|
-
return this.entries.size;
|
|
3365
|
-
}
|
|
3366
|
-
};
|
|
3367
|
-
function collisionMessage(channelRequestId, reason, newKind) {
|
|
3368
|
-
if (reason === "owner") {
|
|
3369
|
-
return `channel_request_owner_mismatch: ${channelRequestId} owned by a different runtime`;
|
|
3370
|
-
}
|
|
3371
|
-
if (reason === "kind") {
|
|
3372
|
-
const otherKind = newKind === "permission" ? "question" : "permission";
|
|
3373
|
-
return `channel_request_id_collision: ${channelRequestId} is bound to a ${otherKind} relay`;
|
|
3374
|
-
}
|
|
3375
|
-
return `channel_request_id_collision: ${channelRequestId} payload mismatch`;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// src/gateway/relay/coordinator.ts
|
|
3379
|
-
var DEFAULT_RELAY_TTL_MS = 5 * 6e4;
|
|
3380
|
-
var RelayCoordinator = class {
|
|
3381
|
-
adapters;
|
|
3382
|
-
defaultTtlMs;
|
|
3383
|
-
idFactory;
|
|
3384
|
-
log;
|
|
3385
|
-
registry = new PendingRegistry();
|
|
3386
|
-
constructor(opts) {
|
|
3387
|
-
this.adapters = opts.adapters;
|
|
3388
|
-
this.defaultTtlMs = opts.defaultTtlMs ?? DEFAULT_RELAY_TTL_MS;
|
|
3389
|
-
this.idFactory = opts.idFactory ?? generateChannelRequestId;
|
|
3390
|
-
this.log = opts.log;
|
|
3391
|
-
}
|
|
3392
|
-
requestPermission(req) {
|
|
3393
|
-
const channelRequestId = req.channelRequestId ?? this.idFactory();
|
|
3394
|
-
const targets = this.adapters().filter(
|
|
3395
|
-
(a) => a.capabilities.relayPermission && typeof a.requestPermissionVerdict === "function"
|
|
3396
|
-
);
|
|
3397
|
-
if (targets.length === 0) {
|
|
3398
|
-
return { channelRequestId, result: Promise.resolve({ kind: "no_relay" }) };
|
|
3399
|
-
}
|
|
3400
|
-
const fingerprint = permissionFingerprint(req);
|
|
3401
|
-
const inspect = this.registry.inspect(
|
|
3402
|
-
channelRequestId,
|
|
3403
|
-
"permission",
|
|
3404
|
-
fingerprint,
|
|
3405
|
-
req.runtimeId
|
|
3406
|
-
);
|
|
3407
|
-
if (inspect.kind === "collision") {
|
|
3408
|
-
throw new Error(
|
|
3409
|
-
collisionMessage(channelRequestId, inspect.reason, "permission")
|
|
3410
|
-
);
|
|
3411
|
-
}
|
|
3412
|
-
if (inspect.kind === "attach") {
|
|
3413
|
-
return {
|
|
3414
|
-
channelRequestId,
|
|
3415
|
-
result: inspect.entry.result
|
|
3416
|
-
};
|
|
3417
|
-
}
|
|
3418
|
-
const fullReq = {
|
|
3419
|
-
channelRequestId,
|
|
3420
|
-
toolName: req.toolName,
|
|
3421
|
-
description: req.description,
|
|
3422
|
-
inputPreview: req.inputPreview
|
|
3423
|
-
};
|
|
3424
|
-
const result = this.broadcast({
|
|
3425
|
-
kind: "permission",
|
|
3426
|
-
channelRequestId,
|
|
3427
|
-
ttlMs: req.ttlMs === null ? null : req.ttlMs ?? this.defaultTtlMs,
|
|
3428
|
-
runtimeId: req.runtimeId,
|
|
3429
|
-
fingerprint,
|
|
3430
|
-
targets,
|
|
3431
|
-
perAdapter: async (adapter, signal) => {
|
|
3432
|
-
const res = await adapter.requestPermissionVerdict(fullReq, signal);
|
|
3433
|
-
return res.kind === "verdict" ? { ...res, channelId: adapter.id } : null;
|
|
3434
|
-
}
|
|
3435
|
-
});
|
|
3436
|
-
return { channelRequestId, result };
|
|
3437
|
-
}
|
|
3438
|
-
requestQuestion(req) {
|
|
3439
|
-
const channelRequestId = req.channelRequestId ?? this.idFactory();
|
|
3440
|
-
const targets = this.adapters().filter(
|
|
3441
|
-
(a) => a.capabilities.relayQuestion && typeof a.requestQuestionAnswer === "function"
|
|
3442
|
-
);
|
|
3443
|
-
if (targets.length === 0) {
|
|
3444
|
-
return { channelRequestId, result: Promise.resolve({ kind: "no_relay" }) };
|
|
3445
|
-
}
|
|
3446
|
-
const fingerprint = questionFingerprint(req);
|
|
3447
|
-
const inspect = this.registry.inspect(
|
|
3448
|
-
channelRequestId,
|
|
3449
|
-
"question",
|
|
3450
|
-
fingerprint,
|
|
3451
|
-
req.runtimeId
|
|
3452
|
-
);
|
|
3453
|
-
if (inspect.kind === "collision") {
|
|
3454
|
-
throw new Error(
|
|
3455
|
-
collisionMessage(channelRequestId, inspect.reason, "question")
|
|
3456
|
-
);
|
|
3457
|
-
}
|
|
3458
|
-
if (inspect.kind === "attach") {
|
|
3459
|
-
return {
|
|
3460
|
-
channelRequestId,
|
|
3461
|
-
result: inspect.entry.result
|
|
3462
|
-
};
|
|
3463
|
-
}
|
|
3464
|
-
const fullReq = {
|
|
3465
|
-
channelRequestId,
|
|
3466
|
-
title: req.title,
|
|
3467
|
-
questions: req.questions
|
|
3468
|
-
};
|
|
3469
|
-
const result = this.broadcast({
|
|
3470
|
-
kind: "question",
|
|
3471
|
-
channelRequestId,
|
|
3472
|
-
ttlMs: req.ttlMs === null ? null : req.ttlMs ?? this.defaultTtlMs,
|
|
3473
|
-
runtimeId: req.runtimeId,
|
|
3474
|
-
fingerprint,
|
|
3475
|
-
targets,
|
|
3476
|
-
perAdapter: async (adapter, signal) => {
|
|
3477
|
-
const res = await adapter.requestQuestionAnswer(fullReq, signal);
|
|
3478
|
-
return res.kind === "answer" ? { ...res, channelId: adapter.id } : null;
|
|
3479
|
-
}
|
|
3480
|
-
});
|
|
3481
|
-
return { channelRequestId, result };
|
|
3482
|
-
}
|
|
3483
|
-
cancel(channelRequestId, reason, expectedRuntimeId) {
|
|
3484
|
-
return this.registry.cancel(channelRequestId, reason, expectedRuntimeId);
|
|
3485
|
-
}
|
|
3486
|
-
pendingCount() {
|
|
3487
|
-
return this.registry.count();
|
|
3488
|
-
}
|
|
3489
|
-
disposeAll(reason = "auto_resolved") {
|
|
3490
|
-
this.registry.disposeAll(reason);
|
|
3491
|
-
}
|
|
3492
|
-
broadcast(args) {
|
|
3493
|
-
const {
|
|
3494
|
-
kind,
|
|
3495
|
-
channelRequestId,
|
|
3496
|
-
ttlMs,
|
|
3497
|
-
runtimeId,
|
|
3498
|
-
fingerprint,
|
|
3499
|
-
targets,
|
|
3500
|
-
perAdapter
|
|
3501
|
-
} = args;
|
|
3502
|
-
const controllers = targets.map(() => new AbortController());
|
|
3503
|
-
let resolveFn;
|
|
3504
|
-
const result = new Promise((resolve) => {
|
|
3505
|
-
resolveFn = resolve;
|
|
3506
|
-
});
|
|
3507
|
-
const timer = ttlMs === null ? void 0 : setTimeout(() => {
|
|
3508
|
-
this.registry.settle(channelRequestId, {
|
|
3509
|
-
kind: "cancelled",
|
|
3510
|
-
reason: "timeout"
|
|
3511
|
-
});
|
|
3512
|
-
}, ttlMs);
|
|
3513
|
-
if (timer && typeof timer.unref === "function") timer.unref();
|
|
3514
|
-
this.registry.register({
|
|
3515
|
-
kind,
|
|
3516
|
-
channelRequestId,
|
|
3517
|
-
fingerprint,
|
|
3518
|
-
runtimeId,
|
|
3519
|
-
controllers,
|
|
3520
|
-
timer,
|
|
3521
|
-
resolve: resolveFn,
|
|
3522
|
-
result,
|
|
3523
|
-
settled: false
|
|
3524
|
-
});
|
|
3525
|
-
targets.forEach((adapter, idx) => {
|
|
3526
|
-
const ctrl = controllers[idx];
|
|
3527
|
-
Promise.resolve().then(() => perAdapter(adapter, ctrl.signal)).then((res) => {
|
|
3528
|
-
if (res !== null) {
|
|
3529
|
-
this.registry.settle(channelRequestId, res);
|
|
3530
|
-
}
|
|
3531
|
-
}).catch((err) => {
|
|
3532
|
-
this.log?.(
|
|
3533
|
-
"warn",
|
|
3534
|
-
`adapter ${adapter.id} ${kind} relay failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3535
|
-
);
|
|
3536
|
-
});
|
|
3537
|
-
});
|
|
3538
|
-
return result;
|
|
3539
|
-
}
|
|
3540
|
-
};
|
|
3541
|
-
function permissionFingerprint(req) {
|
|
3542
|
-
return JSON.stringify([req.toolName, req.description, req.inputPreview]);
|
|
3543
|
-
}
|
|
3544
|
-
function questionFingerprint(req) {
|
|
3545
|
-
return JSON.stringify([req.title, req.questions]);
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
// src/gateway/state/db.ts
|
|
3549
|
-
import fs3 from "fs";
|
|
3550
|
-
import path3 from "path";
|
|
3551
|
-
import Database from "better-sqlite3";
|
|
3552
|
-
var GATEWAY_STATE_VERSION = 1;
|
|
3553
|
-
function openGatewayState(dbPath) {
|
|
3554
|
-
if (dbPath !== ":memory:") {
|
|
3555
|
-
fs3.mkdirSync(path3.dirname(dbPath), { recursive: true, mode: 448 });
|
|
3556
|
-
}
|
|
3557
|
-
const db = new Database(dbPath);
|
|
3558
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
3559
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
3560
|
-
initGatewayStateSchema(db);
|
|
3561
|
-
if (dbPath !== ":memory:" && process.platform !== "win32") {
|
|
3562
|
-
try {
|
|
3563
|
-
fs3.chmodSync(dbPath, 384);
|
|
3564
|
-
} catch {
|
|
3565
|
-
}
|
|
3566
|
-
}
|
|
3567
|
-
return db;
|
|
3568
|
-
}
|
|
3569
|
-
function initGatewayStateSchema(db) {
|
|
3570
|
-
db.exec(`
|
|
3571
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
3572
|
-
version INTEGER NOT NULL
|
|
3573
|
-
);
|
|
3574
|
-
|
|
3575
|
-
-- Inbound chat messages parked while no runtime is registered. Drained
|
|
3576
|
-
-- in FIFO id order on session.register. Idempotency key prevents the
|
|
3577
|
-
-- same provider message from being parked twice if an adapter retries.
|
|
3578
|
-
CREATE TABLE IF NOT EXISTS inbound_queue (
|
|
3579
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3580
|
-
channel_id TEXT NOT NULL,
|
|
3581
|
-
account_id TEXT NOT NULL,
|
|
3582
|
-
idempotency_key TEXT NOT NULL,
|
|
3583
|
-
payload_json TEXT NOT NULL,
|
|
3584
|
-
created_at INTEGER NOT NULL
|
|
3585
|
-
);
|
|
3586
|
-
CREATE UNIQUE INDEX IF NOT EXISTS ix_inbound_queue_idem
|
|
3587
|
-
ON inbound_queue(channel_id, account_id, idempotency_key);
|
|
3588
|
-
|
|
3589
|
-
-- Outbound messages whose adapter send() failed transiently. Drained
|
|
3590
|
-
-- by a periodic retry loop with exponential backoff. Idempotency key
|
|
3591
|
-
-- on the OutboundMessage prevents double-delivery if the adapter
|
|
3592
|
-
-- partially succeeded before throwing.
|
|
3593
|
-
CREATE TABLE IF NOT EXISTS channel_outbox (
|
|
3594
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3595
|
-
channel_id TEXT NOT NULL,
|
|
3596
|
-
payload_json TEXT NOT NULL,
|
|
3597
|
-
attempt INTEGER NOT NULL DEFAULT 0,
|
|
3598
|
-
next_attempt_at INTEGER NOT NULL,
|
|
3599
|
-
last_error TEXT,
|
|
3600
|
-
created_at INTEGER NOT NULL
|
|
3601
|
-
);
|
|
3602
|
-
CREATE INDEX IF NOT EXISTS ix_channel_outbox_due
|
|
3603
|
-
ON channel_outbox(next_attempt_at);
|
|
3604
|
-
`);
|
|
3605
|
-
const existing = db.prepare("SELECT version FROM schema_version").get();
|
|
3606
|
-
if (existing && existing.version > GATEWAY_STATE_VERSION) {
|
|
3607
|
-
throw new Error(
|
|
3608
|
-
`Gateway state DB has newer schema version ${existing.version} (expected <= ${GATEWAY_STATE_VERSION}). Update athena-cli.`
|
|
3609
|
-
);
|
|
3610
|
-
}
|
|
3611
|
-
if (!existing) {
|
|
3612
|
-
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(
|
|
3613
|
-
GATEWAY_STATE_VERSION
|
|
3614
|
-
);
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
// src/gateway/transport/tlsWs.ts
|
|
3619
|
-
import { WebSocketServer } from "ws";
|
|
3620
|
-
import { createServer as createHttpsServer } from "https";
|
|
3621
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
3622
|
-
function createWsServerTransport(opts) {
|
|
3623
|
-
if (!opts.allowNonLoopback && !isLoopbackHost(opts.host)) {
|
|
3624
|
-
throw new Error(`gateway: refusing non-loopback bind without --insecure`);
|
|
3625
|
-
}
|
|
3626
|
-
let endpoint = null;
|
|
3627
|
-
const scheme = opts.tls ? "wss" : "ws";
|
|
3628
|
-
const rateLimit = createConnectRateLimiter(opts.rateLimitPerMin ?? 10);
|
|
3629
|
-
return {
|
|
3630
|
-
kind: "ws",
|
|
3631
|
-
endpoint: () => {
|
|
3632
|
-
if (!endpoint) {
|
|
3633
|
-
throw new Error("gateway: WS transport has not started listening");
|
|
3634
|
-
}
|
|
3635
|
-
return endpoint;
|
|
3636
|
-
},
|
|
3637
|
-
listen: (onConnection) => new Promise((resolve, reject) => {
|
|
3638
|
-
const verifyClient = (info) => rateLimit.allow(info.req.socket.remoteAddress ?? "unknown");
|
|
3639
|
-
const { wss, httpsServer } = createWss({
|
|
3640
|
-
host: opts.host,
|
|
3641
|
-
port: opts.port,
|
|
3642
|
-
tls: opts.tls,
|
|
3643
|
-
verifyClient
|
|
3644
|
-
});
|
|
3645
|
-
const onError = (err) => reject(err);
|
|
3646
|
-
wss.once("error", onError);
|
|
3647
|
-
if (httpsServer) httpsServer.once("error", onError);
|
|
3648
|
-
const onListening = () => {
|
|
3649
|
-
wss.off("error", onError);
|
|
3650
|
-
if (httpsServer) httpsServer.off("error", onError);
|
|
3651
|
-
const addr = httpsServer ? httpsServer.address() : wss.address();
|
|
3652
|
-
if (typeof addr === "string" || addr === null) {
|
|
3653
|
-
wss.close();
|
|
3654
|
-
httpsServer?.close();
|
|
3655
|
-
reject(
|
|
3656
|
-
new Error("gateway: WS listener did not expose TCP address")
|
|
3657
|
-
);
|
|
3658
|
-
return;
|
|
3659
|
-
}
|
|
3660
|
-
endpoint = {
|
|
3661
|
-
host: opts.host,
|
|
3662
|
-
port: addr.port,
|
|
3663
|
-
url: `${scheme}://${opts.host}:${addr.port}`
|
|
3664
|
-
};
|
|
3665
|
-
resolve({
|
|
3666
|
-
close: () => new Promise((closeResolve) => {
|
|
3667
|
-
for (const client of wss.clients) client.terminate();
|
|
3668
|
-
wss.close(() => {
|
|
3669
|
-
if (httpsServer) httpsServer.close(() => closeResolve());
|
|
3670
|
-
else closeResolve();
|
|
3671
|
-
});
|
|
3672
|
-
})
|
|
3673
|
-
});
|
|
3674
|
-
};
|
|
3675
|
-
if (httpsServer) httpsServer.once("listening", onListening);
|
|
3676
|
-
else wss.once("listening", onListening);
|
|
3677
|
-
const pingIntervalMs = opts.pingIntervalMs ?? 15e3;
|
|
3678
|
-
const pongTimeoutMs = opts.pongTimeoutMs ?? 3e4;
|
|
3679
|
-
wss.on("connection", (ws) => {
|
|
3680
|
-
attachHeartbeat(ws, pingIntervalMs, pongTimeoutMs);
|
|
3681
|
-
onConnection(createWsConnection(ws, `${scheme}:${opts.host}`));
|
|
3682
|
-
});
|
|
3683
|
-
})
|
|
3684
|
-
};
|
|
3685
|
-
}
|
|
3686
|
-
function loadTlsOptions(tls) {
|
|
3687
|
-
return {
|
|
3688
|
-
cert: readFileSync3(tls.certPath),
|
|
3689
|
-
key: readFileSync3(tls.keyPath)
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
function createWss(input) {
|
|
3693
|
-
if (input.tls) {
|
|
3694
|
-
const httpsServer = createHttpsServer(loadTlsOptions(input.tls));
|
|
3695
|
-
httpsServer.listen({ host: input.host, port: input.port });
|
|
3696
|
-
return {
|
|
3697
|
-
wss: new WebSocketServer({
|
|
3698
|
-
server: httpsServer,
|
|
3699
|
-
verifyClient: input.verifyClient
|
|
3700
|
-
}),
|
|
3701
|
-
httpsServer
|
|
3702
|
-
};
|
|
3703
|
-
}
|
|
3704
|
-
return {
|
|
3705
|
-
wss: new WebSocketServer({
|
|
3706
|
-
host: input.host,
|
|
3707
|
-
port: input.port,
|
|
3708
|
-
verifyClient: input.verifyClient
|
|
3709
|
-
}),
|
|
3710
|
-
httpsServer: null
|
|
3711
|
-
};
|
|
3712
|
-
}
|
|
3713
|
-
function createConnectRateLimiter(maxPerMin) {
|
|
3714
|
-
if (maxPerMin <= 0) return { allow: () => true };
|
|
3715
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
3716
|
-
let pruneCountdown = 256;
|
|
3717
|
-
const prune = (cutoff) => {
|
|
3718
|
-
for (const [ip, arr] of buckets) {
|
|
3719
|
-
const fresh = arr.filter((t) => t > cutoff);
|
|
3720
|
-
if (fresh.length === 0) buckets.delete(ip);
|
|
3721
|
-
else if (fresh.length !== arr.length) buckets.set(ip, fresh);
|
|
3722
|
-
}
|
|
3723
|
-
};
|
|
3724
|
-
return {
|
|
3725
|
-
allow(ip) {
|
|
3726
|
-
const now = Date.now();
|
|
3727
|
-
const cutoff = now - 6e4;
|
|
3728
|
-
pruneCountdown -= 1;
|
|
3729
|
-
if (pruneCountdown <= 0) {
|
|
3730
|
-
prune(cutoff);
|
|
3731
|
-
pruneCountdown = 256;
|
|
3732
|
-
}
|
|
3733
|
-
const recent = (buckets.get(ip) ?? []).filter((t) => t > cutoff);
|
|
3734
|
-
if (recent.length >= maxPerMin) {
|
|
3735
|
-
buckets.set(ip, recent);
|
|
3736
|
-
return false;
|
|
3737
|
-
}
|
|
3738
|
-
recent.push(now);
|
|
3739
|
-
buckets.set(ip, recent);
|
|
3740
|
-
return true;
|
|
3741
|
-
}
|
|
3742
|
-
};
|
|
3743
|
-
}
|
|
3744
|
-
function attachHeartbeat(ws, pingIntervalMs, pongTimeoutMs) {
|
|
3745
|
-
if (pingIntervalMs <= 0) return;
|
|
3746
|
-
let pongTimer = null;
|
|
3747
|
-
const clearPongTimer = () => {
|
|
3748
|
-
if (pongTimer) {
|
|
3749
|
-
clearTimeout(pongTimer);
|
|
3750
|
-
pongTimer = null;
|
|
3751
|
-
}
|
|
3752
|
-
};
|
|
3753
|
-
ws.on("pong", clearPongTimer);
|
|
3754
|
-
const interval = setInterval(() => {
|
|
3755
|
-
if (ws.readyState !== ws.OPEN) return;
|
|
3756
|
-
try {
|
|
3757
|
-
ws.ping();
|
|
3758
|
-
} catch {
|
|
3759
|
-
return;
|
|
3760
|
-
}
|
|
3761
|
-
if (!pongTimer) {
|
|
3762
|
-
pongTimer = setTimeout(() => ws.terminate(), pongTimeoutMs);
|
|
3763
|
-
}
|
|
3764
|
-
}, pingIntervalMs);
|
|
3765
|
-
const stop = () => {
|
|
3766
|
-
clearInterval(interval);
|
|
3767
|
-
clearPongTimer();
|
|
3768
|
-
};
|
|
3769
|
-
ws.on("close", stop);
|
|
3770
|
-
ws.on("error", stop);
|
|
3771
|
-
}
|
|
3772
|
-
function createWsConnection(ws, peer) {
|
|
3773
|
-
const frameHandlers = /* @__PURE__ */ new Set();
|
|
3774
|
-
const closeHandlers = /* @__PURE__ */ new Set();
|
|
3775
|
-
const errorHandlers = /* @__PURE__ */ new Set();
|
|
3776
|
-
ws.on("message", (data) => {
|
|
3777
|
-
let parsed;
|
|
3778
|
-
try {
|
|
3779
|
-
parsed = JSON.parse(data.toString());
|
|
3780
|
-
} catch {
|
|
3781
|
-
ws.close();
|
|
3782
|
-
return;
|
|
3783
|
-
}
|
|
3784
|
-
traceGatewayFrame("ws", peer, "in", parsed);
|
|
3785
|
-
for (const handler of frameHandlers) handler(parsed);
|
|
3786
|
-
});
|
|
3787
|
-
ws.on("error", (err) => {
|
|
3788
|
-
for (const handler of errorHandlers) handler(err);
|
|
3789
|
-
});
|
|
3790
|
-
ws.on("close", () => {
|
|
3791
|
-
for (const handler of closeHandlers) handler();
|
|
3792
|
-
});
|
|
3793
|
-
return {
|
|
3794
|
-
kind: "ws",
|
|
3795
|
-
peer,
|
|
3796
|
-
send: (frame) => {
|
|
3797
|
-
if (ws.readyState !== ws.OPEN) return;
|
|
3798
|
-
traceGatewayFrame("ws", peer, "out", frame);
|
|
3799
|
-
ws.send(JSON.stringify(frame));
|
|
3800
|
-
},
|
|
3801
|
-
close: () => ws.close(),
|
|
3802
|
-
onFrame: (cb) => {
|
|
3803
|
-
frameHandlers.add(cb);
|
|
3804
|
-
return () => frameHandlers.delete(cb);
|
|
3805
|
-
},
|
|
3806
|
-
onClose: (cb) => {
|
|
3807
|
-
closeHandlers.add(cb);
|
|
3808
|
-
return () => closeHandlers.delete(cb);
|
|
3809
|
-
},
|
|
3810
|
-
onError: (cb) => {
|
|
3811
|
-
errorHandlers.add(cb);
|
|
3812
|
-
return () => errorHandlers.delete(cb);
|
|
3813
|
-
}
|
|
3814
|
-
};
|
|
3815
|
-
}
|
|
3816
|
-
|
|
3817
|
-
// src/gateway/daemon.ts
|
|
3818
|
-
function buildListenerStatus(spec, resolvedPort) {
|
|
3819
|
-
if (spec.kind === "uds") {
|
|
3820
|
-
return { kind: "uds", socketPath: spec.socketPath };
|
|
3821
|
-
}
|
|
3822
|
-
const port = resolvedPort ?? spec.port;
|
|
3823
|
-
const tls = Boolean(spec.tls);
|
|
3824
|
-
const scheme = tls ? "wss" : "ws";
|
|
3825
|
-
return {
|
|
3826
|
-
kind: "tcp",
|
|
3827
|
-
host: spec.host,
|
|
3828
|
-
port,
|
|
3829
|
-
url: `${scheme}://${spec.host}:${port}`,
|
|
3830
|
-
tls,
|
|
3831
|
-
insecure: spec.insecure,
|
|
3832
|
-
loopback: isLoopbackHost(spec.host)
|
|
3833
|
-
};
|
|
3834
|
-
}
|
|
3835
|
-
function pathIdFromSidecarPath(filePath) {
|
|
3836
|
-
const base = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
3837
|
-
return base.endsWith(".json") ? base.slice(0, -".json".length) : base;
|
|
3838
|
-
}
|
|
3839
|
-
async function startDaemon(opts) {
|
|
3840
|
-
const startedAt = Date.now();
|
|
3841
|
-
const pid = process.pid;
|
|
3842
|
-
const paths = opts.paths ?? resolveGatewayPaths(opts.env);
|
|
3843
|
-
fs4.mkdirSync(paths.runDir, { recursive: true, mode: 448 });
|
|
3844
|
-
fs4.mkdirSync(paths.configDir, { recursive: true, mode: 448 });
|
|
3845
|
-
if (process.platform !== "win32") {
|
|
3846
|
-
try {
|
|
3847
|
-
fs4.chmodSync(paths.runDir, 448);
|
|
3848
|
-
fs4.chmodSync(paths.configDir, 448);
|
|
3849
|
-
} catch {
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
const lock = acquireLock(paths.lockPath);
|
|
3853
|
-
const token = loadOrCreateToken(paths.tokenPath);
|
|
3854
|
-
const listenSpec = opts.listenSpec ?? resolveListenSpec({ paths });
|
|
3855
|
-
requireTokenForBind(listenSpec, token);
|
|
3856
|
-
const listenerHints = {
|
|
3857
|
-
transport: listenSpec.kind === "tcp" ? "ws" : "uds",
|
|
3858
|
-
tls: listenSpec.kind === "tcp" && Boolean(listenSpec.tls),
|
|
3859
|
-
loopback: listenSpec.kind === "uds" || isLoopbackHost(listenSpec.host)
|
|
3860
|
-
};
|
|
3861
|
-
const stateDb = openGatewayState(paths.statePath);
|
|
3862
|
-
const channelManager = new ChannelManager();
|
|
3863
|
-
const relayCoordinator = new RelayCoordinator({
|
|
3864
|
-
adapters: () => channelManager.listAdapters()
|
|
3865
|
-
});
|
|
3866
|
-
const connectionOpenedAt = /* @__PURE__ */ new Map();
|
|
3867
|
-
const disconnectGracePeriodMs = opts.disconnectGracePeriodMs ?? 0;
|
|
3868
|
-
let listenerStatus = null;
|
|
3869
|
-
const log = (level, message) => {
|
|
3870
|
-
if (opts.silent) return;
|
|
3871
|
-
const stream = level === "error" || level === "warn" ? "stderr" : "stdout";
|
|
3872
|
-
process[stream].write(`athena-gateway: [${level}] ${message}
|
|
3873
|
-
`);
|
|
3874
|
-
};
|
|
3875
|
-
const pipeline = new DispatchPipeline({
|
|
3876
|
-
stateDb,
|
|
3877
|
-
send: (channelId, msg) => channelManager.send(channelId, msg),
|
|
3878
|
-
gracePeriodMs: disconnectGracePeriodMs,
|
|
3879
|
-
log,
|
|
3880
|
-
observers: {
|
|
3881
|
-
onRuntimeRebind: ({ gapMs, epoch }) => trackGatewayRuntimeRebind({ gapMs, epoch }),
|
|
3882
|
-
onRuntimeExpired: ({ gapMs }) => trackGatewayRuntimeExpired({ gapMs }),
|
|
3883
|
-
// Single-runtime v1: blanket dispose is safe. Multi-runtime must
|
|
3884
|
-
// scope to the disconnecting runtime via disposeAllForRuntime.
|
|
3885
|
-
onRuntimeConnectionLost: () => relayCoordinator.disposeAll("connection_lost")
|
|
3886
|
-
}
|
|
3887
|
-
});
|
|
3888
|
-
pipeline.start();
|
|
3889
|
-
channelManager.setInboundSink((inbound, ctx) => {
|
|
3890
|
-
pipeline.handleInbound(inbound, ctx);
|
|
3891
|
-
});
|
|
3892
|
-
const channelConfigHome = opts.env?.HOME;
|
|
3893
|
-
const reloadChannels = async () => {
|
|
3894
|
-
const results = [];
|
|
3895
|
-
const { sidecars, errors } = loadChannelSidecars(channelConfigHome);
|
|
3896
|
-
for (const err of errors) {
|
|
3897
|
-
const id = pathIdFromSidecarPath(err.path);
|
|
3898
|
-
results.push({
|
|
3899
|
-
id,
|
|
3900
|
-
ok: false,
|
|
3901
|
-
action: "failed",
|
|
3902
|
-
reason: err.reason
|
|
3903
|
-
});
|
|
3904
|
-
}
|
|
3905
|
-
const sidecarIds = new Set(sidecars.map((s) => s.instanceId));
|
|
3906
|
-
for (const channel of channelManager.listChannels()) {
|
|
3907
|
-
if (sidecarIds.has(channel.id)) continue;
|
|
3908
|
-
try {
|
|
3909
|
-
await channelManager.unregister(channel.id, "shutdown");
|
|
3910
|
-
results.push({
|
|
3911
|
-
id: channel.id,
|
|
3912
|
-
ok: true,
|
|
3913
|
-
action: "unregistered"
|
|
3914
|
-
});
|
|
3915
|
-
} catch (err) {
|
|
3916
|
-
results.push({
|
|
3917
|
-
id: channel.id,
|
|
3918
|
-
ok: false,
|
|
3919
|
-
action: "failed",
|
|
3920
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
3921
|
-
});
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
for (const sidecar of sidecars) {
|
|
3925
|
-
const existed = channelManager.listChannels().some((channel) => channel.id === sidecar.instanceId);
|
|
3926
|
-
if (existed) {
|
|
3927
|
-
try {
|
|
3928
|
-
await channelManager.unregister(sidecar.instanceId, "shutdown");
|
|
3929
|
-
} catch (err) {
|
|
3930
|
-
results.push({
|
|
3931
|
-
id: sidecar.instanceId,
|
|
3932
|
-
ok: false,
|
|
3933
|
-
action: "failed",
|
|
3934
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
3935
|
-
});
|
|
3936
|
-
continue;
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
const built = instantiateAdapter(sidecar);
|
|
3940
|
-
if (!built.ok) {
|
|
3941
|
-
results.push({
|
|
3942
|
-
id: sidecar.instanceId,
|
|
3943
|
-
ok: false,
|
|
3944
|
-
action: "failed",
|
|
3945
|
-
reason: built.reason
|
|
3946
|
-
});
|
|
3947
|
-
continue;
|
|
3948
|
-
}
|
|
3949
|
-
try {
|
|
3950
|
-
await channelManager.register(
|
|
3951
|
-
built.adapter,
|
|
3952
|
-
sidecar.attachmentId !== void 0 ? { attachmentId: sidecar.attachmentId } : {}
|
|
3953
|
-
);
|
|
3954
|
-
results.push({
|
|
3955
|
-
id: sidecar.instanceId,
|
|
3956
|
-
ok: true,
|
|
3957
|
-
action: existed ? "replaced" : "registered"
|
|
3958
|
-
});
|
|
3959
|
-
if (!opts.silent) {
|
|
3960
|
-
process.stdout.write(
|
|
3961
|
-
`athena-gateway: registered ${sidecar.instanceId}
|
|
3962
|
-
`
|
|
3963
|
-
);
|
|
3964
|
-
}
|
|
3965
|
-
} catch (err) {
|
|
3966
|
-
results.push({
|
|
3967
|
-
id: sidecar.instanceId,
|
|
3968
|
-
ok: false,
|
|
3969
|
-
action: "failed",
|
|
3970
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
3971
|
-
});
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
return { results };
|
|
3975
|
-
};
|
|
3976
|
-
if (!opts.skipChannelLoad) {
|
|
3977
|
-
const { sidecars, errors } = loadChannelSidecars(channelConfigHome);
|
|
3978
|
-
for (const err of errors) {
|
|
3979
|
-
process.stderr.write(
|
|
3980
|
-
`athena-gateway: skipping ${err.path}: ${err.reason}
|
|
3981
|
-
`
|
|
3982
|
-
);
|
|
3983
|
-
}
|
|
3984
|
-
for (const sidecar of sidecars) {
|
|
3985
|
-
const built = instantiateAdapter(sidecar);
|
|
3986
|
-
if (!built.ok) {
|
|
3987
|
-
process.stderr.write(
|
|
3988
|
-
`athena-gateway: ${sidecar.instanceId}: ${built.reason}
|
|
3989
|
-
`
|
|
3990
|
-
);
|
|
3991
|
-
continue;
|
|
3992
|
-
}
|
|
3993
|
-
try {
|
|
3994
|
-
await channelManager.register(
|
|
3995
|
-
built.adapter,
|
|
3996
|
-
sidecar.attachmentId !== void 0 ? { attachmentId: sidecar.attachmentId } : {}
|
|
3997
|
-
);
|
|
3998
|
-
if (!opts.silent) {
|
|
3999
|
-
process.stdout.write(
|
|
4000
|
-
`athena-gateway: registered ${sidecar.instanceId}
|
|
4001
|
-
`
|
|
4002
|
-
);
|
|
4003
|
-
}
|
|
4004
|
-
} catch (err) {
|
|
4005
|
-
process.stderr.write(
|
|
4006
|
-
`athena-gateway: register ${sidecar.instanceId} failed: ${err instanceof Error ? err.message : String(err)}
|
|
4007
|
-
`
|
|
4008
|
-
);
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
4012
|
-
const handler = createDispatcher({
|
|
4013
|
-
startedAt,
|
|
4014
|
-
pipeline,
|
|
4015
|
-
channelManager,
|
|
4016
|
-
relayCoordinator,
|
|
4017
|
-
getListener: () => listenerStatus ?? buildListenerStatus(listenSpec, null),
|
|
4018
|
-
reloadChannels
|
|
4019
|
-
});
|
|
4020
|
-
let server;
|
|
4021
|
-
let listener;
|
|
4022
|
-
try {
|
|
4023
|
-
const transport = listenSpec.kind === "tcp" ? createWsServerTransport({
|
|
4024
|
-
host: listenSpec.host,
|
|
4025
|
-
port: listenSpec.port,
|
|
4026
|
-
allowNonLoopback: listenSpec.insecure || Boolean(listenSpec.tls),
|
|
4027
|
-
...listenSpec.tls ? { tls: listenSpec.tls } : {}
|
|
4028
|
-
}) : void 0;
|
|
4029
|
-
server = await startControlServer({
|
|
4030
|
-
socketPath: paths.socketPath,
|
|
4031
|
-
token,
|
|
4032
|
-
startedAt,
|
|
4033
|
-
handler,
|
|
4034
|
-
...transport !== void 0 ? { transport } : {},
|
|
4035
|
-
onConnect: (ctx) => {
|
|
4036
|
-
connectionOpenedAt.set(ctx.connectionId, Date.now());
|
|
4037
|
-
trackGatewayTransportConnect({
|
|
4038
|
-
transport: listenerHints.transport,
|
|
4039
|
-
tls: listenerHints.tls,
|
|
4040
|
-
loopback: listenerHints.loopback
|
|
4041
|
-
});
|
|
4042
|
-
},
|
|
4043
|
-
onDisconnect: (ctx) => {
|
|
4044
|
-
const openedAt = connectionOpenedAt.get(ctx.connectionId);
|
|
4045
|
-
connectionOpenedAt.delete(ctx.connectionId);
|
|
4046
|
-
const durationMs = openedAt !== void 0 ? Date.now() - openedAt : 0;
|
|
4047
|
-
trackGatewayTransportDisconnect({
|
|
4048
|
-
transport: listenerHints.transport,
|
|
4049
|
-
reason: "closed",
|
|
4050
|
-
durationMs
|
|
4051
|
-
});
|
|
4052
|
-
pipeline.notifyConnectionClosed(ctx.connectionId);
|
|
4053
|
-
}
|
|
4054
|
-
});
|
|
4055
|
-
if (listenSpec.kind === "tcp") {
|
|
4056
|
-
const endpoint = transport.endpoint();
|
|
4057
|
-
listener = {
|
|
4058
|
-
kind: "tcp",
|
|
4059
|
-
host: endpoint.host,
|
|
4060
|
-
port: endpoint.port,
|
|
4061
|
-
url: endpoint.url
|
|
4062
|
-
};
|
|
4063
|
-
listenerStatus = buildListenerStatus(listenSpec, endpoint.port);
|
|
4064
|
-
} else {
|
|
4065
|
-
listener = { kind: "uds", socketPath: listenSpec.socketPath };
|
|
4066
|
-
listenerStatus = buildListenerStatus(listenSpec, null);
|
|
4067
|
-
}
|
|
4068
|
-
} catch (err) {
|
|
4069
|
-
lock.release();
|
|
4070
|
-
throw err;
|
|
4071
|
-
}
|
|
4072
|
-
if (!opts.silent) {
|
|
4073
|
-
const target = listener.kind === "tcp" ? listener.url : `socket=${paths.socketPath}`;
|
|
4074
|
-
process.stdout.write(`athena-gateway: ok pid=${pid} ${target}
|
|
4075
|
-
`);
|
|
4076
|
-
}
|
|
4077
|
-
if (listenSpec.kind === "tcp" && listenSpec.insecure && !listenSpec.tls && !isLoopbackHost(listenSpec.host)) {
|
|
4078
|
-
process.stderr.write(
|
|
4079
|
-
`athena-gateway: WARNING --insecure is set on a non-loopback bind (${listenSpec.host}:${listenSpec.port}); token travels in plaintext. Use only behind TLS-terminating reverse proxy or Tailscale/WireGuard tunnel.
|
|
4080
|
-
`
|
|
4081
|
-
);
|
|
4082
|
-
}
|
|
4083
|
-
let stopping = false;
|
|
4084
|
-
const stop = async () => {
|
|
4085
|
-
if (stopping) return;
|
|
4086
|
-
stopping = true;
|
|
4087
|
-
try {
|
|
4088
|
-
await pipeline.stop();
|
|
4089
|
-
relayCoordinator.disposeAll("auto_resolved");
|
|
4090
|
-
await channelManager.stop();
|
|
4091
|
-
await server.close();
|
|
4092
|
-
} finally {
|
|
4093
|
-
try {
|
|
4094
|
-
stateDb.close();
|
|
4095
|
-
} catch {
|
|
4096
|
-
}
|
|
4097
|
-
lock.release();
|
|
4098
|
-
}
|
|
4099
|
-
};
|
|
4100
|
-
if (!opts.skipSignalHandlers) {
|
|
4101
|
-
const onSignal = (signal) => {
|
|
4102
|
-
process.stderr.write(`athena-gateway: received ${signal}, stopping
|
|
4103
|
-
`);
|
|
4104
|
-
void stop().then(() => process.exit(0));
|
|
4105
|
-
};
|
|
4106
|
-
process.once("SIGINT", onSignal);
|
|
4107
|
-
process.once("SIGTERM", onSignal);
|
|
4108
|
-
}
|
|
4109
|
-
return {
|
|
4110
|
-
startedAt,
|
|
4111
|
-
pid,
|
|
4112
|
-
paths,
|
|
4113
|
-
pipeline,
|
|
4114
|
-
channelManager,
|
|
4115
|
-
relayCoordinator,
|
|
4116
|
-
listener,
|
|
4117
|
-
stop
|
|
4118
|
-
};
|
|
4119
|
-
}
|
|
4120
|
-
|
|
4121
|
-
export {
|
|
4122
|
-
startDaemon
|
|
4123
|
-
};
|
|
4124
|
-
//# sourceMappingURL=chunk-OB4HZXR5.js.map
|