@drisp/cli 0.4.2 → 0.4.5
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-NDWLVIFI.js → WorkflowInstallWizard-2MC5A7W4.js} +2 -2
- package/dist/athena-gateway.js +1045 -970
- package/dist/{chunk-GE6PPB6Z.js → chunk-5VK2ZMVV.js} +96 -119
- package/dist/{chunk-6TJHAUNB.js → chunk-M44KEGM7.js} +15 -3
- package/dist/{chunk-WHELLVBL.js → chunk-PJUDHH4R.js} +1498 -834
- package/dist/cli.js +1205 -1153
- package/dist/dashboard-daemon.js +2 -2
- package/package.json +4 -2
package/dist/athena-gateway.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
loadOrCreateToken,
|
|
5
5
|
requireTokenForBind,
|
|
6
6
|
timingSafeTokenEqual
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-M44KEGM7.js";
|
|
8
8
|
import {
|
|
9
9
|
CHANNEL_REQUEST_ID_REGEX,
|
|
10
10
|
createUdsServerTransport,
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
} from "./chunk-4CRZXLIP.js";
|
|
24
24
|
|
|
25
25
|
// src/gateway/daemon.ts
|
|
26
|
-
import crypto from "crypto";
|
|
27
26
|
import fs3 from "fs";
|
|
28
27
|
|
|
29
28
|
// src/gateway/adapters/console/adapter.ts
|
|
@@ -35,6 +34,8 @@ import { WebSocket } from "ws";
|
|
|
35
34
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
36
35
|
var DEFAULT_INITIAL_RECONNECT_MS = 1e3;
|
|
37
36
|
var DEFAULT_MAX_RECONNECT_MS = 3e4;
|
|
37
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
38
|
+
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 9e4;
|
|
38
39
|
function createConsoleBrokerClient(opts) {
|
|
39
40
|
const hasStatic = typeof opts.pairingToken === "string" && opts.pairingToken.length > 0;
|
|
40
41
|
const hasProvider = typeof opts.pairingTokenProvider === "function";
|
|
@@ -46,6 +47,8 @@ function createConsoleBrokerClient(opts) {
|
|
|
46
47
|
const provider = hasStatic ? async () => opts.pairingToken : opts.pairingTokenProvider;
|
|
47
48
|
const initialDelay = opts.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_MS;
|
|
48
49
|
const maxDelay = opts.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_MS;
|
|
50
|
+
const heartbeatInterval = opts.heartbeat?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
51
|
+
const heartbeatTimeout = opts.heartbeat?.timeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
49
52
|
let ws = null;
|
|
50
53
|
let ready = null;
|
|
51
54
|
let closeRequested = false;
|
|
@@ -53,6 +56,8 @@ function createConsoleBrokerClient(opts) {
|
|
|
53
56
|
let reconnectTimer = null;
|
|
54
57
|
let lastHello = null;
|
|
55
58
|
let currentToken = null;
|
|
59
|
+
let heartbeatTimer = null;
|
|
60
|
+
let lastPongAt = null;
|
|
56
61
|
const frameHandlers = /* @__PURE__ */ new Set();
|
|
57
62
|
const closeHandlers = /* @__PURE__ */ new Set();
|
|
58
63
|
const readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -61,6 +66,44 @@ function createConsoleBrokerClient(opts) {
|
|
|
61
66
|
if (!currentToken) return message;
|
|
62
67
|
return message.split(currentToken).join(tokenRedacted);
|
|
63
68
|
}
|
|
69
|
+
function stopHeartbeat() {
|
|
70
|
+
if (heartbeatTimer) {
|
|
71
|
+
clearInterval(heartbeatTimer);
|
|
72
|
+
heartbeatTimer = null;
|
|
73
|
+
}
|
|
74
|
+
lastPongAt = null;
|
|
75
|
+
}
|
|
76
|
+
function startHeartbeat(currentWs) {
|
|
77
|
+
stopHeartbeat();
|
|
78
|
+
lastPongAt = Date.now();
|
|
79
|
+
heartbeatTimer = setInterval(() => {
|
|
80
|
+
if (currentWs !== ws || currentWs.readyState !== WebSocket.OPEN) {
|
|
81
|
+
stopHeartbeat();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (lastPongAt !== null && Date.now() - lastPongAt >= heartbeatTimeout) {
|
|
85
|
+
opts.log(
|
|
86
|
+
"warn",
|
|
87
|
+
"console broker: heartbeat watchdog fired, no pong received \u2014 terminating"
|
|
88
|
+
);
|
|
89
|
+
stopHeartbeat();
|
|
90
|
+
try {
|
|
91
|
+
currentWs.terminate();
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const ping = {
|
|
97
|
+
kind: "console.ping",
|
|
98
|
+
frameId: makeFrameId(),
|
|
99
|
+
sentAt: Date.now()
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
currentWs.send(JSON.stringify(ping));
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}, heartbeatInterval);
|
|
106
|
+
}
|
|
64
107
|
function emitClose(reason) {
|
|
65
108
|
for (const h of [...closeHandlers]) {
|
|
66
109
|
try {
|
|
@@ -216,6 +259,10 @@ function createConsoleBrokerClient(opts) {
|
|
|
216
259
|
);
|
|
217
260
|
return;
|
|
218
261
|
}
|
|
262
|
+
if (parsed.kind === "console.pong") {
|
|
263
|
+
lastPongAt = Date.now();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
219
266
|
for (const h of [...frameHandlers]) {
|
|
220
267
|
try {
|
|
221
268
|
h(parsed);
|
|
@@ -240,12 +287,14 @@ function createConsoleBrokerClient(opts) {
|
|
|
240
287
|
throw err;
|
|
241
288
|
}
|
|
242
289
|
next.on("close", (_code, reasonBuf) => {
|
|
290
|
+
stopHeartbeat();
|
|
243
291
|
if (next !== ws) return;
|
|
244
292
|
ws = null;
|
|
245
293
|
ready = null;
|
|
246
294
|
emitClose(reasonBuf.toString() || "closed");
|
|
247
295
|
if (!closeRequested) scheduleReconnect();
|
|
248
296
|
});
|
|
297
|
+
startHeartbeat(next);
|
|
249
298
|
}
|
|
250
299
|
async function connect(hello) {
|
|
251
300
|
if (ws) throw new Error("console broker client already connected");
|
|
@@ -255,6 +304,7 @@ function createConsoleBrokerClient(opts) {
|
|
|
255
304
|
}
|
|
256
305
|
function close(reason) {
|
|
257
306
|
closeRequested = true;
|
|
307
|
+
stopHeartbeat();
|
|
258
308
|
if (reconnectTimer) {
|
|
259
309
|
clearTimeout(reconnectTimer);
|
|
260
310
|
reconnectTimer = null;
|
|
@@ -303,11 +353,11 @@ function makeFrameId() {
|
|
|
303
353
|
}
|
|
304
354
|
|
|
305
355
|
// src/gateway/adapters/console/adapter.ts
|
|
306
|
-
var
|
|
356
|
+
var CONSOLE_DEFAULT_ID = "console";
|
|
307
357
|
var CLIENT_NAME = "athena-cli";
|
|
308
358
|
var CLIENT_VERSION = "0.0.0";
|
|
309
359
|
var ConsoleAdapter = class {
|
|
310
|
-
id
|
|
360
|
+
id;
|
|
311
361
|
capabilities = {
|
|
312
362
|
chat: true,
|
|
313
363
|
threads: true,
|
|
@@ -319,8 +369,9 @@ var ConsoleAdapter = class {
|
|
|
319
369
|
ctx = null;
|
|
320
370
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
321
371
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
322
|
-
constructor(opts) {
|
|
372
|
+
constructor(opts, id = CONSOLE_DEFAULT_ID) {
|
|
323
373
|
this.opts = opts;
|
|
374
|
+
this.id = id;
|
|
324
375
|
}
|
|
325
376
|
async start(ctx) {
|
|
326
377
|
if (this.client) {
|
|
@@ -452,7 +503,7 @@ var ConsoleAdapter = class {
|
|
|
452
503
|
if (!entry) return;
|
|
453
504
|
this.pendingPermissions.delete(channelRequestId);
|
|
454
505
|
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
455
|
-
entry.resolve({ kind: "verdict", behavior: decision, channelId:
|
|
506
|
+
entry.resolve({ kind: "verdict", behavior: decision, channelId: this.id });
|
|
456
507
|
}
|
|
457
508
|
disposePermissions(reason = "auto_resolved") {
|
|
458
509
|
for (const [id, entry] of [...this.pendingPermissions.entries()]) {
|
|
@@ -521,7 +572,7 @@ var ConsoleAdapter = class {
|
|
|
521
572
|
entry.resolve({ kind: "cancelled", reason: "auto_resolved" });
|
|
522
573
|
return;
|
|
523
574
|
}
|
|
524
|
-
entry.resolve({ kind: "answer", answers: filtered, channelId:
|
|
575
|
+
entry.resolve({ kind: "answer", answers: filtered, channelId: this.id });
|
|
525
576
|
}
|
|
526
577
|
disposeQuestions(reason = "auto_resolved") {
|
|
527
578
|
for (const [id, entry] of [...this.pendingQuestions.entries()]) {
|
|
@@ -562,7 +613,7 @@ var ConsoleAdapter = class {
|
|
|
562
613
|
this.ctx?.log("warn", "console.message.in dropped: missing messageId");
|
|
563
614
|
return;
|
|
564
615
|
}
|
|
565
|
-
const inbound = normalizeInbound(frame, this.opts.runnerId);
|
|
616
|
+
const inbound = normalizeInbound(frame, this.opts.runnerId, this.id);
|
|
566
617
|
if (!inbound || !this.ctx) return;
|
|
567
618
|
try {
|
|
568
619
|
this.ctx.emitInbound(inbound);
|
|
@@ -673,13 +724,13 @@ function isValidConsoleAddress(value) {
|
|
|
673
724
|
}
|
|
674
725
|
return true;
|
|
675
726
|
}
|
|
676
|
-
function normalizeInbound(frame, runnerId) {
|
|
727
|
+
function normalizeInbound(frame, runnerId, channelId) {
|
|
677
728
|
if (typeof frame.text !== "string" || frame.text.length === 0) return null;
|
|
678
729
|
const userId = frame.address.userId ?? "console-user";
|
|
679
730
|
const idempotencyKey = typeof frame.idempotencyKey === "string" && frame.idempotencyKey.length > 0 ? frame.idempotencyKey : `console:${runnerId}:${frame.messageId}`;
|
|
680
731
|
return {
|
|
681
732
|
location: {
|
|
682
|
-
channelId
|
|
733
|
+
channelId,
|
|
683
734
|
accountId: frame.address.workspaceId ?? runnerId,
|
|
684
735
|
peer: { id: userId, kind: "user" },
|
|
685
736
|
...frame.address.threadId !== void 0 ? { thread: { id: frame.address.threadId } } : frame.address.conversationId !== void 0 ? { thread: { id: frame.address.conversationId } } : {}
|
|
@@ -760,8 +811,8 @@ var consoleModule = {
|
|
|
760
811
|
};
|
|
761
812
|
return { ok: true, config };
|
|
762
813
|
},
|
|
763
|
-
create(config) {
|
|
764
|
-
return new ConsoleAdapter(config);
|
|
814
|
+
create(config, instanceId) {
|
|
815
|
+
return new ConsoleAdapter(config, instanceId);
|
|
765
816
|
}
|
|
766
817
|
};
|
|
767
818
|
function isLoopbackUrl(url) {
|
|
@@ -1470,9 +1521,9 @@ function buildCancelText(reason) {
|
|
|
1470
1521
|
}
|
|
1471
1522
|
|
|
1472
1523
|
// src/gateway/adapters/telegram/adapter.ts
|
|
1473
|
-
var
|
|
1524
|
+
var TELEGRAM_DEFAULT_ID = "telegram";
|
|
1474
1525
|
var TelegramAdapter = class {
|
|
1475
|
-
id
|
|
1526
|
+
id;
|
|
1476
1527
|
capabilities = {
|
|
1477
1528
|
chat: true,
|
|
1478
1529
|
threads: true,
|
|
@@ -1488,8 +1539,9 @@ var TelegramAdapter = class {
|
|
|
1488
1539
|
lastInboundAt;
|
|
1489
1540
|
lastTransportOk = true;
|
|
1490
1541
|
ctx = null;
|
|
1491
|
-
constructor(opts) {
|
|
1542
|
+
constructor(opts, id = TELEGRAM_DEFAULT_ID) {
|
|
1492
1543
|
this.opts = opts;
|
|
1544
|
+
this.id = id;
|
|
1493
1545
|
this.relay = new TelegramRelay({
|
|
1494
1546
|
resolveTarget: () => {
|
|
1495
1547
|
if (this.opts.defaultChatId === void 0) return null;
|
|
@@ -1588,7 +1640,7 @@ var TelegramAdapter = class {
|
|
|
1588
1640
|
this.markHealth(true);
|
|
1589
1641
|
continue;
|
|
1590
1642
|
}
|
|
1591
|
-
const inbound = normalizeInbound2(update, allow);
|
|
1643
|
+
const inbound = normalizeInbound2(update, allow, this.id);
|
|
1592
1644
|
if (!inbound) continue;
|
|
1593
1645
|
this.lastInboundAt = inbound.receivedAt;
|
|
1594
1646
|
this.markHealth(true);
|
|
@@ -1626,7 +1678,7 @@ var TelegramAdapter = class {
|
|
|
1626
1678
|
}
|
|
1627
1679
|
}
|
|
1628
1680
|
};
|
|
1629
|
-
function normalizeInbound2(update, allow) {
|
|
1681
|
+
function normalizeInbound2(update, allow, channelId) {
|
|
1630
1682
|
const message = update.message ?? update.edited_message;
|
|
1631
1683
|
if (!message) return null;
|
|
1632
1684
|
const text = message.text;
|
|
@@ -1641,7 +1693,7 @@ function normalizeInbound2(update, allow) {
|
|
|
1641
1693
|
const threadId = typeof message.message_thread_id === "number" ? String(message.message_thread_id) : void 0;
|
|
1642
1694
|
return {
|
|
1643
1695
|
location: {
|
|
1644
|
-
channelId
|
|
1696
|
+
channelId,
|
|
1645
1697
|
accountId,
|
|
1646
1698
|
...isPrivate ? { peer: { id: chatId, kind: "user" } } : {
|
|
1647
1699
|
room: {
|
|
@@ -1696,8 +1748,8 @@ var telegramModule = {
|
|
|
1696
1748
|
};
|
|
1697
1749
|
return { ok: true, config };
|
|
1698
1750
|
},
|
|
1699
|
-
create(config) {
|
|
1700
|
-
return new TelegramAdapter(config);
|
|
1751
|
+
create(config, instanceId) {
|
|
1752
|
+
return new TelegramAdapter(config, instanceId);
|
|
1701
1753
|
}
|
|
1702
1754
|
};
|
|
1703
1755
|
|
|
@@ -1712,18 +1764,18 @@ function findAdapterModule(name) {
|
|
|
1712
1764
|
|
|
1713
1765
|
// src/gateway/adapters/factory.ts
|
|
1714
1766
|
function instantiateAdapter(sidecar) {
|
|
1715
|
-
const module = findAdapterModule(sidecar.
|
|
1767
|
+
const module = findAdapterModule(sidecar.kind);
|
|
1716
1768
|
if (!module) {
|
|
1717
|
-
return { ok: false, reason: `unknown channel: ${sidecar.
|
|
1769
|
+
return { ok: false, reason: `unknown channel kind: ${sidecar.kind}` };
|
|
1718
1770
|
}
|
|
1719
1771
|
const parsed = module.parseConfig({
|
|
1720
1772
|
options: sidecar.options,
|
|
1721
1773
|
allowedUserIds: sidecar.allowedUserIds
|
|
1722
1774
|
});
|
|
1723
1775
|
if (!parsed.ok) {
|
|
1724
|
-
return { ok: false, reason: `${sidecar.
|
|
1776
|
+
return { ok: false, reason: `${sidecar.instanceId}: ${parsed.reason}` };
|
|
1725
1777
|
}
|
|
1726
|
-
return { ok: true, adapter: module.create(parsed.config) };
|
|
1778
|
+
return { ok: true, adapter: module.create(parsed.config, sidecar.instanceId) };
|
|
1727
1779
|
}
|
|
1728
1780
|
|
|
1729
1781
|
// src/gateway/channelManager.ts
|
|
@@ -1873,8 +1925,7 @@ var ChannelManager = class {
|
|
|
1873
1925
|
// src/gateway/control/handlers.ts
|
|
1874
1926
|
import { createRequire } from "module";
|
|
1875
1927
|
|
|
1876
|
-
// src/gateway/
|
|
1877
|
-
import { randomUUID } from "crypto";
|
|
1928
|
+
// src/gateway/runtimeBindingStore.ts
|
|
1878
1929
|
var AlreadyRegisteredError = class extends Error {
|
|
1879
1930
|
code = "already_registered";
|
|
1880
1931
|
constructor(existing) {
|
|
@@ -1891,120 +1942,145 @@ var NotRegisteredError = class extends Error {
|
|
|
1891
1942
|
this.name = "NotRegisteredError";
|
|
1892
1943
|
}
|
|
1893
1944
|
};
|
|
1894
|
-
var UnknownDispatchError = class extends Error {
|
|
1895
|
-
code = "unknown_dispatch";
|
|
1896
|
-
constructor(id) {
|
|
1897
|
-
super(`unknown dispatchId: ${id}`);
|
|
1898
|
-
this.name = "UnknownDispatchError";
|
|
1899
|
-
}
|
|
1900
|
-
};
|
|
1901
1945
|
function maybeLastRebindAt(value) {
|
|
1902
1946
|
return value !== void 0 ? { lastRebindAt: value } : {};
|
|
1903
1947
|
}
|
|
1904
|
-
var
|
|
1948
|
+
var RuntimeBindingStore = class {
|
|
1905
1949
|
current = null;
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1950
|
+
bindingState = null;
|
|
1951
|
+
staleTimer = null;
|
|
1952
|
+
staleSince = null;
|
|
1953
|
+
gracePeriodMs;
|
|
1954
|
+
observers;
|
|
1909
1955
|
now;
|
|
1910
1956
|
constructor(opts = {}) {
|
|
1911
|
-
this.
|
|
1957
|
+
this.gracePeriodMs = opts.gracePeriodMs ?? 0;
|
|
1958
|
+
this.observers = opts.observers ?? {};
|
|
1912
1959
|
this.now = opts.now ?? Date.now;
|
|
1913
1960
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1961
|
+
// ── lifecycle ─────────────────────────────────────────────
|
|
1962
|
+
/** Register a runtime and bind its connection in one atomic step. */
|
|
1963
|
+
bind(input) {
|
|
1964
|
+
const previous = this.bindingState;
|
|
1965
|
+
const wasStale = previous?.state === "stale";
|
|
1966
|
+
const staleSince = wasStale ? previous.staleSince : null;
|
|
1967
|
+
if (!this.current) {
|
|
1968
|
+
this.current = {
|
|
1969
|
+
runtimeId: input.runtimeId,
|
|
1970
|
+
defaultAgentId: input.defaultAgentId,
|
|
1971
|
+
pid: input.pid,
|
|
1972
|
+
registeredAt: this.now()
|
|
1973
|
+
};
|
|
1974
|
+
} else if (this.current.runtimeId === input.runtimeId) {
|
|
1975
|
+
this.current = {
|
|
1976
|
+
...this.current,
|
|
1977
|
+
defaultAgentId: input.defaultAgentId,
|
|
1978
|
+
pid: input.pid
|
|
1979
|
+
};
|
|
1980
|
+
} else {
|
|
1924
1981
|
throw new AlreadyRegisteredError(this.current);
|
|
1925
1982
|
}
|
|
1926
|
-
this.current = { ...input, registeredAt: this.now() };
|
|
1927
|
-
return this.current;
|
|
1928
|
-
}
|
|
1929
|
-
bindConnection(runtimeId, connectionId) {
|
|
1930
|
-
if (!this.current || this.current.runtimeId !== runtimeId) {
|
|
1931
|
-
throw new NotRegisteredError();
|
|
1932
|
-
}
|
|
1933
|
-
const previous = this.binding;
|
|
1934
1983
|
const now = this.now();
|
|
1935
|
-
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== connectionId);
|
|
1984
|
+
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== input.connectionId);
|
|
1936
1985
|
const lastRebindAt = isRebind ? now : previous?.lastRebindAt;
|
|
1937
1986
|
const epoch = previous ? previous.epoch + (isRebind ? 1 : 0) : 1;
|
|
1938
|
-
this.
|
|
1987
|
+
this.bindingState = {
|
|
1939
1988
|
state: "active",
|
|
1940
|
-
connectionId,
|
|
1989
|
+
connectionId: input.connectionId,
|
|
1941
1990
|
boundAt: now,
|
|
1942
1991
|
epoch,
|
|
1943
1992
|
...maybeLastRebindAt(lastRebindAt)
|
|
1944
1993
|
};
|
|
1994
|
+
this.clearStaleTimer();
|
|
1995
|
+
if (wasStale && staleSince !== null) {
|
|
1996
|
+
this.observers.onRuntimeRebind?.({
|
|
1997
|
+
runtimeId: input.runtimeId,
|
|
1998
|
+
gapMs: now - staleSince,
|
|
1999
|
+
epoch: this.bindingState.epoch
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
return { registeredAt: this.current.registeredAt };
|
|
1945
2003
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
2004
|
+
/** Fully unregister a runtime. Throws NotRegisteredError if id does not match. */
|
|
2005
|
+
unbind(runtimeId) {
|
|
2006
|
+
if (!this.current || this.current.runtimeId !== runtimeId) {
|
|
2007
|
+
throw new NotRegisteredError();
|
|
2008
|
+
}
|
|
2009
|
+
this.current = null;
|
|
2010
|
+
this.bindingState = null;
|
|
2011
|
+
this.clearStaleTimer();
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Called when the transport connection closes.
|
|
2015
|
+
* Returns the runtimeId if the close was for the current binding (caller should
|
|
2016
|
+
* clear the push handle); returns null if the connectionId was not recognised.
|
|
2017
|
+
*/
|
|
2018
|
+
notifyConnectionClosed(connectionId) {
|
|
2019
|
+
if (!this.current || !this.bindingState || this.bindingState.connectionId !== connectionId) {
|
|
1948
2020
|
return null;
|
|
1949
2021
|
}
|
|
1950
|
-
|
|
2022
|
+
const runtimeId = this.current.runtimeId;
|
|
2023
|
+
const now = this.now();
|
|
2024
|
+
this.bindingState = {
|
|
1951
2025
|
state: "stale",
|
|
1952
2026
|
connectionId,
|
|
1953
|
-
staleSince:
|
|
1954
|
-
epoch: this.
|
|
1955
|
-
...maybeLastRebindAt(this.
|
|
2027
|
+
staleSince: now,
|
|
2028
|
+
epoch: this.bindingState.epoch,
|
|
2029
|
+
...maybeLastRebindAt(this.bindingState.lastRebindAt)
|
|
1956
2030
|
};
|
|
1957
|
-
|
|
2031
|
+
if (this.gracePeriodMs <= 0) {
|
|
2032
|
+
this.current = null;
|
|
2033
|
+
this.bindingState = null;
|
|
2034
|
+
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2035
|
+
return runtimeId;
|
|
2036
|
+
}
|
|
2037
|
+
this.staleSince = now;
|
|
2038
|
+
this.staleTimer = setTimeout(() => {
|
|
2039
|
+
this.expireStaleBinding(runtimeId);
|
|
2040
|
+
}, this.gracePeriodMs);
|
|
2041
|
+
return runtimeId;
|
|
1958
2042
|
}
|
|
2043
|
+
stop() {
|
|
2044
|
+
this.clearStaleTimer();
|
|
2045
|
+
}
|
|
2046
|
+
// ── reads ─────────────────────────────────────────────────
|
|
1959
2047
|
hasActiveBinding(runtimeId) {
|
|
1960
|
-
if (!this.current || !this.
|
|
2048
|
+
if (!this.current || !this.bindingState || this.bindingState.state !== "active") {
|
|
1961
2049
|
return false;
|
|
1962
2050
|
}
|
|
1963
2051
|
return runtimeId === void 0 || this.current.runtimeId === runtimeId;
|
|
1964
2052
|
}
|
|
2053
|
+
getCurrent() {
|
|
2054
|
+
return this.current;
|
|
2055
|
+
}
|
|
1965
2056
|
getBinding() {
|
|
1966
|
-
return this.
|
|
2057
|
+
return this.bindingState;
|
|
1967
2058
|
}
|
|
1968
2059
|
getRuntimeIdByConnection(connectionId) {
|
|
1969
|
-
if (!this.current || !this.
|
|
1970
|
-
return this.
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2060
|
+
if (!this.current || !this.bindingState) return null;
|
|
2061
|
+
return this.bindingState.connectionId === connectionId ? this.current.runtimeId : null;
|
|
2062
|
+
}
|
|
2063
|
+
// ── private ───────────────────────────────────────────────
|
|
2064
|
+
expireStaleBinding(runtimeId) {
|
|
2065
|
+
this.staleTimer = null;
|
|
2066
|
+
const since = this.staleSince;
|
|
2067
|
+
this.staleSince = null;
|
|
2068
|
+
if (!this.current || this.current.runtimeId !== runtimeId || this.hasActiveBinding(runtimeId)) {
|
|
2069
|
+
return;
|
|
1975
2070
|
}
|
|
1976
2071
|
this.current = null;
|
|
1977
|
-
this.
|
|
1978
|
-
this.
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
return this.current;
|
|
1982
|
-
}
|
|
1983
|
-
beginDispatch(input) {
|
|
1984
|
-
if (!this.current) {
|
|
1985
|
-
throw new NotRegisteredError();
|
|
2072
|
+
this.bindingState = null;
|
|
2073
|
+
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2074
|
+
if (since !== null) {
|
|
2075
|
+
this.observers.onRuntimeExpired?.({ runtimeId, gapMs: this.now() - since });
|
|
1986
2076
|
}
|
|
1987
|
-
const dispatchId = this.idFactory();
|
|
1988
|
-
const entry = {
|
|
1989
|
-
dispatchId,
|
|
1990
|
-
sessionKey: input.sessionKey,
|
|
1991
|
-
agentId: input.agentId,
|
|
1992
|
-
location: input.location,
|
|
1993
|
-
createdAt: this.now()
|
|
1994
|
-
};
|
|
1995
|
-
this.dispatches.set(dispatchId, entry);
|
|
1996
|
-
return entry;
|
|
1997
2077
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2078
|
+
clearStaleTimer() {
|
|
2079
|
+
if (this.staleTimer) {
|
|
2080
|
+
clearTimeout(this.staleTimer);
|
|
2081
|
+
this.staleTimer = null;
|
|
2002
2082
|
}
|
|
2003
|
-
this.
|
|
2004
|
-
return entry;
|
|
2005
|
-
}
|
|
2006
|
-
pendingDispatchCount() {
|
|
2007
|
-
return this.dispatches.size;
|
|
2083
|
+
this.staleSince = null;
|
|
2008
2084
|
}
|
|
2009
2085
|
};
|
|
2010
2086
|
|
|
@@ -2014,7 +2090,7 @@ var cachedVersion = null;
|
|
|
2014
2090
|
function readVersion() {
|
|
2015
2091
|
if (cachedVersion !== null) return cachedVersion;
|
|
2016
2092
|
try {
|
|
2017
|
-
const injected = "0.4.
|
|
2093
|
+
const injected = "0.4.5";
|
|
2018
2094
|
if (typeof injected === "string" && injected.length > 0) {
|
|
2019
2095
|
cachedVersion = injected;
|
|
2020
2096
|
return cachedVersion;
|
|
@@ -2029,273 +2105,258 @@ function readVersion() {
|
|
|
2029
2105
|
}
|
|
2030
2106
|
return cachedVersion;
|
|
2031
2107
|
}
|
|
2108
|
+
var PING = {
|
|
2109
|
+
kind: "ping",
|
|
2110
|
+
handle: (_envelope, { ts, deps }) => {
|
|
2111
|
+
const payload = {
|
|
2112
|
+
pong: true,
|
|
2113
|
+
daemonPid: process.pid,
|
|
2114
|
+
uptimeMs: ts - deps.startedAt
|
|
2115
|
+
};
|
|
2116
|
+
return payload;
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
var STATUS = {
|
|
2120
|
+
kind: "status",
|
|
2121
|
+
handle: (_envelope, { ts, deps }) => {
|
|
2122
|
+
const channels = (deps.channelManager?.listChannels() ?? []).map((c) => ({
|
|
2123
|
+
id: c.id,
|
|
2124
|
+
state: c.health?.transportOk === false ? "degraded" : "running",
|
|
2125
|
+
...c.health?.at !== void 0 ? { lastHealthAt: c.health.at } : {},
|
|
2126
|
+
...c.health?.note !== void 0 ? { note: c.health.note } : {}
|
|
2127
|
+
}));
|
|
2128
|
+
const payload = {
|
|
2129
|
+
daemonPid: process.pid,
|
|
2130
|
+
startedAt: deps.startedAt,
|
|
2131
|
+
uptimeMs: ts - deps.startedAt,
|
|
2132
|
+
version: readVersion(),
|
|
2133
|
+
listener: deps.getListener?.() ?? {
|
|
2134
|
+
kind: "uds",
|
|
2135
|
+
socketPath: "<unknown>"
|
|
2136
|
+
},
|
|
2137
|
+
channels,
|
|
2138
|
+
runtimes: runtimeStatusEntries(deps.pipeline)
|
|
2139
|
+
};
|
|
2140
|
+
return payload;
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
var CHANNELS_RELOAD = {
|
|
2144
|
+
kind: "channels.reload",
|
|
2145
|
+
requires: ["reloadChannels"],
|
|
2146
|
+
unsupportedMessage: "channel reload not configured",
|
|
2147
|
+
handle: async (_envelope, { deps }) => {
|
|
2148
|
+
const payload = await deps.reloadChannels();
|
|
2149
|
+
return payload;
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
var SESSION_REGISTER = {
|
|
2153
|
+
kind: "session.register",
|
|
2154
|
+
requires: ["pipeline"],
|
|
2155
|
+
unsupportedMessage: "session.register not configured",
|
|
2156
|
+
handle: (envelope, { deps, connection }) => {
|
|
2157
|
+
const req = envelope.payload;
|
|
2158
|
+
const reg = deps.pipeline.registerRuntime({
|
|
2159
|
+
runtimeId: req.runtimeId,
|
|
2160
|
+
defaultAgentId: req.defaultAgentId,
|
|
2161
|
+
pid: req.pid,
|
|
2162
|
+
connectionId: connection.connectionId,
|
|
2163
|
+
push: connection.push
|
|
2164
|
+
});
|
|
2165
|
+
const payload = {
|
|
2166
|
+
registeredAt: reg.registeredAt,
|
|
2167
|
+
gatewayStartedAt: deps.startedAt
|
|
2168
|
+
};
|
|
2169
|
+
return payload;
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
var SESSION_UNREGISTER = {
|
|
2173
|
+
kind: "session.unregister",
|
|
2174
|
+
requires: ["pipeline"],
|
|
2175
|
+
unsupportedMessage: "session.unregister not configured",
|
|
2176
|
+
handle: (envelope, { deps, ts }) => {
|
|
2177
|
+
const req = envelope.payload;
|
|
2178
|
+
deps.pipeline.unregisterRuntime(req.runtimeId);
|
|
2179
|
+
const payload = {
|
|
2180
|
+
unregisteredAt: ts
|
|
2181
|
+
};
|
|
2182
|
+
return payload;
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
var SESSION_TURN_COMPLETE = {
|
|
2186
|
+
kind: "session.turn.complete",
|
|
2187
|
+
requires: ["pipeline"],
|
|
2188
|
+
unsupportedMessage: "pipeline not configured",
|
|
2189
|
+
handle: async (envelope, { deps }) => {
|
|
2190
|
+
const req = envelope.payload;
|
|
2191
|
+
return await deps.pipeline.handleTurnComplete(req);
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
var CHANNEL_SEND = {
|
|
2195
|
+
kind: "channel.send",
|
|
2196
|
+
requires: ["channelManager"],
|
|
2197
|
+
unsupportedMessage: "channel manager not configured",
|
|
2198
|
+
handle: async (envelope, { deps }) => {
|
|
2199
|
+
const req = envelope.payload;
|
|
2200
|
+
const result = await deps.channelManager.send(
|
|
2201
|
+
req.message.location.channelId,
|
|
2202
|
+
req.message
|
|
2203
|
+
);
|
|
2204
|
+
const payload = {
|
|
2205
|
+
providerMessageId: result.providerMessageId,
|
|
2206
|
+
deliveredAt: result.deliveredAt
|
|
2207
|
+
};
|
|
2208
|
+
return payload;
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
var RELAY_PERMISSION_REQUEST = {
|
|
2212
|
+
kind: "relay.permission.request",
|
|
2213
|
+
requires: ["relayCoordinator"],
|
|
2214
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2215
|
+
requireRegisteredRuntime: true,
|
|
2216
|
+
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2217
|
+
const req = envelope.payload;
|
|
2218
|
+
const broadcast = deps.relayCoordinator.requestPermission({
|
|
2219
|
+
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2220
|
+
toolName: req.toolName,
|
|
2221
|
+
description: req.description,
|
|
2222
|
+
inputPreview: req.inputPreview,
|
|
2223
|
+
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2224
|
+
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2225
|
+
});
|
|
2226
|
+
const result = await broadcast.result;
|
|
2227
|
+
const payload = {
|
|
2228
|
+
channelRequestId: broadcast.channelRequestId,
|
|
2229
|
+
result
|
|
2230
|
+
};
|
|
2231
|
+
return payload;
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
var RELAY_PERMISSION_CANCEL = {
|
|
2235
|
+
kind: "relay.permission.cancel",
|
|
2236
|
+
requires: ["relayCoordinator"],
|
|
2237
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2238
|
+
requireRegisteredRuntime: true,
|
|
2239
|
+
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2240
|
+
const req = envelope.payload;
|
|
2241
|
+
const cancelled = deps.relayCoordinator.cancel(
|
|
2242
|
+
req.channelRequestId,
|
|
2243
|
+
req.reason,
|
|
2244
|
+
callerRuntimeId
|
|
2245
|
+
);
|
|
2246
|
+
const payload = { cancelled };
|
|
2247
|
+
return payload;
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
var RELAY_QUESTION_REQUEST = {
|
|
2251
|
+
kind: "relay.question.request",
|
|
2252
|
+
requires: ["relayCoordinator"],
|
|
2253
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2254
|
+
requireRegisteredRuntime: true,
|
|
2255
|
+
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2256
|
+
const req = envelope.payload;
|
|
2257
|
+
const broadcast = deps.relayCoordinator.requestQuestion({
|
|
2258
|
+
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2259
|
+
title: req.title,
|
|
2260
|
+
questions: req.questions,
|
|
2261
|
+
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2262
|
+
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2263
|
+
});
|
|
2264
|
+
const result = await broadcast.result;
|
|
2265
|
+
const payload = {
|
|
2266
|
+
channelRequestId: broadcast.channelRequestId,
|
|
2267
|
+
result
|
|
2268
|
+
};
|
|
2269
|
+
return payload;
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
var RELAY_QUESTION_CANCEL = {
|
|
2273
|
+
kind: "relay.question.cancel",
|
|
2274
|
+
requires: ["relayCoordinator"],
|
|
2275
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2276
|
+
requireRegisteredRuntime: true,
|
|
2277
|
+
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2278
|
+
const req = envelope.payload;
|
|
2279
|
+
const cancelled = deps.relayCoordinator.cancel(
|
|
2280
|
+
req.channelRequestId,
|
|
2281
|
+
req.reason,
|
|
2282
|
+
callerRuntimeId
|
|
2283
|
+
);
|
|
2284
|
+
const payload = { cancelled };
|
|
2285
|
+
return payload;
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
var HANDLERS = new Map(
|
|
2289
|
+
[
|
|
2290
|
+
PING,
|
|
2291
|
+
STATUS,
|
|
2292
|
+
CHANNELS_RELOAD,
|
|
2293
|
+
SESSION_REGISTER,
|
|
2294
|
+
SESSION_UNREGISTER,
|
|
2295
|
+
SESSION_TURN_COMPLETE,
|
|
2296
|
+
CHANNEL_SEND,
|
|
2297
|
+
RELAY_PERMISSION_REQUEST,
|
|
2298
|
+
RELAY_PERMISSION_CANCEL,
|
|
2299
|
+
RELAY_QUESTION_REQUEST,
|
|
2300
|
+
RELAY_QUESTION_CANCEL
|
|
2301
|
+
].map((spec) => [spec.kind, spec])
|
|
2302
|
+
);
|
|
2032
2303
|
function createDispatcher(deps) {
|
|
2033
2304
|
const handle = async (envelope, connection) => {
|
|
2034
2305
|
const ts = Date.now();
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
state: c.health?.transportOk === false ? "degraded" : "running",
|
|
2048
|
-
...c.health?.at !== void 0 ? { lastHealthAt: c.health.at } : {},
|
|
2049
|
-
...c.health?.note !== void 0 ? { note: c.health.note } : {}
|
|
2050
|
-
}));
|
|
2051
|
-
const payload = {
|
|
2052
|
-
daemonPid: process.pid,
|
|
2053
|
-
startedAt: deps.startedAt,
|
|
2054
|
-
uptimeMs: ts - deps.startedAt,
|
|
2055
|
-
version: readVersion(),
|
|
2056
|
-
listener: deps.getListener?.() ?? {
|
|
2057
|
-
kind: "uds",
|
|
2058
|
-
socketPath: "<unknown>"
|
|
2059
|
-
},
|
|
2060
|
-
channels,
|
|
2061
|
-
runtimes: runtimeStatusEntries(deps.registry)
|
|
2062
|
-
};
|
|
2063
|
-
return ok(envelope, ts, payload);
|
|
2064
|
-
}
|
|
2065
|
-
case "channels.reload": {
|
|
2066
|
-
if (!deps.reloadChannels) {
|
|
2067
|
-
return error(
|
|
2068
|
-
envelope,
|
|
2069
|
-
ts,
|
|
2070
|
-
"unsupported",
|
|
2071
|
-
"channel reload not configured"
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
const payload = await deps.reloadChannels();
|
|
2075
|
-
return ok(envelope, ts, payload);
|
|
2076
|
-
}
|
|
2077
|
-
case "session.register": {
|
|
2078
|
-
if (!deps.registry)
|
|
2079
|
-
return error(
|
|
2080
|
-
envelope,
|
|
2081
|
-
ts,
|
|
2082
|
-
"unsupported",
|
|
2083
|
-
"session.register not configured"
|
|
2084
|
-
);
|
|
2085
|
-
const req = envelope.payload;
|
|
2086
|
-
try {
|
|
2087
|
-
const reg = deps.registry.register({
|
|
2088
|
-
runtimeId: req.runtimeId,
|
|
2089
|
-
defaultAgentId: req.defaultAgentId,
|
|
2090
|
-
pid: req.pid
|
|
2091
|
-
});
|
|
2092
|
-
deps.registerRuntimeConnection?.(req.runtimeId, connection);
|
|
2093
|
-
try {
|
|
2094
|
-
deps.dispatcher?.drainPending();
|
|
2095
|
-
} catch (err) {
|
|
2096
|
-
process.stderr.write(
|
|
2097
|
-
`gateway: drainPending failed: ${err instanceof Error ? err.message : String(err)}
|
|
2098
|
-
`
|
|
2099
|
-
);
|
|
2100
|
-
}
|
|
2101
|
-
const payload = {
|
|
2102
|
-
registeredAt: reg.registeredAt,
|
|
2103
|
-
gatewayStartedAt: deps.startedAt
|
|
2104
|
-
};
|
|
2105
|
-
return ok(envelope, ts, payload);
|
|
2106
|
-
} catch (err) {
|
|
2107
|
-
if (err instanceof AlreadyRegisteredError) {
|
|
2108
|
-
return error(envelope, ts, err.code, err.message);
|
|
2109
|
-
}
|
|
2110
|
-
throw err;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
case "session.unregister": {
|
|
2114
|
-
if (!deps.registry)
|
|
2115
|
-
return error(
|
|
2116
|
-
envelope,
|
|
2117
|
-
ts,
|
|
2118
|
-
"unsupported",
|
|
2119
|
-
"session.unregister not configured"
|
|
2120
|
-
);
|
|
2121
|
-
const req = envelope.payload;
|
|
2122
|
-
try {
|
|
2123
|
-
deps.registry.unregister(req.runtimeId);
|
|
2124
|
-
deps.unregisterRuntimeConnection?.(req.runtimeId);
|
|
2125
|
-
const payload = {
|
|
2126
|
-
unregisteredAt: ts
|
|
2127
|
-
};
|
|
2128
|
-
return ok(envelope, ts, payload);
|
|
2129
|
-
} catch (err) {
|
|
2130
|
-
if (err instanceof NotRegisteredError) {
|
|
2131
|
-
return error(envelope, ts, err.code, err.message);
|
|
2132
|
-
}
|
|
2133
|
-
throw err;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
case "session.turn.complete": {
|
|
2137
|
-
if (!deps.dispatcher)
|
|
2138
|
-
return error(
|
|
2139
|
-
envelope,
|
|
2140
|
-
ts,
|
|
2141
|
-
"unsupported",
|
|
2142
|
-
"dispatcher not configured"
|
|
2143
|
-
);
|
|
2144
|
-
const req = envelope.payload;
|
|
2145
|
-
const result = await deps.dispatcher.handleTurnComplete(req);
|
|
2146
|
-
return ok(envelope, ts, result);
|
|
2147
|
-
}
|
|
2148
|
-
case "channel.send": {
|
|
2149
|
-
if (!deps.channelManager)
|
|
2150
|
-
return error(
|
|
2151
|
-
envelope,
|
|
2152
|
-
ts,
|
|
2153
|
-
"unsupported",
|
|
2154
|
-
"channel manager not configured"
|
|
2155
|
-
);
|
|
2156
|
-
const req = envelope.payload;
|
|
2157
|
-
const result = await deps.channelManager.send(
|
|
2158
|
-
req.message.location.channelId,
|
|
2159
|
-
req.message
|
|
2160
|
-
);
|
|
2161
|
-
const payload = {
|
|
2162
|
-
providerMessageId: result.providerMessageId,
|
|
2163
|
-
deliveredAt: result.deliveredAt
|
|
2164
|
-
};
|
|
2165
|
-
return ok(envelope, ts, payload);
|
|
2166
|
-
}
|
|
2167
|
-
case "relay.permission.request": {
|
|
2168
|
-
if (!deps.relayCoordinator)
|
|
2169
|
-
return error(
|
|
2170
|
-
envelope,
|
|
2171
|
-
ts,
|
|
2172
|
-
"unsupported",
|
|
2173
|
-
"relay coordinator not configured"
|
|
2174
|
-
);
|
|
2175
|
-
const req = envelope.payload;
|
|
2176
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2177
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2178
|
-
return error(
|
|
2179
|
-
envelope,
|
|
2180
|
-
ts,
|
|
2181
|
-
"not_registered",
|
|
2182
|
-
"relay.permission.request requires a registered runtime connection"
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
const broadcast = deps.relayCoordinator.requestPermission({
|
|
2186
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2187
|
-
toolName: req.toolName,
|
|
2188
|
-
description: req.description,
|
|
2189
|
-
inputPreview: req.inputPreview,
|
|
2190
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2191
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2192
|
-
});
|
|
2193
|
-
const result = await broadcast.result;
|
|
2194
|
-
const payload = {
|
|
2195
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2196
|
-
result
|
|
2197
|
-
};
|
|
2198
|
-
return ok(envelope, Date.now(), payload);
|
|
2199
|
-
}
|
|
2200
|
-
case "relay.permission.cancel": {
|
|
2201
|
-
if (!deps.relayCoordinator)
|
|
2202
|
-
return error(
|
|
2203
|
-
envelope,
|
|
2204
|
-
ts,
|
|
2205
|
-
"unsupported",
|
|
2206
|
-
"relay coordinator not configured"
|
|
2207
|
-
);
|
|
2208
|
-
const req = envelope.payload;
|
|
2209
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2210
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2211
|
-
return error(
|
|
2212
|
-
envelope,
|
|
2213
|
-
ts,
|
|
2214
|
-
"not_registered",
|
|
2215
|
-
"relay.permission.cancel requires a registered runtime connection"
|
|
2216
|
-
);
|
|
2217
|
-
}
|
|
2218
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2219
|
-
req.channelRequestId,
|
|
2220
|
-
req.reason,
|
|
2221
|
-
callerRuntimeId
|
|
2222
|
-
);
|
|
2223
|
-
const payload = { cancelled };
|
|
2224
|
-
return ok(envelope, ts, payload);
|
|
2225
|
-
}
|
|
2226
|
-
case "relay.question.request": {
|
|
2227
|
-
if (!deps.relayCoordinator)
|
|
2228
|
-
return error(
|
|
2229
|
-
envelope,
|
|
2230
|
-
ts,
|
|
2231
|
-
"unsupported",
|
|
2232
|
-
"relay coordinator not configured"
|
|
2233
|
-
);
|
|
2234
|
-
const req = envelope.payload;
|
|
2235
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2236
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2237
|
-
return error(
|
|
2238
|
-
envelope,
|
|
2239
|
-
ts,
|
|
2240
|
-
"not_registered",
|
|
2241
|
-
"relay.question.request requires a registered runtime connection"
|
|
2242
|
-
);
|
|
2243
|
-
}
|
|
2244
|
-
const broadcast = deps.relayCoordinator.requestQuestion({
|
|
2245
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2246
|
-
title: req.title,
|
|
2247
|
-
questions: req.questions,
|
|
2248
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2249
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2250
|
-
});
|
|
2251
|
-
const result = await broadcast.result;
|
|
2252
|
-
const payload = {
|
|
2253
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2254
|
-
result
|
|
2255
|
-
};
|
|
2256
|
-
return ok(envelope, Date.now(), payload);
|
|
2257
|
-
}
|
|
2258
|
-
case "relay.question.cancel": {
|
|
2259
|
-
if (!deps.relayCoordinator)
|
|
2306
|
+
const spec = HANDLERS.get(envelope.kind);
|
|
2307
|
+
if (!spec) {
|
|
2308
|
+
return error(
|
|
2309
|
+
envelope,
|
|
2310
|
+
ts,
|
|
2311
|
+
"unknown_kind",
|
|
2312
|
+
`unknown kind: ${envelope.kind}`
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
if (spec.requires) {
|
|
2316
|
+
for (const name of spec.requires) {
|
|
2317
|
+
if (deps[name] === void 0) {
|
|
2260
2318
|
return error(
|
|
2261
2319
|
envelope,
|
|
2262
2320
|
ts,
|
|
2263
2321
|
"unsupported",
|
|
2264
|
-
|
|
2265
|
-
);
|
|
2266
|
-
const req = envelope.payload;
|
|
2267
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2268
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2269
|
-
return error(
|
|
2270
|
-
envelope,
|
|
2271
|
-
ts,
|
|
2272
|
-
"not_registered",
|
|
2273
|
-
"relay.question.cancel requires a registered runtime connection"
|
|
2322
|
+
spec.unsupportedMessage ?? `${spec.kind} not configured`
|
|
2274
2323
|
);
|
|
2275
2324
|
}
|
|
2276
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2277
|
-
req.channelRequestId,
|
|
2278
|
-
req.reason,
|
|
2279
|
-
callerRuntimeId
|
|
2280
|
-
);
|
|
2281
|
-
const payload = { cancelled };
|
|
2282
|
-
return ok(envelope, ts, payload);
|
|
2283
2325
|
}
|
|
2284
|
-
|
|
2326
|
+
}
|
|
2327
|
+
let callerRuntimeId;
|
|
2328
|
+
if (spec.requireRegisteredRuntime) {
|
|
2329
|
+
callerRuntimeId = deps.pipeline?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2330
|
+
if (deps.pipeline && callerRuntimeId === void 0) {
|
|
2285
2331
|
return error(
|
|
2286
2332
|
envelope,
|
|
2287
2333
|
ts,
|
|
2288
|
-
"
|
|
2289
|
-
|
|
2334
|
+
"not_registered",
|
|
2335
|
+
`${spec.kind} requires a registered runtime connection`
|
|
2290
2336
|
);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
const payload = await spec.handle(envelope, {
|
|
2341
|
+
deps,
|
|
2342
|
+
connection,
|
|
2343
|
+
callerRuntimeId,
|
|
2344
|
+
ts
|
|
2345
|
+
});
|
|
2346
|
+
return ok(envelope, Date.now(), payload);
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
if (err instanceof AlreadyRegisteredError || err instanceof NotRegisteredError) {
|
|
2349
|
+
return error(envelope, ts, err.code, err.message);
|
|
2350
|
+
}
|
|
2351
|
+
throw err;
|
|
2291
2352
|
}
|
|
2292
2353
|
};
|
|
2293
2354
|
return handle;
|
|
2294
2355
|
}
|
|
2295
|
-
function runtimeStatusEntries(
|
|
2296
|
-
const runtime =
|
|
2297
|
-
if (!runtime || !
|
|
2298
|
-
const binding =
|
|
2356
|
+
function runtimeStatusEntries(pipeline) {
|
|
2357
|
+
const runtime = pipeline?.getCurrentRuntime();
|
|
2358
|
+
if (!runtime || !pipeline) return [];
|
|
2359
|
+
const binding = pipeline.getBinding();
|
|
2299
2360
|
return [
|
|
2300
2361
|
{
|
|
2301
2362
|
runtimeId: runtime.runtimeId,
|
|
@@ -2313,7 +2374,7 @@ function runtimeStatusEntries(registry) {
|
|
|
2313
2374
|
epoch: binding.epoch,
|
|
2314
2375
|
...maybeLastRebindAt(binding.lastRebindAt)
|
|
2315
2376
|
} : { state: "none" },
|
|
2316
|
-
pendingDispatchCount:
|
|
2377
|
+
pendingDispatchCount: pipeline.pendingDispatchCount()
|
|
2317
2378
|
}
|
|
2318
2379
|
];
|
|
2319
2380
|
}
|
|
@@ -2436,55 +2497,367 @@ function handleConnection(connection, expectedToken, startedAt, handler, logErro
|
|
|
2436
2497
|
});
|
|
2437
2498
|
}
|
|
2438
2499
|
|
|
2439
|
-
// src/gateway/
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2500
|
+
// src/gateway/dispatchPipeline.ts
|
|
2501
|
+
import crypto from "crypto";
|
|
2502
|
+
|
|
2503
|
+
// src/gateway/outboundDispatcher.ts
|
|
2504
|
+
var DEFAULT_BACKOFF = [
|
|
2505
|
+
1e3,
|
|
2506
|
+
// 1s
|
|
2507
|
+
2e3,
|
|
2508
|
+
// 2s
|
|
2509
|
+
4e3,
|
|
2510
|
+
// 4s
|
|
2511
|
+
8e3,
|
|
2512
|
+
// 8s
|
|
2513
|
+
16e3,
|
|
2514
|
+
// 16s
|
|
2515
|
+
3e4
|
|
2516
|
+
// 30s
|
|
2517
|
+
];
|
|
2518
|
+
var DEFAULT_MAX_ATTEMPTS = 10;
|
|
2519
|
+
var DEFAULT_TICK_MS = 1e3;
|
|
2520
|
+
var DEFAULT_BATCH = 16;
|
|
2521
|
+
var OutboundDispatcher = class {
|
|
2522
|
+
outbox;
|
|
2523
|
+
send;
|
|
2524
|
+
backoff;
|
|
2525
|
+
maxAttempts;
|
|
2526
|
+
tickMs;
|
|
2527
|
+
batchSize;
|
|
2528
|
+
now;
|
|
2529
|
+
log;
|
|
2530
|
+
timer = null;
|
|
2531
|
+
draining = false;
|
|
2532
|
+
stopped = false;
|
|
2533
|
+
constructor(opts) {
|
|
2534
|
+
this.outbox = opts.outbox;
|
|
2535
|
+
this.send = opts.send;
|
|
2536
|
+
this.backoff = opts.backoffSchedule ?? DEFAULT_BACKOFF;
|
|
2537
|
+
this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2538
|
+
this.tickMs = opts.tickIntervalMs ?? DEFAULT_TICK_MS;
|
|
2539
|
+
this.batchSize = opts.drainBatchSize ?? DEFAULT_BATCH;
|
|
2540
|
+
this.now = opts.now ?? Date.now;
|
|
2541
|
+
this.log = opts.log;
|
|
2542
|
+
}
|
|
2543
|
+
start() {
|
|
2544
|
+
if (this.timer) return;
|
|
2545
|
+
this.timer = setInterval(() => {
|
|
2546
|
+
void this.drain();
|
|
2547
|
+
}, this.tickMs);
|
|
2548
|
+
this.timer.unref();
|
|
2549
|
+
}
|
|
2550
|
+
stop() {
|
|
2551
|
+
this.stopped = true;
|
|
2552
|
+
if (this.timer) {
|
|
2553
|
+
clearInterval(this.timer);
|
|
2554
|
+
this.timer = null;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
async dispatch(channelId, msg) {
|
|
2558
|
+
try {
|
|
2559
|
+
const result = await this.send(channelId, msg);
|
|
2560
|
+
return { kind: "sent", result };
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
const error2 = err instanceof Error ? err.message : String(err);
|
|
2563
|
+
const nextAttemptAt = this.now() + this.backoffFor(0);
|
|
2564
|
+
const id = this.outbox.enqueue({
|
|
2565
|
+
channelId,
|
|
2566
|
+
message: msg,
|
|
2567
|
+
nextAttemptAt,
|
|
2568
|
+
lastError: error2
|
|
2569
|
+
});
|
|
2570
|
+
this.log?.(
|
|
2571
|
+
"warn",
|
|
2572
|
+
`send to ${channelId} failed; parked as outbox#${id}: ${error2}`
|
|
2573
|
+
);
|
|
2574
|
+
return { kind: "queued", outboxId: id, error: error2 };
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Drain due entries. Exposed for tests; the timer also calls this. Safe
|
|
2579
|
+
* to call concurrently — a re-entry guard short-circuits.
|
|
2580
|
+
*/
|
|
2581
|
+
async drain() {
|
|
2582
|
+
if (this.draining || this.stopped) {
|
|
2583
|
+
return { retried: 0, succeeded: 0, dropped: 0 };
|
|
2584
|
+
}
|
|
2585
|
+
this.draining = true;
|
|
2586
|
+
let retried = 0;
|
|
2587
|
+
let succeeded = 0;
|
|
2588
|
+
let dropped = 0;
|
|
2589
|
+
try {
|
|
2590
|
+
const due = this.outbox.peekDue(this.now(), this.batchSize);
|
|
2591
|
+
for (const row of due) {
|
|
2592
|
+
retried += 1;
|
|
2593
|
+
const outcome = await this.attempt(row);
|
|
2594
|
+
if (outcome === "succeeded") succeeded += 1;
|
|
2595
|
+
else if (outcome === "dropped") dropped += 1;
|
|
2596
|
+
}
|
|
2597
|
+
} finally {
|
|
2598
|
+
this.draining = false;
|
|
2599
|
+
}
|
|
2600
|
+
return { retried, succeeded, dropped };
|
|
2601
|
+
}
|
|
2602
|
+
async attempt(row) {
|
|
2603
|
+
try {
|
|
2604
|
+
await this.send(row.channelId, row.message);
|
|
2605
|
+
this.outbox.delete(row.id);
|
|
2606
|
+
this.log?.(
|
|
2607
|
+
"info",
|
|
2608
|
+
`outbox#${row.id} delivered to ${row.channelId} on attempt ${row.attempt + 1}`
|
|
2609
|
+
);
|
|
2610
|
+
return "succeeded";
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
const error2 = err instanceof Error ? err.message : String(err);
|
|
2613
|
+
const nextAttempt = row.attempt + 1;
|
|
2614
|
+
if (nextAttempt >= this.maxAttempts) {
|
|
2615
|
+
this.outbox.delete(row.id);
|
|
2616
|
+
this.log?.(
|
|
2617
|
+
"error",
|
|
2618
|
+
`outbox#${row.id} dropped after ${nextAttempt} attempts: ${error2}`
|
|
2619
|
+
);
|
|
2620
|
+
return "dropped";
|
|
2621
|
+
}
|
|
2622
|
+
const nextAttemptAt = this.now() + this.backoffFor(nextAttempt);
|
|
2623
|
+
this.outbox.recordFailure({
|
|
2624
|
+
id: row.id,
|
|
2625
|
+
nextAttemptAt,
|
|
2626
|
+
lastError: error2
|
|
2627
|
+
});
|
|
2628
|
+
return "requeued";
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
backoffFor(attempt) {
|
|
2632
|
+
if (this.backoff.length === 0) return 1e3;
|
|
2633
|
+
const idx = Math.min(attempt, this.backoff.length - 1);
|
|
2634
|
+
return this.backoff[idx];
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
// src/gateway/router/sessionKey.ts
|
|
2639
|
+
function deriveSessionKey(loc) {
|
|
2640
|
+
const c = loc.channelId;
|
|
2641
|
+
const a = loc.accountId;
|
|
2642
|
+
if (loc.peer?.id) {
|
|
2643
|
+
const peer = loc.peer.id;
|
|
2644
|
+
if (loc.thread?.id) {
|
|
2645
|
+
return `peer:${c}:${a}:${peer}:${loc.thread.id}`;
|
|
2646
|
+
}
|
|
2647
|
+
return `peer:${c}:${a}:${peer}`;
|
|
2648
|
+
}
|
|
2649
|
+
if (loc.room?.id) {
|
|
2650
|
+
const room = loc.room.id;
|
|
2651
|
+
if (loc.thread?.id) {
|
|
2652
|
+
return `room:${c}:${a}:${room}:${loc.thread.id}`;
|
|
2653
|
+
}
|
|
2455
2654
|
return `room:${c}:${a}:${room}`;
|
|
2456
2655
|
}
|
|
2457
2656
|
return `default:${c}:${a}`;
|
|
2458
2657
|
}
|
|
2459
2658
|
|
|
2460
|
-
// src/gateway/
|
|
2461
|
-
|
|
2659
|
+
// src/gateway/sessionRegistry.ts
|
|
2660
|
+
import { randomUUID } from "crypto";
|
|
2661
|
+
var UnknownDispatchError = class extends Error {
|
|
2662
|
+
code = "unknown_dispatch";
|
|
2663
|
+
constructor(id) {
|
|
2664
|
+
super(`unknown dispatchId: ${id}`);
|
|
2665
|
+
this.name = "UnknownDispatchError";
|
|
2666
|
+
}
|
|
2667
|
+
};
|
|
2668
|
+
var SessionRegistry = class {
|
|
2669
|
+
dispatches = /* @__PURE__ */ new Map();
|
|
2670
|
+
idFactory;
|
|
2671
|
+
now;
|
|
2672
|
+
constructor(opts = {}) {
|
|
2673
|
+
this.idFactory = opts.idFactory ?? randomUUID;
|
|
2674
|
+
this.now = opts.now ?? Date.now;
|
|
2675
|
+
}
|
|
2676
|
+
beginDispatch(input) {
|
|
2677
|
+
const dispatchId = this.idFactory();
|
|
2678
|
+
const entry = {
|
|
2679
|
+
dispatchId,
|
|
2680
|
+
sessionKey: input.sessionKey,
|
|
2681
|
+
agentId: input.agentId,
|
|
2682
|
+
location: input.location,
|
|
2683
|
+
createdAt: this.now()
|
|
2684
|
+
};
|
|
2685
|
+
this.dispatches.set(dispatchId, entry);
|
|
2686
|
+
return entry;
|
|
2687
|
+
}
|
|
2688
|
+
completeDispatch(dispatchId) {
|
|
2689
|
+
const entry = this.dispatches.get(dispatchId);
|
|
2690
|
+
if (!entry) {
|
|
2691
|
+
throw new UnknownDispatchError(dispatchId);
|
|
2692
|
+
}
|
|
2693
|
+
this.dispatches.delete(dispatchId);
|
|
2694
|
+
return entry;
|
|
2695
|
+
}
|
|
2696
|
+
pendingDispatchCount() {
|
|
2697
|
+
return this.dispatches.size;
|
|
2698
|
+
}
|
|
2699
|
+
clearDispatches() {
|
|
2700
|
+
this.dispatches.clear();
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
// src/gateway/state/inboundQueue.ts
|
|
2705
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2706
|
+
var InboundQueue = class {
|
|
2707
|
+
db;
|
|
2708
|
+
maxEntries;
|
|
2709
|
+
constructor(db, opts = {}) {
|
|
2710
|
+
this.db = db;
|
|
2711
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2712
|
+
}
|
|
2713
|
+
size() {
|
|
2714
|
+
const row = this.db.prepare("SELECT COUNT(*) as n FROM inbound_queue").get();
|
|
2715
|
+
return row.n;
|
|
2716
|
+
}
|
|
2717
|
+
enqueue(inbound) {
|
|
2718
|
+
if (this.size() >= this.maxEntries) {
|
|
2719
|
+
return { kind: "rejected", reason: "queue_full" };
|
|
2720
|
+
}
|
|
2721
|
+
const stmt = this.db.prepare(
|
|
2722
|
+
`INSERT INTO inbound_queue
|
|
2723
|
+
(channel_id, account_id, idempotency_key, payload_json, created_at)
|
|
2724
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2725
|
+
ON CONFLICT(channel_id, account_id, idempotency_key) DO NOTHING`
|
|
2726
|
+
);
|
|
2727
|
+
const result = stmt.run(
|
|
2728
|
+
inbound.location.channelId,
|
|
2729
|
+
inbound.location.accountId,
|
|
2730
|
+
inbound.idempotencyKey,
|
|
2731
|
+
JSON.stringify(inbound),
|
|
2732
|
+
Date.now()
|
|
2733
|
+
);
|
|
2734
|
+
if (result.changes === 0) {
|
|
2735
|
+
return { kind: "duplicate" };
|
|
2736
|
+
}
|
|
2737
|
+
return { kind: "queued", id: Number(result.lastInsertRowid) };
|
|
2738
|
+
}
|
|
2739
|
+
/** Atomically read and remove all parked entries in FIFO order. */
|
|
2740
|
+
drain() {
|
|
2741
|
+
return this.db.transaction(() => {
|
|
2742
|
+
const rows = this.db.prepare("SELECT id, payload_json FROM inbound_queue ORDER BY id ASC").all();
|
|
2743
|
+
if (rows.length > 0) {
|
|
2744
|
+
this.db.prepare("DELETE FROM inbound_queue").run();
|
|
2745
|
+
}
|
|
2746
|
+
return rows.map((r) => ({
|
|
2747
|
+
id: r.id,
|
|
2748
|
+
inbound: JSON.parse(r.payload_json)
|
|
2749
|
+
}));
|
|
2750
|
+
})();
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
// src/gateway/state/outbox.ts
|
|
2755
|
+
var Outbox = class {
|
|
2756
|
+
db;
|
|
2757
|
+
constructor(db) {
|
|
2758
|
+
this.db = db;
|
|
2759
|
+
}
|
|
2760
|
+
size() {
|
|
2761
|
+
const row = this.db.prepare("SELECT COUNT(*) as n FROM channel_outbox").get();
|
|
2762
|
+
return row.n;
|
|
2763
|
+
}
|
|
2764
|
+
enqueue(input) {
|
|
2765
|
+
const result = this.db.prepare(
|
|
2766
|
+
`INSERT INTO channel_outbox
|
|
2767
|
+
(channel_id, payload_json, attempt, next_attempt_at, last_error, created_at)
|
|
2768
|
+
VALUES (?, ?, 0, ?, ?, ?)`
|
|
2769
|
+
).run(
|
|
2770
|
+
input.channelId,
|
|
2771
|
+
JSON.stringify(input.message),
|
|
2772
|
+
input.nextAttemptAt,
|
|
2773
|
+
input.lastError ?? null,
|
|
2774
|
+
Date.now()
|
|
2775
|
+
);
|
|
2776
|
+
return Number(result.lastInsertRowid);
|
|
2777
|
+
}
|
|
2778
|
+
/** Rows whose `next_attempt_at` is at or before `now`, oldest first. */
|
|
2779
|
+
peekDue(now, limit) {
|
|
2780
|
+
const rows = this.db.prepare(
|
|
2781
|
+
`SELECT id, channel_id, payload_json, attempt, next_attempt_at, last_error
|
|
2782
|
+
FROM channel_outbox
|
|
2783
|
+
WHERE next_attempt_at <= ?
|
|
2784
|
+
ORDER BY next_attempt_at ASC, id ASC
|
|
2785
|
+
LIMIT ?`
|
|
2786
|
+
).all(now, limit);
|
|
2787
|
+
return rows.map((r) => ({
|
|
2788
|
+
id: r.id,
|
|
2789
|
+
channelId: r.channel_id,
|
|
2790
|
+
message: JSON.parse(r.payload_json),
|
|
2791
|
+
attempt: r.attempt,
|
|
2792
|
+
nextAttemptAt: r.next_attempt_at,
|
|
2793
|
+
lastError: r.last_error
|
|
2794
|
+
}));
|
|
2795
|
+
}
|
|
2796
|
+
delete(id) {
|
|
2797
|
+
this.db.prepare("DELETE FROM channel_outbox WHERE id = ?").run(id);
|
|
2798
|
+
}
|
|
2799
|
+
recordFailure(input) {
|
|
2800
|
+
this.db.prepare(
|
|
2801
|
+
`UPDATE channel_outbox
|
|
2802
|
+
SET attempt = attempt + 1,
|
|
2803
|
+
next_attempt_at = ?,
|
|
2804
|
+
last_error = ?
|
|
2805
|
+
WHERE id = ?`
|
|
2806
|
+
).run(input.nextAttemptAt, input.lastError, input.id);
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
|
|
2810
|
+
// src/gateway/dispatchPipeline.ts
|
|
2811
|
+
var DispatchPipeline = class {
|
|
2812
|
+
bindingStore;
|
|
2462
2813
|
registry;
|
|
2463
|
-
pushDispatch;
|
|
2464
|
-
sendOutbound;
|
|
2465
|
-
resolveAgent;
|
|
2466
2814
|
inboundQueue;
|
|
2467
|
-
|
|
2815
|
+
outbox;
|
|
2816
|
+
outboundDispatcher;
|
|
2817
|
+
resolveAgent;
|
|
2468
2818
|
log;
|
|
2819
|
+
now;
|
|
2820
|
+
idFactory;
|
|
2821
|
+
push = null;
|
|
2469
2822
|
constructor(opts) {
|
|
2470
|
-
this.
|
|
2471
|
-
|
|
2472
|
-
|
|
2823
|
+
this.bindingStore = new RuntimeBindingStore({
|
|
2824
|
+
gracePeriodMs: opts.gracePeriodMs,
|
|
2825
|
+
observers: opts.observers,
|
|
2826
|
+
now: opts.now
|
|
2827
|
+
});
|
|
2828
|
+
this.registry = new SessionRegistry({
|
|
2829
|
+
now: opts.now ?? Date.now,
|
|
2830
|
+
...opts.idFactory ? { idFactory: opts.idFactory } : {}
|
|
2831
|
+
});
|
|
2832
|
+
this.inboundQueue = new InboundQueue(opts.stateDb, opts.inboundQueue ?? {});
|
|
2833
|
+
this.outbox = new Outbox(opts.stateDb);
|
|
2834
|
+
this.outboundDispatcher = new OutboundDispatcher({
|
|
2835
|
+
outbox: this.outbox,
|
|
2836
|
+
send: opts.send,
|
|
2837
|
+
...opts.outbox?.backoffSchedule ? { backoffSchedule: opts.outbox.backoffSchedule } : {},
|
|
2838
|
+
...opts.outbox?.maxAttempts !== void 0 ? { maxAttempts: opts.outbox.maxAttempts } : {},
|
|
2839
|
+
...opts.outbox?.tickIntervalMs !== void 0 ? { tickIntervalMs: opts.outbox.tickIntervalMs } : {},
|
|
2840
|
+
...opts.outbox?.drainBatchSize !== void 0 ? { drainBatchSize: opts.outbox.drainBatchSize } : {},
|
|
2841
|
+
...opts.now ? { now: opts.now } : {},
|
|
2842
|
+
...opts.log ? { log: opts.log } : {}
|
|
2843
|
+
});
|
|
2473
2844
|
this.resolveAgent = opts.resolveAgent ?? ((input) => input.defaultAgentId);
|
|
2474
|
-
this.inboundQueue = opts.inboundQueue;
|
|
2475
|
-
this.canDispatch = opts.canDispatch ?? (() => true);
|
|
2476
2845
|
this.log = opts.log;
|
|
2846
|
+
this.now = opts.now ?? Date.now;
|
|
2847
|
+
this.idFactory = opts.idFactory ?? crypto.randomUUID;
|
|
2848
|
+
}
|
|
2849
|
+
// ── lifecycle ────────────────────────────────────────────
|
|
2850
|
+
start() {
|
|
2851
|
+
this.outboundDispatcher.start();
|
|
2852
|
+
}
|
|
2853
|
+
async stop() {
|
|
2854
|
+
this.outboundDispatcher.stop();
|
|
2855
|
+
this.bindingStore.stop();
|
|
2477
2856
|
}
|
|
2857
|
+
// ── inbound (channel side) ───────────────────────────────
|
|
2478
2858
|
handleInbound(inbound) {
|
|
2479
|
-
const current = this.
|
|
2480
|
-
if (!current || !this.
|
|
2481
|
-
if (!this.inboundQueue) {
|
|
2482
|
-
this.log?.(
|
|
2483
|
-
"debug",
|
|
2484
|
-
`no runtime registered; dropping inbound ${inbound.idempotencyKey}`
|
|
2485
|
-
);
|
|
2486
|
-
return { kind: "dropped", reason: "no_runtime" };
|
|
2487
|
-
}
|
|
2859
|
+
const current = this.bindingStore.getCurrent();
|
|
2860
|
+
if (!current || !this.bindingStore.hasActiveBinding(current.runtimeId)) {
|
|
2488
2861
|
const result = this.inboundQueue.enqueue(inbound);
|
|
2489
2862
|
if (result.kind === "queued") {
|
|
2490
2863
|
this.log?.(
|
|
@@ -2506,6 +2879,9 @@ var Dispatcher = class {
|
|
|
2506
2879
|
);
|
|
2507
2880
|
return { kind: "dropped", reason: "queue_full" };
|
|
2508
2881
|
}
|
|
2882
|
+
return this.dispatchInboundToRuntime(inbound, current);
|
|
2883
|
+
}
|
|
2884
|
+
dispatchInboundToRuntime(inbound, current) {
|
|
2509
2885
|
const sessionKey = deriveSessionKey(inbound.location);
|
|
2510
2886
|
const agentId = this.resolveAgent({
|
|
2511
2887
|
sessionKey,
|
|
@@ -2530,64 +2906,50 @@ var Dispatcher = class {
|
|
|
2530
2906
|
agentId,
|
|
2531
2907
|
inbound
|
|
2532
2908
|
});
|
|
2533
|
-
return {
|
|
2534
|
-
kind: "dispatched",
|
|
2535
|
-
dispatchId: entry.dispatchId,
|
|
2536
|
-
sessionKey
|
|
2537
|
-
};
|
|
2909
|
+
return { kind: "dispatched", dispatchId: entry.dispatchId, sessionKey };
|
|
2538
2910
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
});
|
|
2577
|
-
dispatched += 1;
|
|
2578
|
-
}
|
|
2579
|
-
if (dispatched > 0 || dropped > 0) {
|
|
2580
|
-
this.log?.(
|
|
2581
|
-
"info",
|
|
2582
|
-
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
2583
|
-
);
|
|
2584
|
-
}
|
|
2585
|
-
return { dispatched, dropped };
|
|
2911
|
+
pushDispatch(payload) {
|
|
2912
|
+
if (!this.push) return;
|
|
2913
|
+
this.push.push({
|
|
2914
|
+
push_id: this.idFactory(),
|
|
2915
|
+
ts: this.now(),
|
|
2916
|
+
kind: "session.dispatch.turn",
|
|
2917
|
+
payload
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
// ── runtime side ─────────────────────────────────────────
|
|
2921
|
+
registerRuntime(input) {
|
|
2922
|
+
const result = this.bindingStore.bind({
|
|
2923
|
+
runtimeId: input.runtimeId,
|
|
2924
|
+
defaultAgentId: input.defaultAgentId,
|
|
2925
|
+
pid: input.pid,
|
|
2926
|
+
connectionId: input.connectionId
|
|
2927
|
+
});
|
|
2928
|
+
this.push = { connectionId: input.connectionId, push: input.push };
|
|
2929
|
+
writeGatewayTrace(
|
|
2930
|
+
`pipeline registered runtime runtimeId=${input.runtimeId} connectionId=${input.connectionId}`
|
|
2931
|
+
);
|
|
2932
|
+
this.drainPending();
|
|
2933
|
+
return { registeredAt: result.registeredAt };
|
|
2934
|
+
}
|
|
2935
|
+
unregisterRuntime(runtimeId) {
|
|
2936
|
+
this.bindingStore.unbind(runtimeId);
|
|
2937
|
+
this.registry.clearDispatches();
|
|
2938
|
+
this.push = null;
|
|
2939
|
+
writeGatewayTrace(`pipeline unregistered runtime runtimeId=${runtimeId}`);
|
|
2940
|
+
}
|
|
2941
|
+
notifyConnectionClosed(connectionId) {
|
|
2942
|
+
const runtimeId = this.bindingStore.notifyConnectionClosed(connectionId);
|
|
2943
|
+
if (runtimeId === null) return;
|
|
2944
|
+
writeGatewayTrace(
|
|
2945
|
+
`pipeline runtime connection closed runtimeId=${runtimeId} connectionId=${connectionId}`
|
|
2946
|
+
);
|
|
2947
|
+
this.push = null;
|
|
2586
2948
|
}
|
|
2587
2949
|
async handleTurnComplete(payload) {
|
|
2588
|
-
const current = this.
|
|
2950
|
+
const current = this.bindingStore.getCurrent();
|
|
2589
2951
|
writeGatewayTrace(
|
|
2590
|
-
`
|
|
2952
|
+
`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}`
|
|
2591
2953
|
);
|
|
2592
2954
|
if (!current || current.runtimeId !== payload.runtimeId) {
|
|
2593
2955
|
throw new Error("runtime mismatch on session.turn.complete");
|
|
@@ -2598,28 +2960,75 @@ var Dispatcher = class {
|
|
|
2598
2960
|
} catch (err) {
|
|
2599
2961
|
if (err instanceof UnknownDispatchError) {
|
|
2600
2962
|
writeGatewayTrace(
|
|
2601
|
-
`
|
|
2963
|
+
`pipeline turn.complete unknown dispatchId=${payload.dispatchId}`
|
|
2602
2964
|
);
|
|
2603
2965
|
return { delivered: false };
|
|
2604
2966
|
}
|
|
2605
2967
|
throw err;
|
|
2606
2968
|
}
|
|
2969
|
+
const result = await this.sendOutbound(entry.location, payload);
|
|
2607
2970
|
writeGatewayTrace(
|
|
2608
|
-
`
|
|
2971
|
+
`pipeline sendOutbound delivered dispatchId=${payload.dispatchId} providerMessageId=${result.providerMessageId}`
|
|
2609
2972
|
);
|
|
2610
|
-
|
|
2973
|
+
return { delivered: true, providerMessageId: result.providerMessageId };
|
|
2974
|
+
}
|
|
2975
|
+
async sendOutbound(_parkedLocation, payload) {
|
|
2976
|
+
const out = {
|
|
2611
2977
|
location: payload.location,
|
|
2612
2978
|
text: payload.text,
|
|
2613
2979
|
idempotencyKey: payload.idempotencyKey
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2980
|
+
};
|
|
2981
|
+
const result = await this.outboundDispatcher.dispatch(
|
|
2982
|
+
payload.location.channelId,
|
|
2983
|
+
out
|
|
2617
2984
|
);
|
|
2985
|
+
if (result.kind === "sent") return result.result;
|
|
2618
2986
|
return {
|
|
2619
|
-
|
|
2620
|
-
|
|
2987
|
+
providerMessageId: `outbox:${result.outboxId}`,
|
|
2988
|
+
deliveredAt: this.now()
|
|
2621
2989
|
};
|
|
2622
2990
|
}
|
|
2991
|
+
drainPending() {
|
|
2992
|
+
const current = this.bindingStore.getCurrent();
|
|
2993
|
+
if (!current || !this.bindingStore.hasActiveBinding(current.runtimeId))
|
|
2994
|
+
return;
|
|
2995
|
+
const parked = this.inboundQueue.drain();
|
|
2996
|
+
let dispatched = 0;
|
|
2997
|
+
let dropped = 0;
|
|
2998
|
+
for (const { inbound } of parked) {
|
|
2999
|
+
const result = this.dispatchInboundToRuntime(inbound, current);
|
|
3000
|
+
if (result.kind === "dispatched") dispatched += 1;
|
|
3001
|
+
else dropped += 1;
|
|
3002
|
+
}
|
|
3003
|
+
if (dispatched > 0 || dropped > 0) {
|
|
3004
|
+
this.log?.(
|
|
3005
|
+
"info",
|
|
3006
|
+
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
// ── reads ────────────────────────────────────────────────
|
|
3011
|
+
getCurrentRuntime() {
|
|
3012
|
+
return this.bindingStore.getCurrent();
|
|
3013
|
+
}
|
|
3014
|
+
getBinding() {
|
|
3015
|
+
return this.bindingStore.getBinding();
|
|
3016
|
+
}
|
|
3017
|
+
hasActiveBinding(runtimeId) {
|
|
3018
|
+
return this.bindingStore.hasActiveBinding(runtimeId);
|
|
3019
|
+
}
|
|
3020
|
+
getRuntimeIdByConnection(connectionId) {
|
|
3021
|
+
return this.bindingStore.getRuntimeIdByConnection(connectionId);
|
|
3022
|
+
}
|
|
3023
|
+
pendingDispatchCount() {
|
|
3024
|
+
return this.registry.pendingDispatchCount();
|
|
3025
|
+
}
|
|
3026
|
+
pendingInboundCount() {
|
|
3027
|
+
return this.inboundQueue.size();
|
|
3028
|
+
}
|
|
3029
|
+
pendingOutboxCount() {
|
|
3030
|
+
return this.outbox.size();
|
|
3031
|
+
}
|
|
2623
3032
|
};
|
|
2624
3033
|
|
|
2625
3034
|
// src/gateway/lock.ts
|
|
@@ -2687,140 +3096,63 @@ function acquireLock(lockPath) {
|
|
|
2687
3096
|
throw new Error(`failed to acquire gateway lock at ${lockPath}`);
|
|
2688
3097
|
}
|
|
2689
3098
|
|
|
2690
|
-
// src/gateway/
|
|
2691
|
-
var
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
// 8s
|
|
2700
|
-
16e3,
|
|
2701
|
-
// 16s
|
|
2702
|
-
3e4
|
|
2703
|
-
// 30s
|
|
2704
|
-
];
|
|
2705
|
-
var DEFAULT_MAX_ATTEMPTS = 10;
|
|
2706
|
-
var DEFAULT_TICK_MS = 1e3;
|
|
2707
|
-
var DEFAULT_BATCH = 16;
|
|
2708
|
-
var OutboundDispatcher = class {
|
|
2709
|
-
outbox;
|
|
2710
|
-
send;
|
|
2711
|
-
backoff;
|
|
2712
|
-
maxAttempts;
|
|
2713
|
-
tickMs;
|
|
2714
|
-
batchSize;
|
|
2715
|
-
now;
|
|
2716
|
-
log;
|
|
2717
|
-
timer = null;
|
|
2718
|
-
draining = false;
|
|
2719
|
-
stopped = false;
|
|
2720
|
-
constructor(opts) {
|
|
2721
|
-
this.outbox = opts.outbox;
|
|
2722
|
-
this.send = opts.send;
|
|
2723
|
-
this.backoff = opts.backoffSchedule ?? DEFAULT_BACKOFF;
|
|
2724
|
-
this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2725
|
-
this.tickMs = opts.tickIntervalMs ?? DEFAULT_TICK_MS;
|
|
2726
|
-
this.batchSize = opts.drainBatchSize ?? DEFAULT_BATCH;
|
|
2727
|
-
this.now = opts.now ?? Date.now;
|
|
2728
|
-
this.log = opts.log;
|
|
2729
|
-
}
|
|
2730
|
-
start() {
|
|
2731
|
-
if (this.timer) return;
|
|
2732
|
-
this.timer = setInterval(() => {
|
|
2733
|
-
void this.drain();
|
|
2734
|
-
}, this.tickMs);
|
|
2735
|
-
this.timer.unref();
|
|
2736
|
-
}
|
|
2737
|
-
stop() {
|
|
2738
|
-
this.stopped = true;
|
|
2739
|
-
if (this.timer) {
|
|
2740
|
-
clearInterval(this.timer);
|
|
2741
|
-
this.timer = null;
|
|
3099
|
+
// src/gateway/relay/pendingRegistry.ts
|
|
3100
|
+
var PendingRegistry = class {
|
|
3101
|
+
entries = /* @__PURE__ */ new Map();
|
|
3102
|
+
inspect(channelRequestId, kind, fingerprint, runtimeId) {
|
|
3103
|
+
const existing = this.entries.get(channelRequestId);
|
|
3104
|
+
if (!existing) return { kind: "absent" };
|
|
3105
|
+
if (existing.kind !== kind) return { kind: "collision", reason: "kind" };
|
|
3106
|
+
if (existing.fingerprint !== fingerprint) {
|
|
3107
|
+
return { kind: "collision", reason: "payload" };
|
|
2742
3108
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
try {
|
|
2746
|
-
const result = await this.send(channelId, msg);
|
|
2747
|
-
return { kind: "sent", result };
|
|
2748
|
-
} catch (err) {
|
|
2749
|
-
const error2 = err instanceof Error ? err.message : String(err);
|
|
2750
|
-
const nextAttemptAt = this.now() + this.backoffFor(0);
|
|
2751
|
-
const id = this.outbox.enqueue({
|
|
2752
|
-
channelId,
|
|
2753
|
-
message: msg,
|
|
2754
|
-
nextAttemptAt,
|
|
2755
|
-
lastError: error2
|
|
2756
|
-
});
|
|
2757
|
-
this.log?.(
|
|
2758
|
-
"warn",
|
|
2759
|
-
`send to ${channelId} failed; parked as outbox#${id}: ${error2}`
|
|
2760
|
-
);
|
|
2761
|
-
return { kind: "queued", outboxId: id, error: error2 };
|
|
3109
|
+
if (existing.runtimeId !== runtimeId) {
|
|
3110
|
+
return { kind: "collision", reason: "owner" };
|
|
2762
3111
|
}
|
|
3112
|
+
return { kind: "attach", entry: existing };
|
|
2763
3113
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
if (
|
|
2770
|
-
|
|
3114
|
+
register(entry) {
|
|
3115
|
+
this.entries.set(entry.channelRequestId, entry);
|
|
3116
|
+
}
|
|
3117
|
+
settle(channelRequestId, result) {
|
|
3118
|
+
const entry = this.entries.get(channelRequestId);
|
|
3119
|
+
if (!entry || entry.settled) return false;
|
|
3120
|
+
entry.settled = true;
|
|
3121
|
+
this.entries.delete(channelRequestId);
|
|
3122
|
+
clearTimeout(entry.timer);
|
|
3123
|
+
for (const ctrl of entry.controllers) {
|
|
3124
|
+
if (!ctrl.signal.aborted) ctrl.abort();
|
|
2771
3125
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
const outcome = await this.attempt(row);
|
|
2781
|
-
if (outcome === "succeeded") succeeded += 1;
|
|
2782
|
-
else if (outcome === "dropped") dropped += 1;
|
|
2783
|
-
}
|
|
2784
|
-
} finally {
|
|
2785
|
-
this.draining = false;
|
|
3126
|
+
entry.resolve(result);
|
|
3127
|
+
return true;
|
|
3128
|
+
}
|
|
3129
|
+
cancel(channelRequestId, reason, expectedRuntimeId) {
|
|
3130
|
+
const entry = this.entries.get(channelRequestId);
|
|
3131
|
+
if (!entry) return false;
|
|
3132
|
+
if (expectedRuntimeId !== void 0 && entry.runtimeId !== void 0 && entry.runtimeId !== expectedRuntimeId) {
|
|
3133
|
+
return false;
|
|
2786
3134
|
}
|
|
2787
|
-
return {
|
|
3135
|
+
return this.settle(channelRequestId, { kind: "cancelled", reason });
|
|
2788
3136
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
this.outbox.delete(row.id);
|
|
2793
|
-
this.log?.(
|
|
2794
|
-
"info",
|
|
2795
|
-
`outbox#${row.id} delivered to ${row.channelId} on attempt ${row.attempt + 1}`
|
|
2796
|
-
);
|
|
2797
|
-
return "succeeded";
|
|
2798
|
-
} catch (err) {
|
|
2799
|
-
const error2 = err instanceof Error ? err.message : String(err);
|
|
2800
|
-
const nextAttempt = row.attempt + 1;
|
|
2801
|
-
if (nextAttempt >= this.maxAttempts) {
|
|
2802
|
-
this.outbox.delete(row.id);
|
|
2803
|
-
this.log?.(
|
|
2804
|
-
"error",
|
|
2805
|
-
`outbox#${row.id} dropped after ${nextAttempt} attempts: ${error2}`
|
|
2806
|
-
);
|
|
2807
|
-
return "dropped";
|
|
2808
|
-
}
|
|
2809
|
-
const nextAttemptAt = this.now() + this.backoffFor(nextAttempt);
|
|
2810
|
-
this.outbox.recordFailure({
|
|
2811
|
-
id: row.id,
|
|
2812
|
-
nextAttemptAt,
|
|
2813
|
-
lastError: error2
|
|
2814
|
-
});
|
|
2815
|
-
return "requeued";
|
|
3137
|
+
disposeAll(reason) {
|
|
3138
|
+
for (const id of [...this.entries.keys()]) {
|
|
3139
|
+
this.cancel(id, reason, void 0);
|
|
2816
3140
|
}
|
|
2817
3141
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
const idx = Math.min(attempt, this.backoff.length - 1);
|
|
2821
|
-
return this.backoff[idx];
|
|
3142
|
+
count() {
|
|
3143
|
+
return this.entries.size;
|
|
2822
3144
|
}
|
|
2823
3145
|
};
|
|
3146
|
+
function collisionMessage(channelRequestId, reason, newKind) {
|
|
3147
|
+
if (reason === "owner") {
|
|
3148
|
+
return `channel_request_owner_mismatch: ${channelRequestId} owned by a different runtime`;
|
|
3149
|
+
}
|
|
3150
|
+
if (reason === "kind") {
|
|
3151
|
+
const otherKind = newKind === "permission" ? "question" : "permission";
|
|
3152
|
+
return `channel_request_id_collision: ${channelRequestId} is bound to a ${otherKind} relay`;
|
|
3153
|
+
}
|
|
3154
|
+
return `channel_request_id_collision: ${channelRequestId} payload mismatch`;
|
|
3155
|
+
}
|
|
2824
3156
|
|
|
2825
3157
|
// src/gateway/relay/coordinator.ts
|
|
2826
3158
|
var DEFAULT_RELAY_TTL_MS = 5 * 6e4;
|
|
@@ -2829,214 +3161,160 @@ var RelayCoordinator = class {
|
|
|
2829
3161
|
defaultTtlMs;
|
|
2830
3162
|
idFactory;
|
|
2831
3163
|
log;
|
|
2832
|
-
|
|
3164
|
+
registry = new PendingRegistry();
|
|
2833
3165
|
constructor(opts) {
|
|
2834
3166
|
this.adapters = opts.adapters;
|
|
2835
|
-
this.defaultTtlMs = opts.defaultTtlMs ?? DEFAULT_RELAY_TTL_MS;
|
|
2836
|
-
this.idFactory = opts.idFactory ?? generateChannelRequestId;
|
|
2837
|
-
this.log = opts.log;
|
|
2838
|
-
}
|
|
2839
|
-
requestPermission(req) {
|
|
2840
|
-
const channelRequestId = req.channelRequestId ?? this.idFactory();
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
)
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
};
|
|
2850
|
-
}
|
|
2851
|
-
const fingerprint = permissionFingerprint(req);
|
|
2852
|
-
const existing = this.pending.get(channelRequestId);
|
|
2853
|
-
if (existing) {
|
|
2854
|
-
if (existing.kind !== "permission") {
|
|
2855
|
-
throw new Error(
|
|
2856
|
-
`channel_request_id_collision: ${channelRequestId} is bound to a question relay`
|
|
2857
|
-
);
|
|
2858
|
-
}
|
|
2859
|
-
if (existing.fingerprint !== fingerprint) {
|
|
2860
|
-
throw new Error(
|
|
2861
|
-
`channel_request_id_collision: ${channelRequestId} payload mismatch`
|
|
2862
|
-
);
|
|
2863
|
-
}
|
|
2864
|
-
if (existing.runtimeId !== req.runtimeId) {
|
|
2865
|
-
throw new Error(
|
|
2866
|
-
`channel_request_owner_mismatch: ${channelRequestId} owned by a different runtime`
|
|
2867
|
-
);
|
|
2868
|
-
}
|
|
2869
|
-
return { channelRequestId, result: existing.result };
|
|
2870
|
-
}
|
|
2871
|
-
const controllers = targets.map(() => new AbortController());
|
|
2872
|
-
let resolveFn;
|
|
2873
|
-
const result = new Promise((resolve) => {
|
|
2874
|
-
resolveFn = resolve;
|
|
2875
|
-
});
|
|
2876
|
-
const timer = setTimeout(() => {
|
|
2877
|
-
this.settlePermission(channelRequestId, {
|
|
2878
|
-
kind: "cancelled",
|
|
2879
|
-
reason: "timeout"
|
|
2880
|
-
});
|
|
2881
|
-
}, ttlMs);
|
|
2882
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
2883
|
-
const entry = {
|
|
2884
|
-
kind: "permission",
|
|
3167
|
+
this.defaultTtlMs = opts.defaultTtlMs ?? DEFAULT_RELAY_TTL_MS;
|
|
3168
|
+
this.idFactory = opts.idFactory ?? generateChannelRequestId;
|
|
3169
|
+
this.log = opts.log;
|
|
3170
|
+
}
|
|
3171
|
+
requestPermission(req) {
|
|
3172
|
+
const channelRequestId = req.channelRequestId ?? this.idFactory();
|
|
3173
|
+
const targets = this.adapters().filter(
|
|
3174
|
+
(a) => a.capabilities.relayPermission && typeof a.requestPermissionVerdict === "function"
|
|
3175
|
+
);
|
|
3176
|
+
if (targets.length === 0) {
|
|
3177
|
+
return { channelRequestId, result: Promise.resolve({ kind: "no_relay" }) };
|
|
3178
|
+
}
|
|
3179
|
+
const fingerprint = permissionFingerprint(req);
|
|
3180
|
+
const inspect = this.registry.inspect(
|
|
2885
3181
|
channelRequestId,
|
|
3182
|
+
"permission",
|
|
2886
3183
|
fingerprint,
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
}
|
|
2894
|
-
|
|
3184
|
+
req.runtimeId
|
|
3185
|
+
);
|
|
3186
|
+
if (inspect.kind === "collision") {
|
|
3187
|
+
throw new Error(
|
|
3188
|
+
collisionMessage(channelRequestId, inspect.reason, "permission")
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
if (inspect.kind === "attach") {
|
|
3192
|
+
return {
|
|
3193
|
+
channelRequestId,
|
|
3194
|
+
result: inspect.entry.result
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
2895
3197
|
const fullReq = {
|
|
2896
3198
|
channelRequestId,
|
|
2897
3199
|
toolName: req.toolName,
|
|
2898
3200
|
description: req.description,
|
|
2899
3201
|
inputPreview: req.inputPreview
|
|
2900
3202
|
};
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
"warn",
|
|
2913
|
-
`adapter ${adapter.id} permission relay failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2914
|
-
);
|
|
2915
|
-
});
|
|
3203
|
+
const result = this.broadcast({
|
|
3204
|
+
kind: "permission",
|
|
3205
|
+
channelRequestId,
|
|
3206
|
+
ttlMs: req.ttlMs ?? this.defaultTtlMs,
|
|
3207
|
+
runtimeId: req.runtimeId,
|
|
3208
|
+
fingerprint,
|
|
3209
|
+
targets,
|
|
3210
|
+
perAdapter: async (adapter, signal) => {
|
|
3211
|
+
const res = await adapter.requestPermissionVerdict(fullReq, signal);
|
|
3212
|
+
return res.kind === "verdict" ? { ...res, channelId: adapter.id } : null;
|
|
3213
|
+
}
|
|
2916
3214
|
});
|
|
2917
3215
|
return { channelRequestId, result };
|
|
2918
3216
|
}
|
|
2919
3217
|
requestQuestion(req) {
|
|
2920
3218
|
const channelRequestId = req.channelRequestId ?? this.idFactory();
|
|
2921
|
-
const ttlMs = req.ttlMs ?? this.defaultTtlMs;
|
|
2922
3219
|
const targets = this.adapters().filter(
|
|
2923
3220
|
(a) => a.capabilities.relayQuestion && typeof a.requestQuestionAnswer === "function"
|
|
2924
3221
|
);
|
|
2925
3222
|
if (targets.length === 0) {
|
|
3223
|
+
return { channelRequestId, result: Promise.resolve({ kind: "no_relay" }) };
|
|
3224
|
+
}
|
|
3225
|
+
const fingerprint = questionFingerprint(req);
|
|
3226
|
+
const inspect = this.registry.inspect(
|
|
3227
|
+
channelRequestId,
|
|
3228
|
+
"question",
|
|
3229
|
+
fingerprint,
|
|
3230
|
+
req.runtimeId
|
|
3231
|
+
);
|
|
3232
|
+
if (inspect.kind === "collision") {
|
|
3233
|
+
throw new Error(
|
|
3234
|
+
collisionMessage(channelRequestId, inspect.reason, "question")
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
if (inspect.kind === "attach") {
|
|
2926
3238
|
return {
|
|
2927
3239
|
channelRequestId,
|
|
2928
|
-
result:
|
|
3240
|
+
result: inspect.entry.result
|
|
2929
3241
|
};
|
|
2930
3242
|
}
|
|
2931
|
-
const
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
`channel_request_owner_mismatch: ${channelRequestId} owned by a different runtime`
|
|
2947
|
-
);
|
|
3243
|
+
const fullReq = {
|
|
3244
|
+
channelRequestId,
|
|
3245
|
+
title: req.title,
|
|
3246
|
+
questions: req.questions
|
|
3247
|
+
};
|
|
3248
|
+
const result = this.broadcast({
|
|
3249
|
+
kind: "question",
|
|
3250
|
+
channelRequestId,
|
|
3251
|
+
ttlMs: req.ttlMs ?? this.defaultTtlMs,
|
|
3252
|
+
runtimeId: req.runtimeId,
|
|
3253
|
+
fingerprint,
|
|
3254
|
+
targets,
|
|
3255
|
+
perAdapter: async (adapter, signal) => {
|
|
3256
|
+
const res = await adapter.requestQuestionAnswer(fullReq, signal);
|
|
3257
|
+
return res.kind === "answer" ? { ...res, channelId: adapter.id } : null;
|
|
2948
3258
|
}
|
|
2949
|
-
|
|
2950
|
-
}
|
|
3259
|
+
});
|
|
3260
|
+
return { channelRequestId, result };
|
|
3261
|
+
}
|
|
3262
|
+
cancel(channelRequestId, reason, expectedRuntimeId) {
|
|
3263
|
+
return this.registry.cancel(channelRequestId, reason, expectedRuntimeId);
|
|
3264
|
+
}
|
|
3265
|
+
pendingCount() {
|
|
3266
|
+
return this.registry.count();
|
|
3267
|
+
}
|
|
3268
|
+
disposeAll(reason = "auto_resolved") {
|
|
3269
|
+
this.registry.disposeAll(reason);
|
|
3270
|
+
}
|
|
3271
|
+
broadcast(args) {
|
|
3272
|
+
const {
|
|
3273
|
+
kind,
|
|
3274
|
+
channelRequestId,
|
|
3275
|
+
ttlMs,
|
|
3276
|
+
runtimeId,
|
|
3277
|
+
fingerprint,
|
|
3278
|
+
targets,
|
|
3279
|
+
perAdapter
|
|
3280
|
+
} = args;
|
|
2951
3281
|
const controllers = targets.map(() => new AbortController());
|
|
2952
3282
|
let resolveFn;
|
|
2953
3283
|
const result = new Promise((resolve) => {
|
|
2954
3284
|
resolveFn = resolve;
|
|
2955
3285
|
});
|
|
2956
3286
|
const timer = setTimeout(() => {
|
|
2957
|
-
this.
|
|
3287
|
+
this.registry.settle(channelRequestId, {
|
|
2958
3288
|
kind: "cancelled",
|
|
2959
3289
|
reason: "timeout"
|
|
2960
3290
|
});
|
|
2961
3291
|
}, ttlMs);
|
|
2962
3292
|
if (typeof timer.unref === "function") timer.unref();
|
|
2963
|
-
|
|
2964
|
-
kind
|
|
3293
|
+
this.registry.register({
|
|
3294
|
+
kind,
|
|
2965
3295
|
channelRequestId,
|
|
2966
3296
|
fingerprint,
|
|
2967
|
-
|
|
3297
|
+
runtimeId,
|
|
2968
3298
|
controllers,
|
|
2969
3299
|
timer,
|
|
2970
3300
|
resolve: resolveFn,
|
|
2971
3301
|
result,
|
|
2972
3302
|
settled: false
|
|
2973
|
-
};
|
|
2974
|
-
this.pending.set(channelRequestId, entry);
|
|
2975
|
-
const fullReq = {
|
|
2976
|
-
channelRequestId,
|
|
2977
|
-
title: req.title,
|
|
2978
|
-
questions: req.questions
|
|
2979
|
-
};
|
|
3303
|
+
});
|
|
2980
3304
|
targets.forEach((adapter, idx) => {
|
|
2981
3305
|
const ctrl = controllers[idx];
|
|
2982
|
-
Promise.resolve().then(() => adapter
|
|
2983
|
-
if (res
|
|
2984
|
-
this.
|
|
2985
|
-
...res,
|
|
2986
|
-
channelId: adapter.id
|
|
2987
|
-
});
|
|
3306
|
+
Promise.resolve().then(() => perAdapter(adapter, ctrl.signal)).then((res) => {
|
|
3307
|
+
if (res !== null) {
|
|
3308
|
+
this.registry.settle(channelRequestId, res);
|
|
2988
3309
|
}
|
|
2989
3310
|
}).catch((err) => {
|
|
2990
3311
|
this.log?.(
|
|
2991
3312
|
"warn",
|
|
2992
|
-
`adapter ${adapter.id}
|
|
3313
|
+
`adapter ${adapter.id} ${kind} relay failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2993
3314
|
);
|
|
2994
3315
|
});
|
|
2995
3316
|
});
|
|
2996
|
-
return
|
|
2997
|
-
}
|
|
2998
|
-
cancel(channelRequestId, reason, expectedRuntimeId) {
|
|
2999
|
-
const entry = this.pending.get(channelRequestId);
|
|
3000
|
-
if (!entry) return false;
|
|
3001
|
-
if (expectedRuntimeId !== void 0 && entry.runtimeId !== void 0 && entry.runtimeId !== expectedRuntimeId) {
|
|
3002
|
-
return false;
|
|
3003
|
-
}
|
|
3004
|
-
if (entry.kind === "permission") {
|
|
3005
|
-
this.settlePermission(channelRequestId, { kind: "cancelled", reason });
|
|
3006
|
-
} else {
|
|
3007
|
-
this.settleQuestion(channelRequestId, { kind: "cancelled", reason });
|
|
3008
|
-
}
|
|
3009
|
-
return true;
|
|
3010
|
-
}
|
|
3011
|
-
pendingCount() {
|
|
3012
|
-
return this.pending.size;
|
|
3013
|
-
}
|
|
3014
|
-
disposeAll(reason = "auto_resolved") {
|
|
3015
|
-
for (const id of [...this.pending.keys()]) {
|
|
3016
|
-
this.cancel(id, reason);
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
settlePermission(channelRequestId, result) {
|
|
3020
|
-
const entry = this.pending.get(channelRequestId);
|
|
3021
|
-
if (!entry || entry.kind !== "permission" || entry.settled) return;
|
|
3022
|
-
entry.settled = true;
|
|
3023
|
-
this.pending.delete(channelRequestId);
|
|
3024
|
-
clearTimeout(entry.timer);
|
|
3025
|
-
for (const ctrl of entry.controllers) {
|
|
3026
|
-
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3027
|
-
}
|
|
3028
|
-
entry.resolve(result);
|
|
3029
|
-
}
|
|
3030
|
-
settleQuestion(channelRequestId, result) {
|
|
3031
|
-
const entry = this.pending.get(channelRequestId);
|
|
3032
|
-
if (!entry || entry.kind !== "question" || entry.settled) return;
|
|
3033
|
-
entry.settled = true;
|
|
3034
|
-
this.pending.delete(channelRequestId);
|
|
3035
|
-
clearTimeout(entry.timer);
|
|
3036
|
-
for (const ctrl of entry.controllers) {
|
|
3037
|
-
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3038
|
-
}
|
|
3039
|
-
entry.resolve(result);
|
|
3317
|
+
return result;
|
|
3040
3318
|
}
|
|
3041
3319
|
};
|
|
3042
3320
|
function permissionFingerprint(req) {
|
|
@@ -3116,112 +3394,6 @@ function initGatewayStateSchema(db) {
|
|
|
3116
3394
|
}
|
|
3117
3395
|
}
|
|
3118
3396
|
|
|
3119
|
-
// src/gateway/state/inboundQueue.ts
|
|
3120
|
-
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
3121
|
-
var InboundQueue = class {
|
|
3122
|
-
db;
|
|
3123
|
-
maxEntries;
|
|
3124
|
-
constructor(db, opts = {}) {
|
|
3125
|
-
this.db = db;
|
|
3126
|
-
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
3127
|
-
}
|
|
3128
|
-
size() {
|
|
3129
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM inbound_queue").get();
|
|
3130
|
-
return row.n;
|
|
3131
|
-
}
|
|
3132
|
-
enqueue(inbound) {
|
|
3133
|
-
if (this.size() >= this.maxEntries) {
|
|
3134
|
-
return { kind: "rejected", reason: "queue_full" };
|
|
3135
|
-
}
|
|
3136
|
-
const stmt = this.db.prepare(
|
|
3137
|
-
`INSERT INTO inbound_queue
|
|
3138
|
-
(channel_id, account_id, idempotency_key, payload_json, created_at)
|
|
3139
|
-
VALUES (?, ?, ?, ?, ?)
|
|
3140
|
-
ON CONFLICT(channel_id, account_id, idempotency_key) DO NOTHING`
|
|
3141
|
-
);
|
|
3142
|
-
const result = stmt.run(
|
|
3143
|
-
inbound.location.channelId,
|
|
3144
|
-
inbound.location.accountId,
|
|
3145
|
-
inbound.idempotencyKey,
|
|
3146
|
-
JSON.stringify(inbound),
|
|
3147
|
-
Date.now()
|
|
3148
|
-
);
|
|
3149
|
-
if (result.changes === 0) {
|
|
3150
|
-
return { kind: "duplicate" };
|
|
3151
|
-
}
|
|
3152
|
-
return { kind: "queued", id: Number(result.lastInsertRowid) };
|
|
3153
|
-
}
|
|
3154
|
-
/** Atomically read and remove all parked entries in FIFO order. */
|
|
3155
|
-
drain() {
|
|
3156
|
-
return this.db.transaction(() => {
|
|
3157
|
-
const rows = this.db.prepare("SELECT id, payload_json FROM inbound_queue ORDER BY id ASC").all();
|
|
3158
|
-
if (rows.length > 0) {
|
|
3159
|
-
this.db.prepare("DELETE FROM inbound_queue").run();
|
|
3160
|
-
}
|
|
3161
|
-
return rows.map((r) => ({
|
|
3162
|
-
id: r.id,
|
|
3163
|
-
inbound: JSON.parse(r.payload_json)
|
|
3164
|
-
}));
|
|
3165
|
-
})();
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3168
|
-
|
|
3169
|
-
// src/gateway/state/outbox.ts
|
|
3170
|
-
var Outbox = class {
|
|
3171
|
-
db;
|
|
3172
|
-
constructor(db) {
|
|
3173
|
-
this.db = db;
|
|
3174
|
-
}
|
|
3175
|
-
size() {
|
|
3176
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM channel_outbox").get();
|
|
3177
|
-
return row.n;
|
|
3178
|
-
}
|
|
3179
|
-
enqueue(input) {
|
|
3180
|
-
const result = this.db.prepare(
|
|
3181
|
-
`INSERT INTO channel_outbox
|
|
3182
|
-
(channel_id, payload_json, attempt, next_attempt_at, last_error, created_at)
|
|
3183
|
-
VALUES (?, ?, 0, ?, ?, ?)`
|
|
3184
|
-
).run(
|
|
3185
|
-
input.channelId,
|
|
3186
|
-
JSON.stringify(input.message),
|
|
3187
|
-
input.nextAttemptAt,
|
|
3188
|
-
input.lastError ?? null,
|
|
3189
|
-
Date.now()
|
|
3190
|
-
);
|
|
3191
|
-
return Number(result.lastInsertRowid);
|
|
3192
|
-
}
|
|
3193
|
-
/** Rows whose `next_attempt_at` is at or before `now`, oldest first. */
|
|
3194
|
-
peekDue(now, limit) {
|
|
3195
|
-
const rows = this.db.prepare(
|
|
3196
|
-
`SELECT id, channel_id, payload_json, attempt, next_attempt_at, last_error
|
|
3197
|
-
FROM channel_outbox
|
|
3198
|
-
WHERE next_attempt_at <= ?
|
|
3199
|
-
ORDER BY next_attempt_at ASC, id ASC
|
|
3200
|
-
LIMIT ?`
|
|
3201
|
-
).all(now, limit);
|
|
3202
|
-
return rows.map((r) => ({
|
|
3203
|
-
id: r.id,
|
|
3204
|
-
channelId: r.channel_id,
|
|
3205
|
-
message: JSON.parse(r.payload_json),
|
|
3206
|
-
attempt: r.attempt,
|
|
3207
|
-
nextAttemptAt: r.next_attempt_at,
|
|
3208
|
-
lastError: r.last_error
|
|
3209
|
-
}));
|
|
3210
|
-
}
|
|
3211
|
-
delete(id) {
|
|
3212
|
-
this.db.prepare("DELETE FROM channel_outbox WHERE id = ?").run(id);
|
|
3213
|
-
}
|
|
3214
|
-
recordFailure(input) {
|
|
3215
|
-
this.db.prepare(
|
|
3216
|
-
`UPDATE channel_outbox
|
|
3217
|
-
SET attempt = attempt + 1,
|
|
3218
|
-
next_attempt_at = ?,
|
|
3219
|
-
last_error = ?
|
|
3220
|
-
WHERE id = ?`
|
|
3221
|
-
).run(input.nextAttemptAt, input.lastError, input.id);
|
|
3222
|
-
}
|
|
3223
|
-
};
|
|
3224
|
-
|
|
3225
3397
|
// src/gateway/transport/tlsWs.ts
|
|
3226
3398
|
import { WebSocketServer } from "ws";
|
|
3227
3399
|
import { createServer as createHttpsServer } from "https";
|
|
@@ -3466,62 +3638,35 @@ async function startDaemon(opts) {
|
|
|
3466
3638
|
loopback: listenSpec.kind === "uds" || isLoopbackHost(listenSpec.host)
|
|
3467
3639
|
};
|
|
3468
3640
|
const stateDb = openGatewayState(paths.statePath);
|
|
3469
|
-
const inboundQueue = new InboundQueue(stateDb);
|
|
3470
|
-
const outbox = new Outbox(stateDb);
|
|
3471
|
-
const registry = new SessionRegistry();
|
|
3472
3641
|
const channelManager = new ChannelManager();
|
|
3473
3642
|
const relayCoordinator = new RelayCoordinator({
|
|
3474
3643
|
adapters: () => channelManager.listAdapters()
|
|
3475
3644
|
});
|
|
3476
|
-
const runtimeConnections = /* @__PURE__ */ new Map();
|
|
3477
|
-
const staleRuntimeTimers = /* @__PURE__ */ new Map();
|
|
3478
3645
|
const connectionOpenedAt = /* @__PURE__ */ new Map();
|
|
3479
3646
|
const disconnectGracePeriodMs = opts.disconnectGracePeriodMs ?? 0;
|
|
3480
3647
|
let listenerStatus = null;
|
|
3481
|
-
const pushDispatch = (payload) => {
|
|
3482
|
-
const current = registry.getCurrent();
|
|
3483
|
-
if (!current) return;
|
|
3484
|
-
const ctx = runtimeConnections.get(current.runtimeId);
|
|
3485
|
-
if (!ctx) return;
|
|
3486
|
-
ctx.push({
|
|
3487
|
-
push_id: crypto.randomUUID(),
|
|
3488
|
-
ts: Date.now(),
|
|
3489
|
-
kind: "session.dispatch.turn",
|
|
3490
|
-
payload
|
|
3491
|
-
});
|
|
3492
|
-
};
|
|
3493
3648
|
const log = (level, message) => {
|
|
3494
3649
|
if (opts.silent) return;
|
|
3495
3650
|
const stream = level === "error" || level === "warn" ? "stderr" : "stdout";
|
|
3496
3651
|
process[stream].write(`athena-gateway: [${level}] ${message}
|
|
3497
3652
|
`);
|
|
3498
3653
|
};
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3654
|
+
const pipeline = new DispatchPipeline({
|
|
3655
|
+
stateDb,
|
|
3501
3656
|
send: (channelId, msg) => channelManager.send(channelId, msg),
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
},
|
|
3512
|
-
sendOutbound: async (channelId, msg) => {
|
|
3513
|
-
const result = await outboundDispatcher.dispatch(channelId, msg);
|
|
3514
|
-
if (result.kind === "sent") return result.result;
|
|
3515
|
-
return {
|
|
3516
|
-
providerMessageId: `outbox:${result.outboxId}`,
|
|
3517
|
-
deliveredAt: Date.now()
|
|
3518
|
-
};
|
|
3519
|
-
},
|
|
3520
|
-
inboundQueue,
|
|
3521
|
-
log
|
|
3657
|
+
gracePeriodMs: disconnectGracePeriodMs,
|
|
3658
|
+
log,
|
|
3659
|
+
observers: {
|
|
3660
|
+
onRuntimeRebind: ({ gapMs, epoch }) => trackGatewayRuntimeRebind({ gapMs, epoch }),
|
|
3661
|
+
onRuntimeExpired: ({ gapMs }) => trackGatewayRuntimeExpired({ gapMs }),
|
|
3662
|
+
// Single-runtime v1: blanket dispose is safe. Multi-runtime must
|
|
3663
|
+
// scope to the disconnecting runtime via disposeAllForRuntime.
|
|
3664
|
+
onRuntimeConnectionLost: () => relayCoordinator.disposeAll("connection_lost")
|
|
3665
|
+
}
|
|
3522
3666
|
});
|
|
3667
|
+
pipeline.start();
|
|
3523
3668
|
channelManager.setInboundSink((inbound) => {
|
|
3524
|
-
|
|
3669
|
+
pipeline.handleInbound(inbound);
|
|
3525
3670
|
});
|
|
3526
3671
|
const channelConfigHome = opts.env?.HOME;
|
|
3527
3672
|
const reloadChannels = async () => {
|
|
@@ -3536,7 +3681,7 @@ async function startDaemon(opts) {
|
|
|
3536
3681
|
reason: err.reason
|
|
3537
3682
|
});
|
|
3538
3683
|
}
|
|
3539
|
-
const sidecarIds = new Set(sidecars.map((s) => s.
|
|
3684
|
+
const sidecarIds = new Set(sidecars.map((s) => s.instanceId));
|
|
3540
3685
|
for (const channel of channelManager.listChannels()) {
|
|
3541
3686
|
if (sidecarIds.has(channel.id)) continue;
|
|
3542
3687
|
try {
|
|
@@ -3556,13 +3701,13 @@ async function startDaemon(opts) {
|
|
|
3556
3701
|
}
|
|
3557
3702
|
}
|
|
3558
3703
|
for (const sidecar of sidecars) {
|
|
3559
|
-
const existed = channelManager.listChannels().some((channel) => channel.id === sidecar.
|
|
3704
|
+
const existed = channelManager.listChannels().some((channel) => channel.id === sidecar.instanceId);
|
|
3560
3705
|
if (existed) {
|
|
3561
3706
|
try {
|
|
3562
|
-
await channelManager.unregister(sidecar.
|
|
3707
|
+
await channelManager.unregister(sidecar.instanceId, "shutdown");
|
|
3563
3708
|
} catch (err) {
|
|
3564
3709
|
results.push({
|
|
3565
|
-
id: sidecar.
|
|
3710
|
+
id: sidecar.instanceId,
|
|
3566
3711
|
ok: false,
|
|
3567
3712
|
action: "failed",
|
|
3568
3713
|
reason: err instanceof Error ? err.message : String(err)
|
|
@@ -3573,7 +3718,7 @@ async function startDaemon(opts) {
|
|
|
3573
3718
|
const built = instantiateAdapter(sidecar);
|
|
3574
3719
|
if (!built.ok) {
|
|
3575
3720
|
results.push({
|
|
3576
|
-
id: sidecar.
|
|
3721
|
+
id: sidecar.instanceId,
|
|
3577
3722
|
ok: false,
|
|
3578
3723
|
action: "failed",
|
|
3579
3724
|
reason: built.reason
|
|
@@ -3583,17 +3728,19 @@ async function startDaemon(opts) {
|
|
|
3583
3728
|
try {
|
|
3584
3729
|
await channelManager.register(built.adapter);
|
|
3585
3730
|
results.push({
|
|
3586
|
-
id: sidecar.
|
|
3731
|
+
id: sidecar.instanceId,
|
|
3587
3732
|
ok: true,
|
|
3588
3733
|
action: existed ? "replaced" : "registered"
|
|
3589
3734
|
});
|
|
3590
3735
|
if (!opts.silent) {
|
|
3591
|
-
process.stdout.write(
|
|
3592
|
-
`
|
|
3736
|
+
process.stdout.write(
|
|
3737
|
+
`athena-gateway: registered ${sidecar.instanceId}
|
|
3738
|
+
`
|
|
3739
|
+
);
|
|
3593
3740
|
}
|
|
3594
3741
|
} catch (err) {
|
|
3595
3742
|
results.push({
|
|
3596
|
-
id: sidecar.
|
|
3743
|
+
id: sidecar.instanceId,
|
|
3597
3744
|
ok: false,
|
|
3598
3745
|
action: "failed",
|
|
3599
3746
|
reason: err instanceof Error ? err.message : String(err)
|
|
@@ -3614,7 +3761,7 @@ async function startDaemon(opts) {
|
|
|
3614
3761
|
const built = instantiateAdapter(sidecar);
|
|
3615
3762
|
if (!built.ok) {
|
|
3616
3763
|
process.stderr.write(
|
|
3617
|
-
`athena-gateway: ${sidecar.
|
|
3764
|
+
`athena-gateway: ${sidecar.instanceId}: ${built.reason}
|
|
3618
3765
|
`
|
|
3619
3766
|
);
|
|
3620
3767
|
continue;
|
|
@@ -3622,12 +3769,14 @@ async function startDaemon(opts) {
|
|
|
3622
3769
|
try {
|
|
3623
3770
|
await channelManager.register(built.adapter);
|
|
3624
3771
|
if (!opts.silent) {
|
|
3625
|
-
process.stdout.write(
|
|
3626
|
-
`
|
|
3772
|
+
process.stdout.write(
|
|
3773
|
+
`athena-gateway: registered ${sidecar.instanceId}
|
|
3774
|
+
`
|
|
3775
|
+
);
|
|
3627
3776
|
}
|
|
3628
3777
|
} catch (err) {
|
|
3629
3778
|
process.stderr.write(
|
|
3630
|
-
`athena-gateway: register ${sidecar.
|
|
3779
|
+
`athena-gateway: register ${sidecar.instanceId} failed: ${err instanceof Error ? err.message : String(err)}
|
|
3631
3780
|
`
|
|
3632
3781
|
);
|
|
3633
3782
|
}
|
|
@@ -3635,45 +3784,11 @@ async function startDaemon(opts) {
|
|
|
3635
3784
|
}
|
|
3636
3785
|
const handler = createDispatcher({
|
|
3637
3786
|
startedAt,
|
|
3638
|
-
|
|
3639
|
-
dispatcher,
|
|
3787
|
+
pipeline,
|
|
3640
3788
|
channelManager,
|
|
3641
3789
|
relayCoordinator,
|
|
3642
3790
|
getListener: () => listenerStatus ?? buildListenerStatus(listenSpec, null),
|
|
3643
|
-
reloadChannels
|
|
3644
|
-
registerRuntimeConnection: (runtimeId, ctx) => {
|
|
3645
|
-
const timer = staleRuntimeTimers.get(runtimeId);
|
|
3646
|
-
if (timer) {
|
|
3647
|
-
clearTimeout(timer);
|
|
3648
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3649
|
-
}
|
|
3650
|
-
const previousBinding = registry.getBinding();
|
|
3651
|
-
const wasStale = previousBinding?.state === "stale";
|
|
3652
|
-
const staleSince = wasStale ? previousBinding.staleSince : null;
|
|
3653
|
-
registry.bindConnection(runtimeId, ctx.connectionId);
|
|
3654
|
-
runtimeConnections.set(runtimeId, ctx);
|
|
3655
|
-
writeGatewayTrace(
|
|
3656
|
-
`daemon registered runtime runtimeId=${runtimeId} connectionId=${ctx.connectionId}`
|
|
3657
|
-
);
|
|
3658
|
-
if (wasStale && staleSince !== null) {
|
|
3659
|
-
const newBinding = registry.getBinding();
|
|
3660
|
-
if (newBinding?.state === "active") {
|
|
3661
|
-
trackGatewayRuntimeRebind({
|
|
3662
|
-
gapMs: Date.now() - staleSince,
|
|
3663
|
-
epoch: newBinding.epoch
|
|
3664
|
-
});
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
},
|
|
3668
|
-
unregisterRuntimeConnection: (runtimeId) => {
|
|
3669
|
-
const timer = staleRuntimeTimers.get(runtimeId);
|
|
3670
|
-
if (timer) {
|
|
3671
|
-
clearTimeout(timer);
|
|
3672
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3673
|
-
}
|
|
3674
|
-
runtimeConnections.delete(runtimeId);
|
|
3675
|
-
writeGatewayTrace(`daemon unregistered runtime runtimeId=${runtimeId}`);
|
|
3676
|
-
}
|
|
3791
|
+
reloadChannels
|
|
3677
3792
|
});
|
|
3678
3793
|
let server;
|
|
3679
3794
|
let listener;
|
|
@@ -3707,39 +3822,7 @@ async function startDaemon(opts) {
|
|
|
3707
3822
|
reason: "closed",
|
|
3708
3823
|
durationMs
|
|
3709
3824
|
});
|
|
3710
|
-
|
|
3711
|
-
if (current && runtimeConnections.get(current.runtimeId)?.connectionId === ctx.connectionId) {
|
|
3712
|
-
writeGatewayTrace(
|
|
3713
|
-
`daemon runtime connection disconnected runtimeId=${current.runtimeId} connectionId=${ctx.connectionId}`
|
|
3714
|
-
);
|
|
3715
|
-
runtimeConnections.delete(current.runtimeId);
|
|
3716
|
-
const staleAt = Date.now();
|
|
3717
|
-
registry.markConnectionStale(ctx.connectionId);
|
|
3718
|
-
if (disconnectGracePeriodMs <= 0) {
|
|
3719
|
-
try {
|
|
3720
|
-
registry.unregister(current.runtimeId);
|
|
3721
|
-
} catch {
|
|
3722
|
-
}
|
|
3723
|
-
relayCoordinator.disposeAll("connection_lost");
|
|
3724
|
-
return;
|
|
3725
|
-
}
|
|
3726
|
-
const runtimeId = current.runtimeId;
|
|
3727
|
-
const timer = setTimeout(() => {
|
|
3728
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3729
|
-
const latest = registry.getCurrent();
|
|
3730
|
-
if (latest?.runtimeId === runtimeId && !registry.hasActiveBinding(runtimeId)) {
|
|
3731
|
-
try {
|
|
3732
|
-
registry.unregister(runtimeId);
|
|
3733
|
-
} catch {
|
|
3734
|
-
}
|
|
3735
|
-
relayCoordinator.disposeAll("connection_lost");
|
|
3736
|
-
trackGatewayRuntimeExpired({
|
|
3737
|
-
gapMs: Date.now() - staleAt
|
|
3738
|
-
});
|
|
3739
|
-
}
|
|
3740
|
-
}, disconnectGracePeriodMs);
|
|
3741
|
-
staleRuntimeTimers.set(runtimeId, timer);
|
|
3742
|
-
}
|
|
3825
|
+
pipeline.notifyConnectionClosed(ctx.connectionId);
|
|
3743
3826
|
}
|
|
3744
3827
|
});
|
|
3745
3828
|
if (listenSpec.kind === "tcp") {
|
|
@@ -3775,11 +3858,7 @@ async function startDaemon(opts) {
|
|
|
3775
3858
|
if (stopping) return;
|
|
3776
3859
|
stopping = true;
|
|
3777
3860
|
try {
|
|
3778
|
-
|
|
3779
|
-
for (const timer of staleRuntimeTimers.values()) {
|
|
3780
|
-
clearTimeout(timer);
|
|
3781
|
-
}
|
|
3782
|
-
staleRuntimeTimers.clear();
|
|
3861
|
+
await pipeline.stop();
|
|
3783
3862
|
relayCoordinator.disposeAll("auto_resolved");
|
|
3784
3863
|
await channelManager.stop();
|
|
3785
3864
|
await server.close();
|
|
@@ -3804,13 +3883,9 @@ async function startDaemon(opts) {
|
|
|
3804
3883
|
startedAt,
|
|
3805
3884
|
pid,
|
|
3806
3885
|
paths,
|
|
3807
|
-
|
|
3808
|
-
dispatcher,
|
|
3886
|
+
pipeline,
|
|
3809
3887
|
channelManager,
|
|
3810
3888
|
relayCoordinator,
|
|
3811
|
-
inboundQueue,
|
|
3812
|
-
outbox,
|
|
3813
|
-
outboundDispatcher,
|
|
3814
3889
|
listener,
|
|
3815
3890
|
stop
|
|
3816
3891
|
};
|