@drisp/cli 0.4.1 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{WorkflowInstallWizard-5JWVHIVJ.js → WorkflowInstallWizard-2MC5A7W4.js} +5 -3
- package/dist/athena-gateway.js +928 -974
- package/dist/chunk-2HR7FV3M.js +502 -0
- package/dist/{chunk-3FVULBV4.js → chunk-4CRZXLIP.js} +53 -117
- package/dist/{chunk-LPG5WBPV.js → chunk-5VK2ZMVV.js} +106 -629
- package/dist/chunk-6TJHAUNB.js +161 -0
- package/dist/{chunk-PSD3WBN4.js → chunk-BTKQ67RE.js} +1 -1
- package/dist/chunk-HXBCZAP7.js +1 -0
- package/dist/chunk-JAPBSL7D.js +12898 -0
- package/dist/cli.js +7000 -17642
- package/dist/dashboard-daemon.js +255 -0
- package/dist/hook-forwarder.js +2 -1
- package/package.json +5 -2
package/dist/athena-gateway.js
CHANGED
|
@@ -1,110 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadChannelSidecars,
|
|
4
|
+
loadOrCreateToken,
|
|
5
|
+
requireTokenForBind,
|
|
6
|
+
timingSafeTokenEqual
|
|
7
|
+
} from "./chunk-6TJHAUNB.js";
|
|
2
8
|
import {
|
|
3
9
|
CHANNEL_REQUEST_ID_REGEX,
|
|
4
10
|
createUdsServerTransport,
|
|
5
11
|
generateChannelRequestId,
|
|
6
12
|
isLoopbackHost,
|
|
7
13
|
isValidChannelRequestId,
|
|
8
|
-
loadOrCreateToken,
|
|
9
14
|
refreshDashboardAccessToken,
|
|
10
|
-
requireTokenForBind,
|
|
11
15
|
resolveGatewayPaths,
|
|
12
16
|
resolveListenSpec,
|
|
13
|
-
timingSafeTokenEqual,
|
|
14
17
|
traceGatewayFrame,
|
|
15
18
|
trackGatewayRuntimeExpired,
|
|
16
19
|
trackGatewayRuntimeRebind,
|
|
17
20
|
trackGatewayTransportConnect,
|
|
18
21
|
trackGatewayTransportDisconnect,
|
|
19
22
|
writeGatewayTrace
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4CRZXLIP.js";
|
|
21
24
|
|
|
22
25
|
// src/gateway/daemon.ts
|
|
23
|
-
import
|
|
24
|
-
import fs4 from "fs";
|
|
25
|
-
|
|
26
|
-
// src/infra/config/channels.ts
|
|
27
|
-
import fs from "fs";
|
|
28
|
-
import os from "os";
|
|
29
|
-
import path from "path";
|
|
30
|
-
function channelSidecarDir(home = os.homedir()) {
|
|
31
|
-
return path.join(home, ".config", "athena", "channels");
|
|
32
|
-
}
|
|
33
|
-
function loadChannelSidecars(home = os.homedir()) {
|
|
34
|
-
const dir = channelSidecarDir(home);
|
|
35
|
-
const sidecars = [];
|
|
36
|
-
const errors = [];
|
|
37
|
-
let entries;
|
|
38
|
-
try {
|
|
39
|
-
entries = fs.readdirSync(dir);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
const code = err.code;
|
|
42
|
-
if (code === "ENOENT") return { sidecars, errors };
|
|
43
|
-
errors.push({
|
|
44
|
-
path: dir,
|
|
45
|
-
reason: `read dir failed: ${err instanceof Error ? err.message : String(err)}`
|
|
46
|
-
});
|
|
47
|
-
return { sidecars, errors };
|
|
48
|
-
}
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
if (!entry.endsWith(".json")) continue;
|
|
51
|
-
const full = path.join(dir, entry);
|
|
52
|
-
const name = entry.slice(0, -".json".length);
|
|
53
|
-
const result = loadOne(name, full);
|
|
54
|
-
if (result.ok) sidecars.push(result.sidecar);
|
|
55
|
-
else errors.push({ path: full, reason: result.reason });
|
|
56
|
-
}
|
|
57
|
-
return { sidecars, errors };
|
|
58
|
-
}
|
|
59
|
-
function loadOne(name, filePath) {
|
|
60
|
-
let raw;
|
|
61
|
-
try {
|
|
62
|
-
if (process.platform !== "win32") {
|
|
63
|
-
const stat = fs.statSync(filePath);
|
|
64
|
-
if ((stat.mode & 63) !== 0) {
|
|
65
|
-
return {
|
|
66
|
-
ok: false,
|
|
67
|
-
reason: `file ${filePath} is too permissive (mode ${(stat.mode & 511).toString(8)}); chmod 600`
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
72
|
-
} catch (err) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (typeof raw !== "object" || raw === null) {
|
|
79
|
-
return { ok: false, reason: "config root must be an object" };
|
|
80
|
-
}
|
|
81
|
-
const obj = raw;
|
|
82
|
-
const userIdsRaw = obj["allowed_user_ids"];
|
|
83
|
-
const allowedUserIds = [];
|
|
84
|
-
if (userIdsRaw !== void 0) {
|
|
85
|
-
if (!Array.isArray(userIdsRaw)) {
|
|
86
|
-
return { ok: false, reason: "allowed_user_ids must be an array" };
|
|
87
|
-
}
|
|
88
|
-
for (const id of userIdsRaw) {
|
|
89
|
-
if (typeof id === "string") allowedUserIds.push(id);
|
|
90
|
-
else if (typeof id === "number") allowedUserIds.push(String(id));
|
|
91
|
-
else
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
reason: "allowed_user_ids entries must be string or number"
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const options = {};
|
|
99
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
100
|
-
if (key === "allowed_user_ids") continue;
|
|
101
|
-
options[key] = value;
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
ok: true,
|
|
105
|
-
sidecar: { name, path: filePath, allowedUserIds, options }
|
|
106
|
-
};
|
|
107
|
-
}
|
|
26
|
+
import fs3 from "fs";
|
|
108
27
|
|
|
109
28
|
// src/gateway/adapters/console/adapter.ts
|
|
110
29
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -115,6 +34,8 @@ import { WebSocket } from "ws";
|
|
|
115
34
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
116
35
|
var DEFAULT_INITIAL_RECONNECT_MS = 1e3;
|
|
117
36
|
var DEFAULT_MAX_RECONNECT_MS = 3e4;
|
|
37
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
38
|
+
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 9e4;
|
|
118
39
|
function createConsoleBrokerClient(opts) {
|
|
119
40
|
const hasStatic = typeof opts.pairingToken === "string" && opts.pairingToken.length > 0;
|
|
120
41
|
const hasProvider = typeof opts.pairingTokenProvider === "function";
|
|
@@ -126,6 +47,8 @@ function createConsoleBrokerClient(opts) {
|
|
|
126
47
|
const provider = hasStatic ? async () => opts.pairingToken : opts.pairingTokenProvider;
|
|
127
48
|
const initialDelay = opts.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_MS;
|
|
128
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;
|
|
129
52
|
let ws = null;
|
|
130
53
|
let ready = null;
|
|
131
54
|
let closeRequested = false;
|
|
@@ -133,6 +56,8 @@ function createConsoleBrokerClient(opts) {
|
|
|
133
56
|
let reconnectTimer = null;
|
|
134
57
|
let lastHello = null;
|
|
135
58
|
let currentToken = null;
|
|
59
|
+
let heartbeatTimer = null;
|
|
60
|
+
let lastPongAt = null;
|
|
136
61
|
const frameHandlers = /* @__PURE__ */ new Set();
|
|
137
62
|
const closeHandlers = /* @__PURE__ */ new Set();
|
|
138
63
|
const readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -141,6 +66,44 @@ function createConsoleBrokerClient(opts) {
|
|
|
141
66
|
if (!currentToken) return message;
|
|
142
67
|
return message.split(currentToken).join(tokenRedacted);
|
|
143
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
|
+
}
|
|
144
107
|
function emitClose(reason) {
|
|
145
108
|
for (const h of [...closeHandlers]) {
|
|
146
109
|
try {
|
|
@@ -296,6 +259,10 @@ function createConsoleBrokerClient(opts) {
|
|
|
296
259
|
);
|
|
297
260
|
return;
|
|
298
261
|
}
|
|
262
|
+
if (parsed.kind === "console.pong") {
|
|
263
|
+
lastPongAt = Date.now();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
299
266
|
for (const h of [...frameHandlers]) {
|
|
300
267
|
try {
|
|
301
268
|
h(parsed);
|
|
@@ -320,12 +287,14 @@ function createConsoleBrokerClient(opts) {
|
|
|
320
287
|
throw err;
|
|
321
288
|
}
|
|
322
289
|
next.on("close", (_code, reasonBuf) => {
|
|
290
|
+
stopHeartbeat();
|
|
323
291
|
if (next !== ws) return;
|
|
324
292
|
ws = null;
|
|
325
293
|
ready = null;
|
|
326
294
|
emitClose(reasonBuf.toString() || "closed");
|
|
327
295
|
if (!closeRequested) scheduleReconnect();
|
|
328
296
|
});
|
|
297
|
+
startHeartbeat(next);
|
|
329
298
|
}
|
|
330
299
|
async function connect(hello) {
|
|
331
300
|
if (ws) throw new Error("console broker client already connected");
|
|
@@ -335,6 +304,7 @@ function createConsoleBrokerClient(opts) {
|
|
|
335
304
|
}
|
|
336
305
|
function close(reason) {
|
|
337
306
|
closeRequested = true;
|
|
307
|
+
stopHeartbeat();
|
|
338
308
|
if (reconnectTimer) {
|
|
339
309
|
clearTimeout(reconnectTimer);
|
|
340
310
|
reconnectTimer = null;
|
|
@@ -461,18 +431,12 @@ var ConsoleAdapter = class {
|
|
|
461
431
|
runnerId: this.opts.runnerId,
|
|
462
432
|
...workspaceId.length > 0 ? { workspaceId } : {},
|
|
463
433
|
...msg.location.peer?.id !== void 0 ? { userId: msg.location.peer.id } : {},
|
|
464
|
-
...msg.location.thread?.id !== void 0 ? {
|
|
465
|
-
conversationId: msg.location.thread.id,
|
|
466
|
-
threadId: msg.location.thread.id
|
|
467
|
-
} : {}
|
|
434
|
+
...msg.location.thread?.id !== void 0 ? { threadId: msg.location.thread.id } : {}
|
|
468
435
|
},
|
|
469
436
|
messageId,
|
|
470
437
|
idempotencyKey: msg.idempotencyKey,
|
|
471
438
|
text: msg.text
|
|
472
439
|
};
|
|
473
|
-
writeGatewayTrace(
|
|
474
|
-
`consoleAdapter send message.out runner=${this.opts.runnerId} workspace=${frame.address.workspaceId ?? ""} conversation=${frame.address.conversationId ?? ""} thread=${frame.address.threadId ?? ""} user=${frame.address.userId ?? ""} textLength=${msg.text.length} idempotencyKey=${msg.idempotencyKey}`
|
|
475
|
-
);
|
|
476
440
|
client.sendFrame(frame);
|
|
477
441
|
return {
|
|
478
442
|
providerMessageId: messageId,
|
|
@@ -1959,8 +1923,7 @@ var ChannelManager = class {
|
|
|
1959
1923
|
// src/gateway/control/handlers.ts
|
|
1960
1924
|
import { createRequire } from "module";
|
|
1961
1925
|
|
|
1962
|
-
// src/gateway/
|
|
1963
|
-
import { randomUUID } from "crypto";
|
|
1926
|
+
// src/gateway/runtimeBindingStore.ts
|
|
1964
1927
|
var AlreadyRegisteredError = class extends Error {
|
|
1965
1928
|
code = "already_registered";
|
|
1966
1929
|
constructor(existing) {
|
|
@@ -1977,120 +1940,145 @@ var NotRegisteredError = class extends Error {
|
|
|
1977
1940
|
this.name = "NotRegisteredError";
|
|
1978
1941
|
}
|
|
1979
1942
|
};
|
|
1980
|
-
var UnknownDispatchError = class extends Error {
|
|
1981
|
-
code = "unknown_dispatch";
|
|
1982
|
-
constructor(id) {
|
|
1983
|
-
super(`unknown dispatchId: ${id}`);
|
|
1984
|
-
this.name = "UnknownDispatchError";
|
|
1985
|
-
}
|
|
1986
|
-
};
|
|
1987
1943
|
function maybeLastRebindAt(value) {
|
|
1988
1944
|
return value !== void 0 ? { lastRebindAt: value } : {};
|
|
1989
1945
|
}
|
|
1990
|
-
var
|
|
1946
|
+
var RuntimeBindingStore = class {
|
|
1991
1947
|
current = null;
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1948
|
+
bindingState = null;
|
|
1949
|
+
staleTimer = null;
|
|
1950
|
+
staleSince = null;
|
|
1951
|
+
gracePeriodMs;
|
|
1952
|
+
observers;
|
|
1995
1953
|
now;
|
|
1996
1954
|
constructor(opts = {}) {
|
|
1997
|
-
this.
|
|
1955
|
+
this.gracePeriodMs = opts.gracePeriodMs ?? 0;
|
|
1956
|
+
this.observers = opts.observers ?? {};
|
|
1998
1957
|
this.now = opts.now ?? Date.now;
|
|
1999
1958
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1959
|
+
// ── lifecycle ─────────────────────────────────────────────
|
|
1960
|
+
/** Register a runtime and bind its connection in one atomic step. */
|
|
1961
|
+
bind(input) {
|
|
1962
|
+
const previous = this.bindingState;
|
|
1963
|
+
const wasStale = previous?.state === "stale";
|
|
1964
|
+
const staleSince = wasStale ? previous.staleSince : null;
|
|
1965
|
+
if (!this.current) {
|
|
1966
|
+
this.current = {
|
|
1967
|
+
runtimeId: input.runtimeId,
|
|
1968
|
+
defaultAgentId: input.defaultAgentId,
|
|
1969
|
+
pid: input.pid,
|
|
1970
|
+
registeredAt: this.now()
|
|
1971
|
+
};
|
|
1972
|
+
} else if (this.current.runtimeId === input.runtimeId) {
|
|
1973
|
+
this.current = {
|
|
1974
|
+
...this.current,
|
|
1975
|
+
defaultAgentId: input.defaultAgentId,
|
|
1976
|
+
pid: input.pid
|
|
1977
|
+
};
|
|
1978
|
+
} else {
|
|
2010
1979
|
throw new AlreadyRegisteredError(this.current);
|
|
2011
1980
|
}
|
|
2012
|
-
this.current = { ...input, registeredAt: this.now() };
|
|
2013
|
-
return this.current;
|
|
2014
|
-
}
|
|
2015
|
-
bindConnection(runtimeId, connectionId) {
|
|
2016
|
-
if (!this.current || this.current.runtimeId !== runtimeId) {
|
|
2017
|
-
throw new NotRegisteredError();
|
|
2018
|
-
}
|
|
2019
|
-
const previous = this.binding;
|
|
2020
1981
|
const now = this.now();
|
|
2021
|
-
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== connectionId);
|
|
1982
|
+
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== input.connectionId);
|
|
2022
1983
|
const lastRebindAt = isRebind ? now : previous?.lastRebindAt;
|
|
2023
1984
|
const epoch = previous ? previous.epoch + (isRebind ? 1 : 0) : 1;
|
|
2024
|
-
this.
|
|
1985
|
+
this.bindingState = {
|
|
2025
1986
|
state: "active",
|
|
2026
|
-
connectionId,
|
|
1987
|
+
connectionId: input.connectionId,
|
|
2027
1988
|
boundAt: now,
|
|
2028
1989
|
epoch,
|
|
2029
1990
|
...maybeLastRebindAt(lastRebindAt)
|
|
2030
1991
|
};
|
|
1992
|
+
this.clearStaleTimer();
|
|
1993
|
+
if (wasStale && staleSince !== null) {
|
|
1994
|
+
this.observers.onRuntimeRebind?.({
|
|
1995
|
+
runtimeId: input.runtimeId,
|
|
1996
|
+
gapMs: now - staleSince,
|
|
1997
|
+
epoch: this.bindingState.epoch
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
return { registeredAt: this.current.registeredAt };
|
|
2001
|
+
}
|
|
2002
|
+
/** Fully unregister a runtime. Throws NotRegisteredError if id does not match. */
|
|
2003
|
+
unbind(runtimeId) {
|
|
2004
|
+
if (!this.current || this.current.runtimeId !== runtimeId) {
|
|
2005
|
+
throw new NotRegisteredError();
|
|
2006
|
+
}
|
|
2007
|
+
this.current = null;
|
|
2008
|
+
this.bindingState = null;
|
|
2009
|
+
this.clearStaleTimer();
|
|
2031
2010
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2011
|
+
/**
|
|
2012
|
+
* Called when the transport connection closes.
|
|
2013
|
+
* Returns the runtimeId if the close was for the current binding (caller should
|
|
2014
|
+
* clear the push handle); returns null if the connectionId was not recognised.
|
|
2015
|
+
*/
|
|
2016
|
+
notifyConnectionClosed(connectionId) {
|
|
2017
|
+
if (!this.current || !this.bindingState || this.bindingState.connectionId !== connectionId) {
|
|
2034
2018
|
return null;
|
|
2035
2019
|
}
|
|
2036
|
-
|
|
2020
|
+
const runtimeId = this.current.runtimeId;
|
|
2021
|
+
const now = this.now();
|
|
2022
|
+
this.bindingState = {
|
|
2037
2023
|
state: "stale",
|
|
2038
2024
|
connectionId,
|
|
2039
|
-
staleSince:
|
|
2040
|
-
epoch: this.
|
|
2041
|
-
...maybeLastRebindAt(this.
|
|
2025
|
+
staleSince: now,
|
|
2026
|
+
epoch: this.bindingState.epoch,
|
|
2027
|
+
...maybeLastRebindAt(this.bindingState.lastRebindAt)
|
|
2042
2028
|
};
|
|
2043
|
-
|
|
2029
|
+
if (this.gracePeriodMs <= 0) {
|
|
2030
|
+
this.current = null;
|
|
2031
|
+
this.bindingState = null;
|
|
2032
|
+
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2033
|
+
return runtimeId;
|
|
2034
|
+
}
|
|
2035
|
+
this.staleSince = now;
|
|
2036
|
+
this.staleTimer = setTimeout(() => {
|
|
2037
|
+
this.expireStaleBinding(runtimeId);
|
|
2038
|
+
}, this.gracePeriodMs);
|
|
2039
|
+
return runtimeId;
|
|
2040
|
+
}
|
|
2041
|
+
stop() {
|
|
2042
|
+
this.clearStaleTimer();
|
|
2044
2043
|
}
|
|
2044
|
+
// ── reads ─────────────────────────────────────────────────
|
|
2045
2045
|
hasActiveBinding(runtimeId) {
|
|
2046
|
-
if (!this.current || !this.
|
|
2046
|
+
if (!this.current || !this.bindingState || this.bindingState.state !== "active") {
|
|
2047
2047
|
return false;
|
|
2048
2048
|
}
|
|
2049
2049
|
return runtimeId === void 0 || this.current.runtimeId === runtimeId;
|
|
2050
2050
|
}
|
|
2051
|
+
getCurrent() {
|
|
2052
|
+
return this.current;
|
|
2053
|
+
}
|
|
2051
2054
|
getBinding() {
|
|
2052
|
-
return this.
|
|
2055
|
+
return this.bindingState;
|
|
2053
2056
|
}
|
|
2054
2057
|
getRuntimeIdByConnection(connectionId) {
|
|
2055
|
-
if (!this.current || !this.
|
|
2056
|
-
return this.
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2058
|
+
if (!this.current || !this.bindingState) return null;
|
|
2059
|
+
return this.bindingState.connectionId === connectionId ? this.current.runtimeId : null;
|
|
2060
|
+
}
|
|
2061
|
+
// ── private ───────────────────────────────────────────────
|
|
2062
|
+
expireStaleBinding(runtimeId) {
|
|
2063
|
+
this.staleTimer = null;
|
|
2064
|
+
const since = this.staleSince;
|
|
2065
|
+
this.staleSince = null;
|
|
2066
|
+
if (!this.current || this.current.runtimeId !== runtimeId || this.hasActiveBinding(runtimeId)) {
|
|
2067
|
+
return;
|
|
2061
2068
|
}
|
|
2062
2069
|
this.current = null;
|
|
2063
|
-
this.
|
|
2064
|
-
this.
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
return this.current;
|
|
2068
|
-
}
|
|
2069
|
-
beginDispatch(input) {
|
|
2070
|
-
if (!this.current) {
|
|
2071
|
-
throw new NotRegisteredError();
|
|
2070
|
+
this.bindingState = null;
|
|
2071
|
+
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2072
|
+
if (since !== null) {
|
|
2073
|
+
this.observers.onRuntimeExpired?.({ runtimeId, gapMs: this.now() - since });
|
|
2072
2074
|
}
|
|
2073
|
-
const dispatchId = this.idFactory();
|
|
2074
|
-
const entry = {
|
|
2075
|
-
dispatchId,
|
|
2076
|
-
sessionKey: input.sessionKey,
|
|
2077
|
-
agentId: input.agentId,
|
|
2078
|
-
location: input.location,
|
|
2079
|
-
createdAt: this.now()
|
|
2080
|
-
};
|
|
2081
|
-
this.dispatches.set(dispatchId, entry);
|
|
2082
|
-
return entry;
|
|
2083
2075
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2076
|
+
clearStaleTimer() {
|
|
2077
|
+
if (this.staleTimer) {
|
|
2078
|
+
clearTimeout(this.staleTimer);
|
|
2079
|
+
this.staleTimer = null;
|
|
2088
2080
|
}
|
|
2089
|
-
this.
|
|
2090
|
-
return entry;
|
|
2091
|
-
}
|
|
2092
|
-
pendingDispatchCount() {
|
|
2093
|
-
return this.dispatches.size;
|
|
2081
|
+
this.staleSince = null;
|
|
2094
2082
|
}
|
|
2095
2083
|
};
|
|
2096
2084
|
|
|
@@ -2100,7 +2088,7 @@ var cachedVersion = null;
|
|
|
2100
2088
|
function readVersion() {
|
|
2101
2089
|
if (cachedVersion !== null) return cachedVersion;
|
|
2102
2090
|
try {
|
|
2103
|
-
const injected = "0.4.
|
|
2091
|
+
const injected = "0.4.4";
|
|
2104
2092
|
if (typeof injected === "string" && injected.length > 0) {
|
|
2105
2093
|
cachedVersion = injected;
|
|
2106
2094
|
return cachedVersion;
|
|
@@ -2115,273 +2103,258 @@ function readVersion() {
|
|
|
2115
2103
|
}
|
|
2116
2104
|
return cachedVersion;
|
|
2117
2105
|
}
|
|
2106
|
+
var PING = {
|
|
2107
|
+
kind: "ping",
|
|
2108
|
+
handle: (_envelope, { ts, deps }) => {
|
|
2109
|
+
const payload = {
|
|
2110
|
+
pong: true,
|
|
2111
|
+
daemonPid: process.pid,
|
|
2112
|
+
uptimeMs: ts - deps.startedAt
|
|
2113
|
+
};
|
|
2114
|
+
return payload;
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
var STATUS = {
|
|
2118
|
+
kind: "status",
|
|
2119
|
+
handle: (_envelope, { ts, deps }) => {
|
|
2120
|
+
const channels = (deps.channelManager?.listChannels() ?? []).map((c) => ({
|
|
2121
|
+
id: c.id,
|
|
2122
|
+
state: c.health?.transportOk === false ? "degraded" : "running",
|
|
2123
|
+
...c.health?.at !== void 0 ? { lastHealthAt: c.health.at } : {},
|
|
2124
|
+
...c.health?.note !== void 0 ? { note: c.health.note } : {}
|
|
2125
|
+
}));
|
|
2126
|
+
const payload = {
|
|
2127
|
+
daemonPid: process.pid,
|
|
2128
|
+
startedAt: deps.startedAt,
|
|
2129
|
+
uptimeMs: ts - deps.startedAt,
|
|
2130
|
+
version: readVersion(),
|
|
2131
|
+
listener: deps.getListener?.() ?? {
|
|
2132
|
+
kind: "uds",
|
|
2133
|
+
socketPath: "<unknown>"
|
|
2134
|
+
},
|
|
2135
|
+
channels,
|
|
2136
|
+
runtimes: runtimeStatusEntries(deps.pipeline)
|
|
2137
|
+
};
|
|
2138
|
+
return payload;
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
var CHANNELS_RELOAD = {
|
|
2142
|
+
kind: "channels.reload",
|
|
2143
|
+
requires: ["reloadChannels"],
|
|
2144
|
+
unsupportedMessage: "channel reload not configured",
|
|
2145
|
+
handle: async (_envelope, { deps }) => {
|
|
2146
|
+
const payload = await deps.reloadChannels();
|
|
2147
|
+
return payload;
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
var SESSION_REGISTER = {
|
|
2151
|
+
kind: "session.register",
|
|
2152
|
+
requires: ["pipeline"],
|
|
2153
|
+
unsupportedMessage: "session.register not configured",
|
|
2154
|
+
handle: (envelope, { deps, connection }) => {
|
|
2155
|
+
const req = envelope.payload;
|
|
2156
|
+
const reg = deps.pipeline.registerRuntime({
|
|
2157
|
+
runtimeId: req.runtimeId,
|
|
2158
|
+
defaultAgentId: req.defaultAgentId,
|
|
2159
|
+
pid: req.pid,
|
|
2160
|
+
connectionId: connection.connectionId,
|
|
2161
|
+
push: connection.push
|
|
2162
|
+
});
|
|
2163
|
+
const payload = {
|
|
2164
|
+
registeredAt: reg.registeredAt,
|
|
2165
|
+
gatewayStartedAt: deps.startedAt
|
|
2166
|
+
};
|
|
2167
|
+
return payload;
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
var SESSION_UNREGISTER = {
|
|
2171
|
+
kind: "session.unregister",
|
|
2172
|
+
requires: ["pipeline"],
|
|
2173
|
+
unsupportedMessage: "session.unregister not configured",
|
|
2174
|
+
handle: (envelope, { deps, ts }) => {
|
|
2175
|
+
const req = envelope.payload;
|
|
2176
|
+
deps.pipeline.unregisterRuntime(req.runtimeId);
|
|
2177
|
+
const payload = {
|
|
2178
|
+
unregisteredAt: ts
|
|
2179
|
+
};
|
|
2180
|
+
return payload;
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
var SESSION_TURN_COMPLETE = {
|
|
2184
|
+
kind: "session.turn.complete",
|
|
2185
|
+
requires: ["pipeline"],
|
|
2186
|
+
unsupportedMessage: "pipeline not configured",
|
|
2187
|
+
handle: async (envelope, { deps }) => {
|
|
2188
|
+
const req = envelope.payload;
|
|
2189
|
+
return await deps.pipeline.handleTurnComplete(req);
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
var CHANNEL_SEND = {
|
|
2193
|
+
kind: "channel.send",
|
|
2194
|
+
requires: ["channelManager"],
|
|
2195
|
+
unsupportedMessage: "channel manager not configured",
|
|
2196
|
+
handle: async (envelope, { deps }) => {
|
|
2197
|
+
const req = envelope.payload;
|
|
2198
|
+
const result = await deps.channelManager.send(
|
|
2199
|
+
req.message.location.channelId,
|
|
2200
|
+
req.message
|
|
2201
|
+
);
|
|
2202
|
+
const payload = {
|
|
2203
|
+
providerMessageId: result.providerMessageId,
|
|
2204
|
+
deliveredAt: result.deliveredAt
|
|
2205
|
+
};
|
|
2206
|
+
return payload;
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var RELAY_PERMISSION_REQUEST = {
|
|
2210
|
+
kind: "relay.permission.request",
|
|
2211
|
+
requires: ["relayCoordinator"],
|
|
2212
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2213
|
+
requireRegisteredRuntime: true,
|
|
2214
|
+
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2215
|
+
const req = envelope.payload;
|
|
2216
|
+
const broadcast = deps.relayCoordinator.requestPermission({
|
|
2217
|
+
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2218
|
+
toolName: req.toolName,
|
|
2219
|
+
description: req.description,
|
|
2220
|
+
inputPreview: req.inputPreview,
|
|
2221
|
+
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2222
|
+
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2223
|
+
});
|
|
2224
|
+
const result = await broadcast.result;
|
|
2225
|
+
const payload = {
|
|
2226
|
+
channelRequestId: broadcast.channelRequestId,
|
|
2227
|
+
result
|
|
2228
|
+
};
|
|
2229
|
+
return payload;
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
var RELAY_PERMISSION_CANCEL = {
|
|
2233
|
+
kind: "relay.permission.cancel",
|
|
2234
|
+
requires: ["relayCoordinator"],
|
|
2235
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2236
|
+
requireRegisteredRuntime: true,
|
|
2237
|
+
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2238
|
+
const req = envelope.payload;
|
|
2239
|
+
const cancelled = deps.relayCoordinator.cancel(
|
|
2240
|
+
req.channelRequestId,
|
|
2241
|
+
req.reason,
|
|
2242
|
+
callerRuntimeId
|
|
2243
|
+
);
|
|
2244
|
+
const payload = { cancelled };
|
|
2245
|
+
return payload;
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
var RELAY_QUESTION_REQUEST = {
|
|
2249
|
+
kind: "relay.question.request",
|
|
2250
|
+
requires: ["relayCoordinator"],
|
|
2251
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2252
|
+
requireRegisteredRuntime: true,
|
|
2253
|
+
handle: async (envelope, { deps, callerRuntimeId }) => {
|
|
2254
|
+
const req = envelope.payload;
|
|
2255
|
+
const broadcast = deps.relayCoordinator.requestQuestion({
|
|
2256
|
+
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2257
|
+
title: req.title,
|
|
2258
|
+
questions: req.questions,
|
|
2259
|
+
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2260
|
+
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2261
|
+
});
|
|
2262
|
+
const result = await broadcast.result;
|
|
2263
|
+
const payload = {
|
|
2264
|
+
channelRequestId: broadcast.channelRequestId,
|
|
2265
|
+
result
|
|
2266
|
+
};
|
|
2267
|
+
return payload;
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
var RELAY_QUESTION_CANCEL = {
|
|
2271
|
+
kind: "relay.question.cancel",
|
|
2272
|
+
requires: ["relayCoordinator"],
|
|
2273
|
+
unsupportedMessage: "relay coordinator not configured",
|
|
2274
|
+
requireRegisteredRuntime: true,
|
|
2275
|
+
handle: (envelope, { deps, callerRuntimeId }) => {
|
|
2276
|
+
const req = envelope.payload;
|
|
2277
|
+
const cancelled = deps.relayCoordinator.cancel(
|
|
2278
|
+
req.channelRequestId,
|
|
2279
|
+
req.reason,
|
|
2280
|
+
callerRuntimeId
|
|
2281
|
+
);
|
|
2282
|
+
const payload = { cancelled };
|
|
2283
|
+
return payload;
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
var HANDLERS = new Map(
|
|
2287
|
+
[
|
|
2288
|
+
PING,
|
|
2289
|
+
STATUS,
|
|
2290
|
+
CHANNELS_RELOAD,
|
|
2291
|
+
SESSION_REGISTER,
|
|
2292
|
+
SESSION_UNREGISTER,
|
|
2293
|
+
SESSION_TURN_COMPLETE,
|
|
2294
|
+
CHANNEL_SEND,
|
|
2295
|
+
RELAY_PERMISSION_REQUEST,
|
|
2296
|
+
RELAY_PERMISSION_CANCEL,
|
|
2297
|
+
RELAY_QUESTION_REQUEST,
|
|
2298
|
+
RELAY_QUESTION_CANCEL
|
|
2299
|
+
].map((spec) => [spec.kind, spec])
|
|
2300
|
+
);
|
|
2118
2301
|
function createDispatcher(deps) {
|
|
2119
2302
|
const handle = async (envelope, connection) => {
|
|
2120
2303
|
const ts = Date.now();
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
state: c.health?.transportOk === false ? "degraded" : "running",
|
|
2134
|
-
...c.health?.at !== void 0 ? { lastHealthAt: c.health.at } : {},
|
|
2135
|
-
...c.health?.note !== void 0 ? { note: c.health.note } : {}
|
|
2136
|
-
}));
|
|
2137
|
-
const payload = {
|
|
2138
|
-
daemonPid: process.pid,
|
|
2139
|
-
startedAt: deps.startedAt,
|
|
2140
|
-
uptimeMs: ts - deps.startedAt,
|
|
2141
|
-
version: readVersion(),
|
|
2142
|
-
listener: deps.getListener?.() ?? {
|
|
2143
|
-
kind: "uds",
|
|
2144
|
-
socketPath: "<unknown>"
|
|
2145
|
-
},
|
|
2146
|
-
channels,
|
|
2147
|
-
runtimes: runtimeStatusEntries(deps.registry)
|
|
2148
|
-
};
|
|
2149
|
-
return ok(envelope, ts, payload);
|
|
2150
|
-
}
|
|
2151
|
-
case "channels.reload": {
|
|
2152
|
-
if (!deps.reloadChannels) {
|
|
2153
|
-
return error(
|
|
2154
|
-
envelope,
|
|
2155
|
-
ts,
|
|
2156
|
-
"unsupported",
|
|
2157
|
-
"channel reload not configured"
|
|
2158
|
-
);
|
|
2159
|
-
}
|
|
2160
|
-
const payload = await deps.reloadChannels();
|
|
2161
|
-
return ok(envelope, ts, payload);
|
|
2162
|
-
}
|
|
2163
|
-
case "session.register": {
|
|
2164
|
-
if (!deps.registry)
|
|
2165
|
-
return error(
|
|
2166
|
-
envelope,
|
|
2167
|
-
ts,
|
|
2168
|
-
"unsupported",
|
|
2169
|
-
"session.register not configured"
|
|
2170
|
-
);
|
|
2171
|
-
const req = envelope.payload;
|
|
2172
|
-
try {
|
|
2173
|
-
const reg = deps.registry.register({
|
|
2174
|
-
runtimeId: req.runtimeId,
|
|
2175
|
-
defaultAgentId: req.defaultAgentId,
|
|
2176
|
-
pid: req.pid
|
|
2177
|
-
});
|
|
2178
|
-
deps.registerRuntimeConnection?.(req.runtimeId, connection);
|
|
2179
|
-
try {
|
|
2180
|
-
deps.dispatcher?.drainPending();
|
|
2181
|
-
} catch (err) {
|
|
2182
|
-
process.stderr.write(
|
|
2183
|
-
`gateway: drainPending failed: ${err instanceof Error ? err.message : String(err)}
|
|
2184
|
-
`
|
|
2185
|
-
);
|
|
2186
|
-
}
|
|
2187
|
-
const payload = {
|
|
2188
|
-
registeredAt: reg.registeredAt,
|
|
2189
|
-
gatewayStartedAt: deps.startedAt
|
|
2190
|
-
};
|
|
2191
|
-
return ok(envelope, ts, payload);
|
|
2192
|
-
} catch (err) {
|
|
2193
|
-
if (err instanceof AlreadyRegisteredError) {
|
|
2194
|
-
return error(envelope, ts, err.code, err.message);
|
|
2195
|
-
}
|
|
2196
|
-
throw err;
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
case "session.unregister": {
|
|
2200
|
-
if (!deps.registry)
|
|
2304
|
+
const spec = HANDLERS.get(envelope.kind);
|
|
2305
|
+
if (!spec) {
|
|
2306
|
+
return error(
|
|
2307
|
+
envelope,
|
|
2308
|
+
ts,
|
|
2309
|
+
"unknown_kind",
|
|
2310
|
+
`unknown kind: ${envelope.kind}`
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
if (spec.requires) {
|
|
2314
|
+
for (const name of spec.requires) {
|
|
2315
|
+
if (deps[name] === void 0) {
|
|
2201
2316
|
return error(
|
|
2202
2317
|
envelope,
|
|
2203
2318
|
ts,
|
|
2204
2319
|
"unsupported",
|
|
2205
|
-
|
|
2320
|
+
spec.unsupportedMessage ?? `${spec.kind} not configured`
|
|
2206
2321
|
);
|
|
2207
|
-
const req = envelope.payload;
|
|
2208
|
-
try {
|
|
2209
|
-
deps.registry.unregister(req.runtimeId);
|
|
2210
|
-
deps.unregisterRuntimeConnection?.(req.runtimeId);
|
|
2211
|
-
const payload = {
|
|
2212
|
-
unregisteredAt: ts
|
|
2213
|
-
};
|
|
2214
|
-
return ok(envelope, ts, payload);
|
|
2215
|
-
} catch (err) {
|
|
2216
|
-
if (err instanceof NotRegisteredError) {
|
|
2217
|
-
return error(envelope, ts, err.code, err.message);
|
|
2218
|
-
}
|
|
2219
|
-
throw err;
|
|
2220
2322
|
}
|
|
2221
2323
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
return ok(envelope, ts, result);
|
|
2233
|
-
}
|
|
2234
|
-
case "channel.send": {
|
|
2235
|
-
if (!deps.channelManager)
|
|
2236
|
-
return error(
|
|
2237
|
-
envelope,
|
|
2238
|
-
ts,
|
|
2239
|
-
"unsupported",
|
|
2240
|
-
"channel manager not configured"
|
|
2241
|
-
);
|
|
2242
|
-
const req = envelope.payload;
|
|
2243
|
-
const result = await deps.channelManager.send(
|
|
2244
|
-
req.message.location.channelId,
|
|
2245
|
-
req.message
|
|
2324
|
+
}
|
|
2325
|
+
let callerRuntimeId;
|
|
2326
|
+
if (spec.requireRegisteredRuntime) {
|
|
2327
|
+
callerRuntimeId = deps.pipeline?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2328
|
+
if (deps.pipeline && callerRuntimeId === void 0) {
|
|
2329
|
+
return error(
|
|
2330
|
+
envelope,
|
|
2331
|
+
ts,
|
|
2332
|
+
"not_registered",
|
|
2333
|
+
`${spec.kind} requires a registered runtime connection`
|
|
2246
2334
|
);
|
|
2247
|
-
const payload = {
|
|
2248
|
-
providerMessageId: result.providerMessageId,
|
|
2249
|
-
deliveredAt: result.deliveredAt
|
|
2250
|
-
};
|
|
2251
|
-
return ok(envelope, ts, payload);
|
|
2252
|
-
}
|
|
2253
|
-
case "relay.permission.request": {
|
|
2254
|
-
if (!deps.relayCoordinator)
|
|
2255
|
-
return error(
|
|
2256
|
-
envelope,
|
|
2257
|
-
ts,
|
|
2258
|
-
"unsupported",
|
|
2259
|
-
"relay coordinator not configured"
|
|
2260
|
-
);
|
|
2261
|
-
const req = envelope.payload;
|
|
2262
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2263
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2264
|
-
return error(
|
|
2265
|
-
envelope,
|
|
2266
|
-
ts,
|
|
2267
|
-
"not_registered",
|
|
2268
|
-
"relay.permission.request requires a registered runtime connection"
|
|
2269
|
-
);
|
|
2270
|
-
}
|
|
2271
|
-
const broadcast = deps.relayCoordinator.requestPermission({
|
|
2272
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2273
|
-
toolName: req.toolName,
|
|
2274
|
-
description: req.description,
|
|
2275
|
-
inputPreview: req.inputPreview,
|
|
2276
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2277
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2278
|
-
});
|
|
2279
|
-
const result = await broadcast.result;
|
|
2280
|
-
const payload = {
|
|
2281
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2282
|
-
result
|
|
2283
|
-
};
|
|
2284
|
-
return ok(envelope, Date.now(), payload);
|
|
2285
2335
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
envelope,
|
|
2299
|
-
ts,
|
|
2300
|
-
"not_registered",
|
|
2301
|
-
"relay.permission.cancel requires a registered runtime connection"
|
|
2302
|
-
);
|
|
2303
|
-
}
|
|
2304
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2305
|
-
req.channelRequestId,
|
|
2306
|
-
req.reason,
|
|
2307
|
-
callerRuntimeId
|
|
2308
|
-
);
|
|
2309
|
-
const payload = { cancelled };
|
|
2310
|
-
return ok(envelope, ts, payload);
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
const payload = await spec.handle(envelope, {
|
|
2339
|
+
deps,
|
|
2340
|
+
connection,
|
|
2341
|
+
callerRuntimeId,
|
|
2342
|
+
ts
|
|
2343
|
+
});
|
|
2344
|
+
return ok(envelope, Date.now(), payload);
|
|
2345
|
+
} catch (err) {
|
|
2346
|
+
if (err instanceof AlreadyRegisteredError || err instanceof NotRegisteredError) {
|
|
2347
|
+
return error(envelope, ts, err.code, err.message);
|
|
2311
2348
|
}
|
|
2312
|
-
|
|
2313
|
-
if (!deps.relayCoordinator)
|
|
2314
|
-
return error(
|
|
2315
|
-
envelope,
|
|
2316
|
-
ts,
|
|
2317
|
-
"unsupported",
|
|
2318
|
-
"relay coordinator not configured"
|
|
2319
|
-
);
|
|
2320
|
-
const req = envelope.payload;
|
|
2321
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2322
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2323
|
-
return error(
|
|
2324
|
-
envelope,
|
|
2325
|
-
ts,
|
|
2326
|
-
"not_registered",
|
|
2327
|
-
"relay.question.request requires a registered runtime connection"
|
|
2328
|
-
);
|
|
2329
|
-
}
|
|
2330
|
-
const broadcast = deps.relayCoordinator.requestQuestion({
|
|
2331
|
-
...req.channelRequestId !== void 0 ? { channelRequestId: req.channelRequestId } : {},
|
|
2332
|
-
title: req.title,
|
|
2333
|
-
questions: req.questions,
|
|
2334
|
-
...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {},
|
|
2335
|
-
...callerRuntimeId !== void 0 ? { runtimeId: callerRuntimeId } : {}
|
|
2336
|
-
});
|
|
2337
|
-
const result = await broadcast.result;
|
|
2338
|
-
const payload = {
|
|
2339
|
-
channelRequestId: broadcast.channelRequestId,
|
|
2340
|
-
result
|
|
2341
|
-
};
|
|
2342
|
-
return ok(envelope, Date.now(), payload);
|
|
2343
|
-
}
|
|
2344
|
-
case "relay.question.cancel": {
|
|
2345
|
-
if (!deps.relayCoordinator)
|
|
2346
|
-
return error(
|
|
2347
|
-
envelope,
|
|
2348
|
-
ts,
|
|
2349
|
-
"unsupported",
|
|
2350
|
-
"relay coordinator not configured"
|
|
2351
|
-
);
|
|
2352
|
-
const req = envelope.payload;
|
|
2353
|
-
const callerRuntimeId = deps.registry?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2354
|
-
if (deps.registry && callerRuntimeId === void 0) {
|
|
2355
|
-
return error(
|
|
2356
|
-
envelope,
|
|
2357
|
-
ts,
|
|
2358
|
-
"not_registered",
|
|
2359
|
-
"relay.question.cancel requires a registered runtime connection"
|
|
2360
|
-
);
|
|
2361
|
-
}
|
|
2362
|
-
const cancelled = deps.relayCoordinator.cancel(
|
|
2363
|
-
req.channelRequestId,
|
|
2364
|
-
req.reason,
|
|
2365
|
-
callerRuntimeId
|
|
2366
|
-
);
|
|
2367
|
-
const payload = { cancelled };
|
|
2368
|
-
return ok(envelope, ts, payload);
|
|
2369
|
-
}
|
|
2370
|
-
default:
|
|
2371
|
-
return error(
|
|
2372
|
-
envelope,
|
|
2373
|
-
ts,
|
|
2374
|
-
"unknown_kind",
|
|
2375
|
-
`unknown kind: ${envelope.kind}`
|
|
2376
|
-
);
|
|
2349
|
+
throw err;
|
|
2377
2350
|
}
|
|
2378
2351
|
};
|
|
2379
2352
|
return handle;
|
|
2380
2353
|
}
|
|
2381
|
-
function runtimeStatusEntries(
|
|
2382
|
-
const runtime =
|
|
2383
|
-
if (!runtime || !
|
|
2384
|
-
const binding =
|
|
2354
|
+
function runtimeStatusEntries(pipeline) {
|
|
2355
|
+
const runtime = pipeline?.getCurrentRuntime();
|
|
2356
|
+
if (!runtime || !pipeline) return [];
|
|
2357
|
+
const binding = pipeline.getBinding();
|
|
2385
2358
|
return [
|
|
2386
2359
|
{
|
|
2387
2360
|
runtimeId: runtime.runtimeId,
|
|
@@ -2399,7 +2372,7 @@ function runtimeStatusEntries(registry) {
|
|
|
2399
2372
|
epoch: binding.epoch,
|
|
2400
2373
|
...maybeLastRebindAt(binding.lastRebindAt)
|
|
2401
2374
|
} : { state: "none" },
|
|
2402
|
-
pendingDispatchCount:
|
|
2375
|
+
pendingDispatchCount: pipeline.pendingDispatchCount()
|
|
2403
2376
|
}
|
|
2404
2377
|
];
|
|
2405
2378
|
}
|
|
@@ -2522,256 +2495,8 @@ function handleConnection(connection, expectedToken, startedAt, handler, logErro
|
|
|
2522
2495
|
});
|
|
2523
2496
|
}
|
|
2524
2497
|
|
|
2525
|
-
// src/gateway/
|
|
2526
|
-
|
|
2527
|
-
const c = loc.channelId;
|
|
2528
|
-
const a = loc.accountId;
|
|
2529
|
-
if (loc.peer?.id) {
|
|
2530
|
-
const peer = loc.peer.id;
|
|
2531
|
-
if (loc.thread?.id) {
|
|
2532
|
-
return `peer:${c}:${a}:${peer}:${loc.thread.id}`;
|
|
2533
|
-
}
|
|
2534
|
-
return `peer:${c}:${a}:${peer}`;
|
|
2535
|
-
}
|
|
2536
|
-
if (loc.room?.id) {
|
|
2537
|
-
const room = loc.room.id;
|
|
2538
|
-
if (loc.thread?.id) {
|
|
2539
|
-
return `room:${c}:${a}:${room}:${loc.thread.id}`;
|
|
2540
|
-
}
|
|
2541
|
-
return `room:${c}:${a}:${room}`;
|
|
2542
|
-
}
|
|
2543
|
-
return `default:${c}:${a}`;
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
// src/gateway/dispatcher.ts
|
|
2547
|
-
var Dispatcher = class {
|
|
2548
|
-
registry;
|
|
2549
|
-
pushDispatch;
|
|
2550
|
-
sendOutbound;
|
|
2551
|
-
resolveAgent;
|
|
2552
|
-
inboundQueue;
|
|
2553
|
-
canDispatch;
|
|
2554
|
-
log;
|
|
2555
|
-
constructor(opts) {
|
|
2556
|
-
this.registry = opts.registry;
|
|
2557
|
-
this.pushDispatch = opts.pushDispatch;
|
|
2558
|
-
this.sendOutbound = opts.sendOutbound;
|
|
2559
|
-
this.resolveAgent = opts.resolveAgent ?? ((input) => input.defaultAgentId);
|
|
2560
|
-
this.inboundQueue = opts.inboundQueue;
|
|
2561
|
-
this.canDispatch = opts.canDispatch ?? (() => true);
|
|
2562
|
-
this.log = opts.log;
|
|
2563
|
-
}
|
|
2564
|
-
handleInbound(inbound) {
|
|
2565
|
-
const current = this.registry.getCurrent();
|
|
2566
|
-
if (!current || !this.canDispatch()) {
|
|
2567
|
-
if (!this.inboundQueue) {
|
|
2568
|
-
this.log?.(
|
|
2569
|
-
"debug",
|
|
2570
|
-
`no runtime registered; dropping inbound ${inbound.idempotencyKey}`
|
|
2571
|
-
);
|
|
2572
|
-
return { kind: "dropped", reason: "no_runtime" };
|
|
2573
|
-
}
|
|
2574
|
-
const result = this.inboundQueue.enqueue(inbound);
|
|
2575
|
-
if (result.kind === "queued") {
|
|
2576
|
-
this.log?.(
|
|
2577
|
-
"info",
|
|
2578
|
-
`no runtime registered; parked inbound ${inbound.idempotencyKey} as queue#${result.id}`
|
|
2579
|
-
);
|
|
2580
|
-
return { kind: "queued", queueId: result.id };
|
|
2581
|
-
}
|
|
2582
|
-
if (result.kind === "duplicate") {
|
|
2583
|
-
this.log?.(
|
|
2584
|
-
"debug",
|
|
2585
|
-
`inbound ${inbound.idempotencyKey} already parked; ignoring duplicate`
|
|
2586
|
-
);
|
|
2587
|
-
return { kind: "dropped", reason: "no_runtime" };
|
|
2588
|
-
}
|
|
2589
|
-
this.log?.(
|
|
2590
|
-
"warn",
|
|
2591
|
-
`inbound queue full (>=${this.inboundQueue.size()}); dropping ${inbound.idempotencyKey}`
|
|
2592
|
-
);
|
|
2593
|
-
return { kind: "dropped", reason: "queue_full" };
|
|
2594
|
-
}
|
|
2595
|
-
const sessionKey = deriveSessionKey(inbound.location);
|
|
2596
|
-
const agentId = this.resolveAgent({
|
|
2597
|
-
sessionKey,
|
|
2598
|
-
channelId: inbound.location.channelId,
|
|
2599
|
-
defaultAgentId: current.defaultAgentId
|
|
2600
|
-
});
|
|
2601
|
-
if (!agentId) {
|
|
2602
|
-
this.log?.(
|
|
2603
|
-
"warn",
|
|
2604
|
-
`agent resolution returned empty for sessionKey=${sessionKey}`
|
|
2605
|
-
);
|
|
2606
|
-
return { kind: "dropped", reason: "no_default_agent" };
|
|
2607
|
-
}
|
|
2608
|
-
const entry = this.registry.beginDispatch({
|
|
2609
|
-
sessionKey,
|
|
2610
|
-
agentId,
|
|
2611
|
-
location: inbound.location
|
|
2612
|
-
});
|
|
2613
|
-
this.pushDispatch({
|
|
2614
|
-
dispatchId: entry.dispatchId,
|
|
2615
|
-
sessionKey,
|
|
2616
|
-
agentId,
|
|
2617
|
-
inbound
|
|
2618
|
-
});
|
|
2619
|
-
return {
|
|
2620
|
-
kind: "dispatched",
|
|
2621
|
-
dispatchId: entry.dispatchId,
|
|
2622
|
-
sessionKey
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
|
-
/**
|
|
2626
|
-
* Drain parked inbound messages and dispatch them in FIFO order. Called by
|
|
2627
|
-
* the session.register handler after a runtime attaches. Safe to call when
|
|
2628
|
-
* no queue is configured (no-op).
|
|
2629
|
-
*/
|
|
2630
|
-
drainPending() {
|
|
2631
|
-
if (!this.inboundQueue) return { dispatched: 0, dropped: 0 };
|
|
2632
|
-
const current = this.registry.getCurrent();
|
|
2633
|
-
if (!current || !this.canDispatch()) return { dispatched: 0, dropped: 0 };
|
|
2634
|
-
const parked = this.inboundQueue.drain();
|
|
2635
|
-
let dispatched = 0;
|
|
2636
|
-
let dropped = 0;
|
|
2637
|
-
for (const { inbound } of parked) {
|
|
2638
|
-
const sessionKey = deriveSessionKey(inbound.location);
|
|
2639
|
-
const agentId = this.resolveAgent({
|
|
2640
|
-
sessionKey,
|
|
2641
|
-
channelId: inbound.location.channelId,
|
|
2642
|
-
defaultAgentId: current.defaultAgentId
|
|
2643
|
-
});
|
|
2644
|
-
if (!agentId) {
|
|
2645
|
-
dropped += 1;
|
|
2646
|
-
this.log?.(
|
|
2647
|
-
"warn",
|
|
2648
|
-
`drainPending: no agent for ${sessionKey}; dropping ${inbound.idempotencyKey}`
|
|
2649
|
-
);
|
|
2650
|
-
continue;
|
|
2651
|
-
}
|
|
2652
|
-
const entry = this.registry.beginDispatch({
|
|
2653
|
-
sessionKey,
|
|
2654
|
-
agentId,
|
|
2655
|
-
location: inbound.location
|
|
2656
|
-
});
|
|
2657
|
-
this.pushDispatch({
|
|
2658
|
-
dispatchId: entry.dispatchId,
|
|
2659
|
-
sessionKey,
|
|
2660
|
-
agentId,
|
|
2661
|
-
inbound
|
|
2662
|
-
});
|
|
2663
|
-
dispatched += 1;
|
|
2664
|
-
}
|
|
2665
|
-
if (dispatched > 0 || dropped > 0) {
|
|
2666
|
-
this.log?.(
|
|
2667
|
-
"info",
|
|
2668
|
-
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
2669
|
-
);
|
|
2670
|
-
}
|
|
2671
|
-
return { dispatched, dropped };
|
|
2672
|
-
}
|
|
2673
|
-
async handleTurnComplete(payload) {
|
|
2674
|
-
const current = this.registry.getCurrent();
|
|
2675
|
-
writeGatewayTrace(
|
|
2676
|
-
`dispatcher 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}`
|
|
2677
|
-
);
|
|
2678
|
-
if (!current || current.runtimeId !== payload.runtimeId) {
|
|
2679
|
-
throw new Error("runtime mismatch on session.turn.complete");
|
|
2680
|
-
}
|
|
2681
|
-
let entry;
|
|
2682
|
-
try {
|
|
2683
|
-
entry = this.registry.completeDispatch(payload.dispatchId);
|
|
2684
|
-
} catch (err) {
|
|
2685
|
-
if (err instanceof UnknownDispatchError) {
|
|
2686
|
-
writeGatewayTrace(
|
|
2687
|
-
`dispatcher turn.complete unknown dispatchId=${payload.dispatchId}`
|
|
2688
|
-
);
|
|
2689
|
-
return { delivered: false };
|
|
2690
|
-
}
|
|
2691
|
-
throw err;
|
|
2692
|
-
}
|
|
2693
|
-
writeGatewayTrace(
|
|
2694
|
-
`dispatcher sendOutbound channel=${entry.location.channelId} dispatchId=${payload.dispatchId} parkedAccount=${entry.location.accountId} parkedThread=${entry.location.thread?.id ?? ""}`
|
|
2695
|
-
);
|
|
2696
|
-
const result = await this.sendOutbound(entry.location.channelId, {
|
|
2697
|
-
location: payload.location,
|
|
2698
|
-
text: payload.text,
|
|
2699
|
-
idempotencyKey: payload.idempotencyKey
|
|
2700
|
-
});
|
|
2701
|
-
writeGatewayTrace(
|
|
2702
|
-
`dispatcher sendOutbound delivered dispatchId=${payload.dispatchId} providerMessageId=${result.providerMessageId}`
|
|
2703
|
-
);
|
|
2704
|
-
return {
|
|
2705
|
-
delivered: true,
|
|
2706
|
-
providerMessageId: result.providerMessageId
|
|
2707
|
-
};
|
|
2708
|
-
}
|
|
2709
|
-
};
|
|
2710
|
-
|
|
2711
|
-
// src/gateway/lock.ts
|
|
2712
|
-
import fs2 from "fs";
|
|
2713
|
-
import path2 from "path";
|
|
2714
|
-
var GatewayAlreadyRunningError = class extends Error {
|
|
2715
|
-
otherPid;
|
|
2716
|
-
constructor(otherPid) {
|
|
2717
|
-
super(`gateway already running (pid=${otherPid})`);
|
|
2718
|
-
this.name = "GatewayAlreadyRunningError";
|
|
2719
|
-
this.otherPid = otherPid;
|
|
2720
|
-
}
|
|
2721
|
-
};
|
|
2722
|
-
function isProcessAlive(pid) {
|
|
2723
|
-
try {
|
|
2724
|
-
process.kill(pid, 0);
|
|
2725
|
-
return true;
|
|
2726
|
-
} catch (err) {
|
|
2727
|
-
const code = err.code;
|
|
2728
|
-
return code === "EPERM";
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
function readPidFile(p) {
|
|
2732
|
-
try {
|
|
2733
|
-
const text = fs2.readFileSync(p, "utf-8").trim();
|
|
2734
|
-
const pid = Number.parseInt(text, 10);
|
|
2735
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2736
|
-
} catch {
|
|
2737
|
-
return null;
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
function acquireLock(lockPath) {
|
|
2741
|
-
fs2.mkdirSync(path2.dirname(lockPath), { recursive: true, mode: 448 });
|
|
2742
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2743
|
-
try {
|
|
2744
|
-
const fd = fs2.openSync(lockPath, "wx", 384);
|
|
2745
|
-
fs2.writeSync(fd, String(process.pid) + "\n");
|
|
2746
|
-
fs2.closeSync(fd);
|
|
2747
|
-
return {
|
|
2748
|
-
path: lockPath,
|
|
2749
|
-
pid: process.pid,
|
|
2750
|
-
release: () => {
|
|
2751
|
-
try {
|
|
2752
|
-
const pidNow = readPidFile(lockPath);
|
|
2753
|
-
if (pidNow === process.pid) {
|
|
2754
|
-
fs2.unlinkSync(lockPath);
|
|
2755
|
-
}
|
|
2756
|
-
} catch {
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
};
|
|
2760
|
-
} catch (err) {
|
|
2761
|
-
const code = err.code;
|
|
2762
|
-
if (code !== "EEXIST") throw err;
|
|
2763
|
-
const otherPid = readPidFile(lockPath);
|
|
2764
|
-
if (otherPid !== null && isProcessAlive(otherPid)) {
|
|
2765
|
-
throw new GatewayAlreadyRunningError(otherPid);
|
|
2766
|
-
}
|
|
2767
|
-
try {
|
|
2768
|
-
fs2.unlinkSync(lockPath);
|
|
2769
|
-
} catch {
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
throw new Error(`failed to acquire gateway lock at ${lockPath}`);
|
|
2774
|
-
}
|
|
2498
|
+
// src/gateway/dispatchPipeline.ts
|
|
2499
|
+
import crypto from "crypto";
|
|
2775
2500
|
|
|
2776
2501
|
// src/gateway/outboundDispatcher.ts
|
|
2777
2502
|
var DEFAULT_BACKOFF = [
|
|
@@ -2870,43 +2595,504 @@ var OutboundDispatcher = class {
|
|
|
2870
2595
|
} finally {
|
|
2871
2596
|
this.draining = false;
|
|
2872
2597
|
}
|
|
2873
|
-
return { retried, succeeded, dropped };
|
|
2874
|
-
}
|
|
2875
|
-
async attempt(row) {
|
|
2876
|
-
try {
|
|
2877
|
-
await this.send(row.channelId, row.message);
|
|
2878
|
-
this.outbox.delete(row.id);
|
|
2598
|
+
return { retried, succeeded, dropped };
|
|
2599
|
+
}
|
|
2600
|
+
async attempt(row) {
|
|
2601
|
+
try {
|
|
2602
|
+
await this.send(row.channelId, row.message);
|
|
2603
|
+
this.outbox.delete(row.id);
|
|
2604
|
+
this.log?.(
|
|
2605
|
+
"info",
|
|
2606
|
+
`outbox#${row.id} delivered to ${row.channelId} on attempt ${row.attempt + 1}`
|
|
2607
|
+
);
|
|
2608
|
+
return "succeeded";
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
const error2 = err instanceof Error ? err.message : String(err);
|
|
2611
|
+
const nextAttempt = row.attempt + 1;
|
|
2612
|
+
if (nextAttempt >= this.maxAttempts) {
|
|
2613
|
+
this.outbox.delete(row.id);
|
|
2614
|
+
this.log?.(
|
|
2615
|
+
"error",
|
|
2616
|
+
`outbox#${row.id} dropped after ${nextAttempt} attempts: ${error2}`
|
|
2617
|
+
);
|
|
2618
|
+
return "dropped";
|
|
2619
|
+
}
|
|
2620
|
+
const nextAttemptAt = this.now() + this.backoffFor(nextAttempt);
|
|
2621
|
+
this.outbox.recordFailure({
|
|
2622
|
+
id: row.id,
|
|
2623
|
+
nextAttemptAt,
|
|
2624
|
+
lastError: error2
|
|
2625
|
+
});
|
|
2626
|
+
return "requeued";
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
backoffFor(attempt) {
|
|
2630
|
+
if (this.backoff.length === 0) return 1e3;
|
|
2631
|
+
const idx = Math.min(attempt, this.backoff.length - 1);
|
|
2632
|
+
return this.backoff[idx];
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
// src/gateway/router/sessionKey.ts
|
|
2637
|
+
function deriveSessionKey(loc) {
|
|
2638
|
+
const c = loc.channelId;
|
|
2639
|
+
const a = loc.accountId;
|
|
2640
|
+
if (loc.peer?.id) {
|
|
2641
|
+
const peer = loc.peer.id;
|
|
2642
|
+
if (loc.thread?.id) {
|
|
2643
|
+
return `peer:${c}:${a}:${peer}:${loc.thread.id}`;
|
|
2644
|
+
}
|
|
2645
|
+
return `peer:${c}:${a}:${peer}`;
|
|
2646
|
+
}
|
|
2647
|
+
if (loc.room?.id) {
|
|
2648
|
+
const room = loc.room.id;
|
|
2649
|
+
if (loc.thread?.id) {
|
|
2650
|
+
return `room:${c}:${a}:${room}:${loc.thread.id}`;
|
|
2651
|
+
}
|
|
2652
|
+
return `room:${c}:${a}:${room}`;
|
|
2653
|
+
}
|
|
2654
|
+
return `default:${c}:${a}`;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/gateway/sessionRegistry.ts
|
|
2658
|
+
import { randomUUID } from "crypto";
|
|
2659
|
+
var UnknownDispatchError = class extends Error {
|
|
2660
|
+
code = "unknown_dispatch";
|
|
2661
|
+
constructor(id) {
|
|
2662
|
+
super(`unknown dispatchId: ${id}`);
|
|
2663
|
+
this.name = "UnknownDispatchError";
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
var SessionRegistry = class {
|
|
2667
|
+
dispatches = /* @__PURE__ */ new Map();
|
|
2668
|
+
idFactory;
|
|
2669
|
+
now;
|
|
2670
|
+
constructor(opts = {}) {
|
|
2671
|
+
this.idFactory = opts.idFactory ?? randomUUID;
|
|
2672
|
+
this.now = opts.now ?? Date.now;
|
|
2673
|
+
}
|
|
2674
|
+
beginDispatch(input) {
|
|
2675
|
+
const dispatchId = this.idFactory();
|
|
2676
|
+
const entry = {
|
|
2677
|
+
dispatchId,
|
|
2678
|
+
sessionKey: input.sessionKey,
|
|
2679
|
+
agentId: input.agentId,
|
|
2680
|
+
location: input.location,
|
|
2681
|
+
createdAt: this.now()
|
|
2682
|
+
};
|
|
2683
|
+
this.dispatches.set(dispatchId, entry);
|
|
2684
|
+
return entry;
|
|
2685
|
+
}
|
|
2686
|
+
completeDispatch(dispatchId) {
|
|
2687
|
+
const entry = this.dispatches.get(dispatchId);
|
|
2688
|
+
if (!entry) {
|
|
2689
|
+
throw new UnknownDispatchError(dispatchId);
|
|
2690
|
+
}
|
|
2691
|
+
this.dispatches.delete(dispatchId);
|
|
2692
|
+
return entry;
|
|
2693
|
+
}
|
|
2694
|
+
pendingDispatchCount() {
|
|
2695
|
+
return this.dispatches.size;
|
|
2696
|
+
}
|
|
2697
|
+
clearDispatches() {
|
|
2698
|
+
this.dispatches.clear();
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2702
|
+
// src/gateway/state/inboundQueue.ts
|
|
2703
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2704
|
+
var InboundQueue = class {
|
|
2705
|
+
db;
|
|
2706
|
+
maxEntries;
|
|
2707
|
+
constructor(db, opts = {}) {
|
|
2708
|
+
this.db = db;
|
|
2709
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2710
|
+
}
|
|
2711
|
+
size() {
|
|
2712
|
+
const row = this.db.prepare("SELECT COUNT(*) as n FROM inbound_queue").get();
|
|
2713
|
+
return row.n;
|
|
2714
|
+
}
|
|
2715
|
+
enqueue(inbound) {
|
|
2716
|
+
if (this.size() >= this.maxEntries) {
|
|
2717
|
+
return { kind: "rejected", reason: "queue_full" };
|
|
2718
|
+
}
|
|
2719
|
+
const stmt = this.db.prepare(
|
|
2720
|
+
`INSERT INTO inbound_queue
|
|
2721
|
+
(channel_id, account_id, idempotency_key, payload_json, created_at)
|
|
2722
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2723
|
+
ON CONFLICT(channel_id, account_id, idempotency_key) DO NOTHING`
|
|
2724
|
+
);
|
|
2725
|
+
const result = stmt.run(
|
|
2726
|
+
inbound.location.channelId,
|
|
2727
|
+
inbound.location.accountId,
|
|
2728
|
+
inbound.idempotencyKey,
|
|
2729
|
+
JSON.stringify(inbound),
|
|
2730
|
+
Date.now()
|
|
2731
|
+
);
|
|
2732
|
+
if (result.changes === 0) {
|
|
2733
|
+
return { kind: "duplicate" };
|
|
2734
|
+
}
|
|
2735
|
+
return { kind: "queued", id: Number(result.lastInsertRowid) };
|
|
2736
|
+
}
|
|
2737
|
+
/** Atomically read and remove all parked entries in FIFO order. */
|
|
2738
|
+
drain() {
|
|
2739
|
+
return this.db.transaction(() => {
|
|
2740
|
+
const rows = this.db.prepare("SELECT id, payload_json FROM inbound_queue ORDER BY id ASC").all();
|
|
2741
|
+
if (rows.length > 0) {
|
|
2742
|
+
this.db.prepare("DELETE FROM inbound_queue").run();
|
|
2743
|
+
}
|
|
2744
|
+
return rows.map((r) => ({
|
|
2745
|
+
id: r.id,
|
|
2746
|
+
inbound: JSON.parse(r.payload_json)
|
|
2747
|
+
}));
|
|
2748
|
+
})();
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// src/gateway/state/outbox.ts
|
|
2753
|
+
var Outbox = class {
|
|
2754
|
+
db;
|
|
2755
|
+
constructor(db) {
|
|
2756
|
+
this.db = db;
|
|
2757
|
+
}
|
|
2758
|
+
size() {
|
|
2759
|
+
const row = this.db.prepare("SELECT COUNT(*) as n FROM channel_outbox").get();
|
|
2760
|
+
return row.n;
|
|
2761
|
+
}
|
|
2762
|
+
enqueue(input) {
|
|
2763
|
+
const result = this.db.prepare(
|
|
2764
|
+
`INSERT INTO channel_outbox
|
|
2765
|
+
(channel_id, payload_json, attempt, next_attempt_at, last_error, created_at)
|
|
2766
|
+
VALUES (?, ?, 0, ?, ?, ?)`
|
|
2767
|
+
).run(
|
|
2768
|
+
input.channelId,
|
|
2769
|
+
JSON.stringify(input.message),
|
|
2770
|
+
input.nextAttemptAt,
|
|
2771
|
+
input.lastError ?? null,
|
|
2772
|
+
Date.now()
|
|
2773
|
+
);
|
|
2774
|
+
return Number(result.lastInsertRowid);
|
|
2775
|
+
}
|
|
2776
|
+
/** Rows whose `next_attempt_at` is at or before `now`, oldest first. */
|
|
2777
|
+
peekDue(now, limit) {
|
|
2778
|
+
const rows = this.db.prepare(
|
|
2779
|
+
`SELECT id, channel_id, payload_json, attempt, next_attempt_at, last_error
|
|
2780
|
+
FROM channel_outbox
|
|
2781
|
+
WHERE next_attempt_at <= ?
|
|
2782
|
+
ORDER BY next_attempt_at ASC, id ASC
|
|
2783
|
+
LIMIT ?`
|
|
2784
|
+
).all(now, limit);
|
|
2785
|
+
return rows.map((r) => ({
|
|
2786
|
+
id: r.id,
|
|
2787
|
+
channelId: r.channel_id,
|
|
2788
|
+
message: JSON.parse(r.payload_json),
|
|
2789
|
+
attempt: r.attempt,
|
|
2790
|
+
nextAttemptAt: r.next_attempt_at,
|
|
2791
|
+
lastError: r.last_error
|
|
2792
|
+
}));
|
|
2793
|
+
}
|
|
2794
|
+
delete(id) {
|
|
2795
|
+
this.db.prepare("DELETE FROM channel_outbox WHERE id = ?").run(id);
|
|
2796
|
+
}
|
|
2797
|
+
recordFailure(input) {
|
|
2798
|
+
this.db.prepare(
|
|
2799
|
+
`UPDATE channel_outbox
|
|
2800
|
+
SET attempt = attempt + 1,
|
|
2801
|
+
next_attempt_at = ?,
|
|
2802
|
+
last_error = ?
|
|
2803
|
+
WHERE id = ?`
|
|
2804
|
+
).run(input.nextAttemptAt, input.lastError, input.id);
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
// src/gateway/dispatchPipeline.ts
|
|
2809
|
+
var DispatchPipeline = class {
|
|
2810
|
+
bindingStore;
|
|
2811
|
+
registry;
|
|
2812
|
+
inboundQueue;
|
|
2813
|
+
outbox;
|
|
2814
|
+
outboundDispatcher;
|
|
2815
|
+
resolveAgent;
|
|
2816
|
+
log;
|
|
2817
|
+
now;
|
|
2818
|
+
idFactory;
|
|
2819
|
+
push = null;
|
|
2820
|
+
constructor(opts) {
|
|
2821
|
+
this.bindingStore = new RuntimeBindingStore({
|
|
2822
|
+
gracePeriodMs: opts.gracePeriodMs,
|
|
2823
|
+
observers: opts.observers,
|
|
2824
|
+
now: opts.now
|
|
2825
|
+
});
|
|
2826
|
+
this.registry = new SessionRegistry({
|
|
2827
|
+
now: opts.now ?? Date.now,
|
|
2828
|
+
...opts.idFactory ? { idFactory: opts.idFactory } : {}
|
|
2829
|
+
});
|
|
2830
|
+
this.inboundQueue = new InboundQueue(opts.stateDb, opts.inboundQueue ?? {});
|
|
2831
|
+
this.outbox = new Outbox(opts.stateDb);
|
|
2832
|
+
this.outboundDispatcher = new OutboundDispatcher({
|
|
2833
|
+
outbox: this.outbox,
|
|
2834
|
+
send: opts.send,
|
|
2835
|
+
...opts.outbox?.backoffSchedule ? { backoffSchedule: opts.outbox.backoffSchedule } : {},
|
|
2836
|
+
...opts.outbox?.maxAttempts !== void 0 ? { maxAttempts: opts.outbox.maxAttempts } : {},
|
|
2837
|
+
...opts.outbox?.tickIntervalMs !== void 0 ? { tickIntervalMs: opts.outbox.tickIntervalMs } : {},
|
|
2838
|
+
...opts.outbox?.drainBatchSize !== void 0 ? { drainBatchSize: opts.outbox.drainBatchSize } : {},
|
|
2839
|
+
...opts.now ? { now: opts.now } : {},
|
|
2840
|
+
...opts.log ? { log: opts.log } : {}
|
|
2841
|
+
});
|
|
2842
|
+
this.resolveAgent = opts.resolveAgent ?? ((input) => input.defaultAgentId);
|
|
2843
|
+
this.log = opts.log;
|
|
2844
|
+
this.now = opts.now ?? Date.now;
|
|
2845
|
+
this.idFactory = opts.idFactory ?? crypto.randomUUID;
|
|
2846
|
+
}
|
|
2847
|
+
// ── lifecycle ────────────────────────────────────────────
|
|
2848
|
+
start() {
|
|
2849
|
+
this.outboundDispatcher.start();
|
|
2850
|
+
}
|
|
2851
|
+
async stop() {
|
|
2852
|
+
this.outboundDispatcher.stop();
|
|
2853
|
+
this.bindingStore.stop();
|
|
2854
|
+
}
|
|
2855
|
+
// ── inbound (channel side) ───────────────────────────────
|
|
2856
|
+
handleInbound(inbound) {
|
|
2857
|
+
const current = this.bindingStore.getCurrent();
|
|
2858
|
+
if (!current || !this.bindingStore.hasActiveBinding(current.runtimeId)) {
|
|
2859
|
+
const result = this.inboundQueue.enqueue(inbound);
|
|
2860
|
+
if (result.kind === "queued") {
|
|
2861
|
+
this.log?.(
|
|
2862
|
+
"info",
|
|
2863
|
+
`no runtime registered; parked inbound ${inbound.idempotencyKey} as queue#${result.id}`
|
|
2864
|
+
);
|
|
2865
|
+
return { kind: "queued", queueId: result.id };
|
|
2866
|
+
}
|
|
2867
|
+
if (result.kind === "duplicate") {
|
|
2868
|
+
this.log?.(
|
|
2869
|
+
"debug",
|
|
2870
|
+
`inbound ${inbound.idempotencyKey} already parked; ignoring duplicate`
|
|
2871
|
+
);
|
|
2872
|
+
return { kind: "dropped", reason: "no_runtime" };
|
|
2873
|
+
}
|
|
2874
|
+
this.log?.(
|
|
2875
|
+
"warn",
|
|
2876
|
+
`inbound queue full (>=${this.inboundQueue.size()}); dropping ${inbound.idempotencyKey}`
|
|
2877
|
+
);
|
|
2878
|
+
return { kind: "dropped", reason: "queue_full" };
|
|
2879
|
+
}
|
|
2880
|
+
return this.dispatchInboundToRuntime(inbound, current);
|
|
2881
|
+
}
|
|
2882
|
+
dispatchInboundToRuntime(inbound, current) {
|
|
2883
|
+
const sessionKey = deriveSessionKey(inbound.location);
|
|
2884
|
+
const agentId = this.resolveAgent({
|
|
2885
|
+
sessionKey,
|
|
2886
|
+
channelId: inbound.location.channelId,
|
|
2887
|
+
defaultAgentId: current.defaultAgentId
|
|
2888
|
+
});
|
|
2889
|
+
if (!agentId) {
|
|
2890
|
+
this.log?.(
|
|
2891
|
+
"warn",
|
|
2892
|
+
`agent resolution returned empty for sessionKey=${sessionKey}`
|
|
2893
|
+
);
|
|
2894
|
+
return { kind: "dropped", reason: "no_default_agent" };
|
|
2895
|
+
}
|
|
2896
|
+
const entry = this.registry.beginDispatch({
|
|
2897
|
+
sessionKey,
|
|
2898
|
+
agentId,
|
|
2899
|
+
location: inbound.location
|
|
2900
|
+
});
|
|
2901
|
+
this.pushDispatch({
|
|
2902
|
+
dispatchId: entry.dispatchId,
|
|
2903
|
+
sessionKey,
|
|
2904
|
+
agentId,
|
|
2905
|
+
inbound
|
|
2906
|
+
});
|
|
2907
|
+
return { kind: "dispatched", dispatchId: entry.dispatchId, sessionKey };
|
|
2908
|
+
}
|
|
2909
|
+
pushDispatch(payload) {
|
|
2910
|
+
if (!this.push) return;
|
|
2911
|
+
this.push.push({
|
|
2912
|
+
push_id: this.idFactory(),
|
|
2913
|
+
ts: this.now(),
|
|
2914
|
+
kind: "session.dispatch.turn",
|
|
2915
|
+
payload
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
// ── runtime side ─────────────────────────────────────────
|
|
2919
|
+
registerRuntime(input) {
|
|
2920
|
+
const result = this.bindingStore.bind({
|
|
2921
|
+
runtimeId: input.runtimeId,
|
|
2922
|
+
defaultAgentId: input.defaultAgentId,
|
|
2923
|
+
pid: input.pid,
|
|
2924
|
+
connectionId: input.connectionId
|
|
2925
|
+
});
|
|
2926
|
+
this.push = { connectionId: input.connectionId, push: input.push };
|
|
2927
|
+
writeGatewayTrace(
|
|
2928
|
+
`pipeline registered runtime runtimeId=${input.runtimeId} connectionId=${input.connectionId}`
|
|
2929
|
+
);
|
|
2930
|
+
this.drainPending();
|
|
2931
|
+
return { registeredAt: result.registeredAt };
|
|
2932
|
+
}
|
|
2933
|
+
unregisterRuntime(runtimeId) {
|
|
2934
|
+
this.bindingStore.unbind(runtimeId);
|
|
2935
|
+
this.registry.clearDispatches();
|
|
2936
|
+
this.push = null;
|
|
2937
|
+
writeGatewayTrace(`pipeline unregistered runtime runtimeId=${runtimeId}`);
|
|
2938
|
+
}
|
|
2939
|
+
notifyConnectionClosed(connectionId) {
|
|
2940
|
+
const runtimeId = this.bindingStore.notifyConnectionClosed(connectionId);
|
|
2941
|
+
if (runtimeId === null) return;
|
|
2942
|
+
writeGatewayTrace(
|
|
2943
|
+
`pipeline runtime connection closed runtimeId=${runtimeId} connectionId=${connectionId}`
|
|
2944
|
+
);
|
|
2945
|
+
this.push = null;
|
|
2946
|
+
}
|
|
2947
|
+
async handleTurnComplete(payload) {
|
|
2948
|
+
const current = this.bindingStore.getCurrent();
|
|
2949
|
+
writeGatewayTrace(
|
|
2950
|
+
`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}`
|
|
2951
|
+
);
|
|
2952
|
+
if (!current || current.runtimeId !== payload.runtimeId) {
|
|
2953
|
+
throw new Error("runtime mismatch on session.turn.complete");
|
|
2954
|
+
}
|
|
2955
|
+
let entry;
|
|
2956
|
+
try {
|
|
2957
|
+
entry = this.registry.completeDispatch(payload.dispatchId);
|
|
2958
|
+
} catch (err) {
|
|
2959
|
+
if (err instanceof UnknownDispatchError) {
|
|
2960
|
+
writeGatewayTrace(
|
|
2961
|
+
`pipeline turn.complete unknown dispatchId=${payload.dispatchId}`
|
|
2962
|
+
);
|
|
2963
|
+
return { delivered: false };
|
|
2964
|
+
}
|
|
2965
|
+
throw err;
|
|
2966
|
+
}
|
|
2967
|
+
const result = await this.sendOutbound(entry.location, payload);
|
|
2968
|
+
writeGatewayTrace(
|
|
2969
|
+
`pipeline sendOutbound delivered dispatchId=${payload.dispatchId} providerMessageId=${result.providerMessageId}`
|
|
2970
|
+
);
|
|
2971
|
+
return { delivered: true, providerMessageId: result.providerMessageId };
|
|
2972
|
+
}
|
|
2973
|
+
async sendOutbound(_parkedLocation, payload) {
|
|
2974
|
+
const out = {
|
|
2975
|
+
location: payload.location,
|
|
2976
|
+
text: payload.text,
|
|
2977
|
+
idempotencyKey: payload.idempotencyKey
|
|
2978
|
+
};
|
|
2979
|
+
const result = await this.outboundDispatcher.dispatch(
|
|
2980
|
+
payload.location.channelId,
|
|
2981
|
+
out
|
|
2982
|
+
);
|
|
2983
|
+
if (result.kind === "sent") return result.result;
|
|
2984
|
+
return {
|
|
2985
|
+
providerMessageId: `outbox:${result.outboxId}`,
|
|
2986
|
+
deliveredAt: this.now()
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
drainPending() {
|
|
2990
|
+
const current = this.bindingStore.getCurrent();
|
|
2991
|
+
if (!current || !this.bindingStore.hasActiveBinding(current.runtimeId))
|
|
2992
|
+
return;
|
|
2993
|
+
const parked = this.inboundQueue.drain();
|
|
2994
|
+
let dispatched = 0;
|
|
2995
|
+
let dropped = 0;
|
|
2996
|
+
for (const { inbound } of parked) {
|
|
2997
|
+
const result = this.dispatchInboundToRuntime(inbound, current);
|
|
2998
|
+
if (result.kind === "dispatched") dispatched += 1;
|
|
2999
|
+
else dropped += 1;
|
|
3000
|
+
}
|
|
3001
|
+
if (dispatched > 0 || dropped > 0) {
|
|
2879
3002
|
this.log?.(
|
|
2880
3003
|
"info",
|
|
2881
|
-
`
|
|
3004
|
+
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
2882
3005
|
);
|
|
2883
|
-
return "succeeded";
|
|
2884
|
-
} catch (err) {
|
|
2885
|
-
const error2 = err instanceof Error ? err.message : String(err);
|
|
2886
|
-
const nextAttempt = row.attempt + 1;
|
|
2887
|
-
if (nextAttempt >= this.maxAttempts) {
|
|
2888
|
-
this.outbox.delete(row.id);
|
|
2889
|
-
this.log?.(
|
|
2890
|
-
"error",
|
|
2891
|
-
`outbox#${row.id} dropped after ${nextAttempt} attempts: ${error2}`
|
|
2892
|
-
);
|
|
2893
|
-
return "dropped";
|
|
2894
|
-
}
|
|
2895
|
-
const nextAttemptAt = this.now() + this.backoffFor(nextAttempt);
|
|
2896
|
-
this.outbox.recordFailure({
|
|
2897
|
-
id: row.id,
|
|
2898
|
-
nextAttemptAt,
|
|
2899
|
-
lastError: error2
|
|
2900
|
-
});
|
|
2901
|
-
return "requeued";
|
|
2902
3006
|
}
|
|
2903
3007
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3008
|
+
// ── reads ────────────────────────────────────────────────
|
|
3009
|
+
getCurrentRuntime() {
|
|
3010
|
+
return this.bindingStore.getCurrent();
|
|
3011
|
+
}
|
|
3012
|
+
getBinding() {
|
|
3013
|
+
return this.bindingStore.getBinding();
|
|
3014
|
+
}
|
|
3015
|
+
hasActiveBinding(runtimeId) {
|
|
3016
|
+
return this.bindingStore.hasActiveBinding(runtimeId);
|
|
3017
|
+
}
|
|
3018
|
+
getRuntimeIdByConnection(connectionId) {
|
|
3019
|
+
return this.bindingStore.getRuntimeIdByConnection(connectionId);
|
|
3020
|
+
}
|
|
3021
|
+
pendingDispatchCount() {
|
|
3022
|
+
return this.registry.pendingDispatchCount();
|
|
3023
|
+
}
|
|
3024
|
+
pendingInboundCount() {
|
|
3025
|
+
return this.inboundQueue.size();
|
|
3026
|
+
}
|
|
3027
|
+
pendingOutboxCount() {
|
|
3028
|
+
return this.outbox.size();
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
// src/gateway/lock.ts
|
|
3033
|
+
import fs from "fs";
|
|
3034
|
+
import path from "path";
|
|
3035
|
+
var GatewayAlreadyRunningError = class extends Error {
|
|
3036
|
+
otherPid;
|
|
3037
|
+
constructor(otherPid) {
|
|
3038
|
+
super(`gateway already running (pid=${otherPid})`);
|
|
3039
|
+
this.name = "GatewayAlreadyRunningError";
|
|
3040
|
+
this.otherPid = otherPid;
|
|
2908
3041
|
}
|
|
2909
3042
|
};
|
|
3043
|
+
function isProcessAlive(pid) {
|
|
3044
|
+
try {
|
|
3045
|
+
process.kill(pid, 0);
|
|
3046
|
+
return true;
|
|
3047
|
+
} catch (err) {
|
|
3048
|
+
const code = err.code;
|
|
3049
|
+
return code === "EPERM";
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
function readPidFile(p) {
|
|
3053
|
+
try {
|
|
3054
|
+
const text = fs.readFileSync(p, "utf-8").trim();
|
|
3055
|
+
const pid = Number.parseInt(text, 10);
|
|
3056
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
3057
|
+
} catch {
|
|
3058
|
+
return null;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
function acquireLock(lockPath) {
|
|
3062
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
3063
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3064
|
+
try {
|
|
3065
|
+
const fd = fs.openSync(lockPath, "wx", 384);
|
|
3066
|
+
fs.writeSync(fd, String(process.pid) + "\n");
|
|
3067
|
+
fs.closeSync(fd);
|
|
3068
|
+
return {
|
|
3069
|
+
path: lockPath,
|
|
3070
|
+
pid: process.pid,
|
|
3071
|
+
release: () => {
|
|
3072
|
+
try {
|
|
3073
|
+
const pidNow = readPidFile(lockPath);
|
|
3074
|
+
if (pidNow === process.pid) {
|
|
3075
|
+
fs.unlinkSync(lockPath);
|
|
3076
|
+
}
|
|
3077
|
+
} catch {
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
const code = err.code;
|
|
3083
|
+
if (code !== "EEXIST") throw err;
|
|
3084
|
+
const otherPid = readPidFile(lockPath);
|
|
3085
|
+
if (otherPid !== null && isProcessAlive(otherPid)) {
|
|
3086
|
+
throw new GatewayAlreadyRunningError(otherPid);
|
|
3087
|
+
}
|
|
3088
|
+
try {
|
|
3089
|
+
fs.unlinkSync(lockPath);
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
throw new Error(`failed to acquire gateway lock at ${lockPath}`);
|
|
3095
|
+
}
|
|
2910
3096
|
|
|
2911
3097
|
// src/gateway/relay/coordinator.ts
|
|
2912
3098
|
var DEFAULT_RELAY_TTL_MS = 5 * 6e4;
|
|
@@ -2960,10 +3146,7 @@ var RelayCoordinator = class {
|
|
|
2960
3146
|
resolveFn = resolve;
|
|
2961
3147
|
});
|
|
2962
3148
|
const timer = setTimeout(() => {
|
|
2963
|
-
this.
|
|
2964
|
-
kind: "cancelled",
|
|
2965
|
-
reason: "timeout"
|
|
2966
|
-
});
|
|
3149
|
+
this.settle(channelRequestId, { kind: "cancelled", reason: "timeout" });
|
|
2967
3150
|
}, ttlMs);
|
|
2968
3151
|
if (typeof timer.unref === "function") timer.unref();
|
|
2969
3152
|
const entry = {
|
|
@@ -2988,10 +3171,7 @@ var RelayCoordinator = class {
|
|
|
2988
3171
|
const ctrl = controllers[idx];
|
|
2989
3172
|
Promise.resolve().then(() => adapter.requestPermissionVerdict(fullReq, ctrl.signal)).then((res) => {
|
|
2990
3173
|
if (res.kind === "verdict") {
|
|
2991
|
-
this.
|
|
2992
|
-
...res,
|
|
2993
|
-
channelId: adapter.id
|
|
2994
|
-
});
|
|
3174
|
+
this.settle(channelRequestId, { ...res, channelId: adapter.id });
|
|
2995
3175
|
}
|
|
2996
3176
|
}).catch((err) => {
|
|
2997
3177
|
this.log?.(
|
|
@@ -3040,10 +3220,7 @@ var RelayCoordinator = class {
|
|
|
3040
3220
|
resolveFn = resolve;
|
|
3041
3221
|
});
|
|
3042
3222
|
const timer = setTimeout(() => {
|
|
3043
|
-
this.
|
|
3044
|
-
kind: "cancelled",
|
|
3045
|
-
reason: "timeout"
|
|
3046
|
-
});
|
|
3223
|
+
this.settle(channelRequestId, { kind: "cancelled", reason: "timeout" });
|
|
3047
3224
|
}, ttlMs);
|
|
3048
3225
|
if (typeof timer.unref === "function") timer.unref();
|
|
3049
3226
|
const entry = {
|
|
@@ -3067,10 +3244,7 @@ var RelayCoordinator = class {
|
|
|
3067
3244
|
const ctrl = controllers[idx];
|
|
3068
3245
|
Promise.resolve().then(() => adapter.requestQuestionAnswer(fullReq, ctrl.signal)).then((res) => {
|
|
3069
3246
|
if (res.kind === "answer") {
|
|
3070
|
-
this.
|
|
3071
|
-
...res,
|
|
3072
|
-
channelId: adapter.id
|
|
3073
|
-
});
|
|
3247
|
+
this.settle(channelRequestId, { ...res, channelId: adapter.id });
|
|
3074
3248
|
}
|
|
3075
3249
|
}).catch((err) => {
|
|
3076
3250
|
this.log?.(
|
|
@@ -3087,11 +3261,7 @@ var RelayCoordinator = class {
|
|
|
3087
3261
|
if (expectedRuntimeId !== void 0 && entry.runtimeId !== void 0 && entry.runtimeId !== expectedRuntimeId) {
|
|
3088
3262
|
return false;
|
|
3089
3263
|
}
|
|
3090
|
-
|
|
3091
|
-
this.settlePermission(channelRequestId, { kind: "cancelled", reason });
|
|
3092
|
-
} else {
|
|
3093
|
-
this.settleQuestion(channelRequestId, { kind: "cancelled", reason });
|
|
3094
|
-
}
|
|
3264
|
+
this.settle(channelRequestId, { kind: "cancelled", reason });
|
|
3095
3265
|
return true;
|
|
3096
3266
|
}
|
|
3097
3267
|
pendingCount() {
|
|
@@ -3102,27 +3272,18 @@ var RelayCoordinator = class {
|
|
|
3102
3272
|
this.cancel(id, reason);
|
|
3103
3273
|
}
|
|
3104
3274
|
}
|
|
3105
|
-
|
|
3106
|
-
const entry = this.pending.get(channelRequestId);
|
|
3107
|
-
if (!entry || entry.kind !== "permission" || entry.settled) return;
|
|
3108
|
-
entry.settled = true;
|
|
3109
|
-
this.pending.delete(channelRequestId);
|
|
3110
|
-
clearTimeout(entry.timer);
|
|
3111
|
-
for (const ctrl of entry.controllers) {
|
|
3112
|
-
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3113
|
-
}
|
|
3114
|
-
entry.resolve(result);
|
|
3115
|
-
}
|
|
3116
|
-
settleQuestion(channelRequestId, result) {
|
|
3275
|
+
settle(channelRequestId, result) {
|
|
3117
3276
|
const entry = this.pending.get(channelRequestId);
|
|
3118
|
-
if (!entry || entry.
|
|
3277
|
+
if (!entry || entry.settled) return;
|
|
3119
3278
|
entry.settled = true;
|
|
3120
3279
|
this.pending.delete(channelRequestId);
|
|
3121
3280
|
clearTimeout(entry.timer);
|
|
3122
3281
|
for (const ctrl of entry.controllers) {
|
|
3123
3282
|
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3124
3283
|
}
|
|
3125
|
-
entry.resolve(
|
|
3284
|
+
entry.resolve(
|
|
3285
|
+
result
|
|
3286
|
+
);
|
|
3126
3287
|
}
|
|
3127
3288
|
};
|
|
3128
3289
|
function permissionFingerprint(req) {
|
|
@@ -3133,13 +3294,13 @@ function questionFingerprint(req) {
|
|
|
3133
3294
|
}
|
|
3134
3295
|
|
|
3135
3296
|
// src/gateway/state/db.ts
|
|
3136
|
-
import
|
|
3137
|
-
import
|
|
3297
|
+
import fs2 from "fs";
|
|
3298
|
+
import path2 from "path";
|
|
3138
3299
|
import Database from "better-sqlite3";
|
|
3139
3300
|
var GATEWAY_STATE_VERSION = 1;
|
|
3140
3301
|
function openGatewayState(dbPath) {
|
|
3141
3302
|
if (dbPath !== ":memory:") {
|
|
3142
|
-
|
|
3303
|
+
fs2.mkdirSync(path2.dirname(dbPath), { recursive: true, mode: 448 });
|
|
3143
3304
|
}
|
|
3144
3305
|
const db = new Database(dbPath);
|
|
3145
3306
|
db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -3147,7 +3308,7 @@ function openGatewayState(dbPath) {
|
|
|
3147
3308
|
initGatewayStateSchema(db);
|
|
3148
3309
|
if (dbPath !== ":memory:" && process.platform !== "win32") {
|
|
3149
3310
|
try {
|
|
3150
|
-
|
|
3311
|
+
fs2.chmodSync(dbPath, 384);
|
|
3151
3312
|
} catch {
|
|
3152
3313
|
}
|
|
3153
3314
|
}
|
|
@@ -3202,112 +3363,6 @@ function initGatewayStateSchema(db) {
|
|
|
3202
3363
|
}
|
|
3203
3364
|
}
|
|
3204
3365
|
|
|
3205
|
-
// src/gateway/state/inboundQueue.ts
|
|
3206
|
-
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
3207
|
-
var InboundQueue = class {
|
|
3208
|
-
db;
|
|
3209
|
-
maxEntries;
|
|
3210
|
-
constructor(db, opts = {}) {
|
|
3211
|
-
this.db = db;
|
|
3212
|
-
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
3213
|
-
}
|
|
3214
|
-
size() {
|
|
3215
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM inbound_queue").get();
|
|
3216
|
-
return row.n;
|
|
3217
|
-
}
|
|
3218
|
-
enqueue(inbound) {
|
|
3219
|
-
if (this.size() >= this.maxEntries) {
|
|
3220
|
-
return { kind: "rejected", reason: "queue_full" };
|
|
3221
|
-
}
|
|
3222
|
-
const stmt = this.db.prepare(
|
|
3223
|
-
`INSERT INTO inbound_queue
|
|
3224
|
-
(channel_id, account_id, idempotency_key, payload_json, created_at)
|
|
3225
|
-
VALUES (?, ?, ?, ?, ?)
|
|
3226
|
-
ON CONFLICT(channel_id, account_id, idempotency_key) DO NOTHING`
|
|
3227
|
-
);
|
|
3228
|
-
const result = stmt.run(
|
|
3229
|
-
inbound.location.channelId,
|
|
3230
|
-
inbound.location.accountId,
|
|
3231
|
-
inbound.idempotencyKey,
|
|
3232
|
-
JSON.stringify(inbound),
|
|
3233
|
-
Date.now()
|
|
3234
|
-
);
|
|
3235
|
-
if (result.changes === 0) {
|
|
3236
|
-
return { kind: "duplicate" };
|
|
3237
|
-
}
|
|
3238
|
-
return { kind: "queued", id: Number(result.lastInsertRowid) };
|
|
3239
|
-
}
|
|
3240
|
-
/** Atomically read and remove all parked entries in FIFO order. */
|
|
3241
|
-
drain() {
|
|
3242
|
-
return this.db.transaction(() => {
|
|
3243
|
-
const rows = this.db.prepare("SELECT id, payload_json FROM inbound_queue ORDER BY id ASC").all();
|
|
3244
|
-
if (rows.length > 0) {
|
|
3245
|
-
this.db.prepare("DELETE FROM inbound_queue").run();
|
|
3246
|
-
}
|
|
3247
|
-
return rows.map((r) => ({
|
|
3248
|
-
id: r.id,
|
|
3249
|
-
inbound: JSON.parse(r.payload_json)
|
|
3250
|
-
}));
|
|
3251
|
-
})();
|
|
3252
|
-
}
|
|
3253
|
-
};
|
|
3254
|
-
|
|
3255
|
-
// src/gateway/state/outbox.ts
|
|
3256
|
-
var Outbox = class {
|
|
3257
|
-
db;
|
|
3258
|
-
constructor(db) {
|
|
3259
|
-
this.db = db;
|
|
3260
|
-
}
|
|
3261
|
-
size() {
|
|
3262
|
-
const row = this.db.prepare("SELECT COUNT(*) as n FROM channel_outbox").get();
|
|
3263
|
-
return row.n;
|
|
3264
|
-
}
|
|
3265
|
-
enqueue(input) {
|
|
3266
|
-
const result = this.db.prepare(
|
|
3267
|
-
`INSERT INTO channel_outbox
|
|
3268
|
-
(channel_id, payload_json, attempt, next_attempt_at, last_error, created_at)
|
|
3269
|
-
VALUES (?, ?, 0, ?, ?, ?)`
|
|
3270
|
-
).run(
|
|
3271
|
-
input.channelId,
|
|
3272
|
-
JSON.stringify(input.message),
|
|
3273
|
-
input.nextAttemptAt,
|
|
3274
|
-
input.lastError ?? null,
|
|
3275
|
-
Date.now()
|
|
3276
|
-
);
|
|
3277
|
-
return Number(result.lastInsertRowid);
|
|
3278
|
-
}
|
|
3279
|
-
/** Rows whose `next_attempt_at` is at or before `now`, oldest first. */
|
|
3280
|
-
peekDue(now, limit) {
|
|
3281
|
-
const rows = this.db.prepare(
|
|
3282
|
-
`SELECT id, channel_id, payload_json, attempt, next_attempt_at, last_error
|
|
3283
|
-
FROM channel_outbox
|
|
3284
|
-
WHERE next_attempt_at <= ?
|
|
3285
|
-
ORDER BY next_attempt_at ASC, id ASC
|
|
3286
|
-
LIMIT ?`
|
|
3287
|
-
).all(now, limit);
|
|
3288
|
-
return rows.map((r) => ({
|
|
3289
|
-
id: r.id,
|
|
3290
|
-
channelId: r.channel_id,
|
|
3291
|
-
message: JSON.parse(r.payload_json),
|
|
3292
|
-
attempt: r.attempt,
|
|
3293
|
-
nextAttemptAt: r.next_attempt_at,
|
|
3294
|
-
lastError: r.last_error
|
|
3295
|
-
}));
|
|
3296
|
-
}
|
|
3297
|
-
delete(id) {
|
|
3298
|
-
this.db.prepare("DELETE FROM channel_outbox WHERE id = ?").run(id);
|
|
3299
|
-
}
|
|
3300
|
-
recordFailure(input) {
|
|
3301
|
-
this.db.prepare(
|
|
3302
|
-
`UPDATE channel_outbox
|
|
3303
|
-
SET attempt = attempt + 1,
|
|
3304
|
-
next_attempt_at = ?,
|
|
3305
|
-
last_error = ?
|
|
3306
|
-
WHERE id = ?`
|
|
3307
|
-
).run(input.nextAttemptAt, input.lastError, input.id);
|
|
3308
|
-
}
|
|
3309
|
-
};
|
|
3310
|
-
|
|
3311
3366
|
// src/gateway/transport/tlsWs.ts
|
|
3312
3367
|
import { WebSocketServer } from "ws";
|
|
3313
3368
|
import { createServer as createHttpsServer } from "https";
|
|
@@ -3533,12 +3588,12 @@ async function startDaemon(opts) {
|
|
|
3533
3588
|
const startedAt = Date.now();
|
|
3534
3589
|
const pid = process.pid;
|
|
3535
3590
|
const paths = opts.paths ?? resolveGatewayPaths(opts.env);
|
|
3536
|
-
|
|
3537
|
-
|
|
3591
|
+
fs3.mkdirSync(paths.runDir, { recursive: true, mode: 448 });
|
|
3592
|
+
fs3.mkdirSync(paths.configDir, { recursive: true, mode: 448 });
|
|
3538
3593
|
if (process.platform !== "win32") {
|
|
3539
3594
|
try {
|
|
3540
|
-
|
|
3541
|
-
|
|
3595
|
+
fs3.chmodSync(paths.runDir, 448);
|
|
3596
|
+
fs3.chmodSync(paths.configDir, 448);
|
|
3542
3597
|
} catch {
|
|
3543
3598
|
}
|
|
3544
3599
|
}
|
|
@@ -3552,62 +3607,35 @@ async function startDaemon(opts) {
|
|
|
3552
3607
|
loopback: listenSpec.kind === "uds" || isLoopbackHost(listenSpec.host)
|
|
3553
3608
|
};
|
|
3554
3609
|
const stateDb = openGatewayState(paths.statePath);
|
|
3555
|
-
const inboundQueue = new InboundQueue(stateDb);
|
|
3556
|
-
const outbox = new Outbox(stateDb);
|
|
3557
|
-
const registry = new SessionRegistry();
|
|
3558
3610
|
const channelManager = new ChannelManager();
|
|
3559
3611
|
const relayCoordinator = new RelayCoordinator({
|
|
3560
3612
|
adapters: () => channelManager.listAdapters()
|
|
3561
3613
|
});
|
|
3562
|
-
const runtimeConnections = /* @__PURE__ */ new Map();
|
|
3563
|
-
const staleRuntimeTimers = /* @__PURE__ */ new Map();
|
|
3564
3614
|
const connectionOpenedAt = /* @__PURE__ */ new Map();
|
|
3565
3615
|
const disconnectGracePeriodMs = opts.disconnectGracePeriodMs ?? 0;
|
|
3566
3616
|
let listenerStatus = null;
|
|
3567
|
-
const pushDispatch = (payload) => {
|
|
3568
|
-
const current = registry.getCurrent();
|
|
3569
|
-
if (!current) return;
|
|
3570
|
-
const ctx = runtimeConnections.get(current.runtimeId);
|
|
3571
|
-
if (!ctx) return;
|
|
3572
|
-
ctx.push({
|
|
3573
|
-
push_id: crypto.randomUUID(),
|
|
3574
|
-
ts: Date.now(),
|
|
3575
|
-
kind: "session.dispatch.turn",
|
|
3576
|
-
payload
|
|
3577
|
-
});
|
|
3578
|
-
};
|
|
3579
3617
|
const log = (level, message) => {
|
|
3580
3618
|
if (opts.silent) return;
|
|
3581
3619
|
const stream = level === "error" || level === "warn" ? "stderr" : "stdout";
|
|
3582
3620
|
process[stream].write(`athena-gateway: [${level}] ${message}
|
|
3583
3621
|
`);
|
|
3584
3622
|
};
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3623
|
+
const pipeline = new DispatchPipeline({
|
|
3624
|
+
stateDb,
|
|
3587
3625
|
send: (channelId, msg) => channelManager.send(channelId, msg),
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
},
|
|
3598
|
-
sendOutbound: async (channelId, msg) => {
|
|
3599
|
-
const result = await outboundDispatcher.dispatch(channelId, msg);
|
|
3600
|
-
if (result.kind === "sent") return result.result;
|
|
3601
|
-
return {
|
|
3602
|
-
providerMessageId: `outbox:${result.outboxId}`,
|
|
3603
|
-
deliveredAt: Date.now()
|
|
3604
|
-
};
|
|
3605
|
-
},
|
|
3606
|
-
inboundQueue,
|
|
3607
|
-
log
|
|
3626
|
+
gracePeriodMs: disconnectGracePeriodMs,
|
|
3627
|
+
log,
|
|
3628
|
+
observers: {
|
|
3629
|
+
onRuntimeRebind: ({ gapMs, epoch }) => trackGatewayRuntimeRebind({ gapMs, epoch }),
|
|
3630
|
+
onRuntimeExpired: ({ gapMs }) => trackGatewayRuntimeExpired({ gapMs }),
|
|
3631
|
+
// Single-runtime v1: blanket dispose is safe. Multi-runtime must
|
|
3632
|
+
// scope to the disconnecting runtime via disposeAllForRuntime.
|
|
3633
|
+
onRuntimeConnectionLost: () => relayCoordinator.disposeAll("connection_lost")
|
|
3634
|
+
}
|
|
3608
3635
|
});
|
|
3636
|
+
pipeline.start();
|
|
3609
3637
|
channelManager.setInboundSink((inbound) => {
|
|
3610
|
-
|
|
3638
|
+
pipeline.handleInbound(inbound);
|
|
3611
3639
|
});
|
|
3612
3640
|
const channelConfigHome = opts.env?.HOME;
|
|
3613
3641
|
const reloadChannels = async () => {
|
|
@@ -3721,45 +3749,11 @@ async function startDaemon(opts) {
|
|
|
3721
3749
|
}
|
|
3722
3750
|
const handler = createDispatcher({
|
|
3723
3751
|
startedAt,
|
|
3724
|
-
|
|
3725
|
-
dispatcher,
|
|
3752
|
+
pipeline,
|
|
3726
3753
|
channelManager,
|
|
3727
3754
|
relayCoordinator,
|
|
3728
3755
|
getListener: () => listenerStatus ?? buildListenerStatus(listenSpec, null),
|
|
3729
|
-
reloadChannels
|
|
3730
|
-
registerRuntimeConnection: (runtimeId, ctx) => {
|
|
3731
|
-
const timer = staleRuntimeTimers.get(runtimeId);
|
|
3732
|
-
if (timer) {
|
|
3733
|
-
clearTimeout(timer);
|
|
3734
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3735
|
-
}
|
|
3736
|
-
const previousBinding = registry.getBinding();
|
|
3737
|
-
const wasStale = previousBinding?.state === "stale";
|
|
3738
|
-
const staleSince = wasStale ? previousBinding.staleSince : null;
|
|
3739
|
-
registry.bindConnection(runtimeId, ctx.connectionId);
|
|
3740
|
-
runtimeConnections.set(runtimeId, ctx);
|
|
3741
|
-
writeGatewayTrace(
|
|
3742
|
-
`daemon registered runtime runtimeId=${runtimeId} connectionId=${ctx.connectionId}`
|
|
3743
|
-
);
|
|
3744
|
-
if (wasStale && staleSince !== null) {
|
|
3745
|
-
const newBinding = registry.getBinding();
|
|
3746
|
-
if (newBinding?.state === "active") {
|
|
3747
|
-
trackGatewayRuntimeRebind({
|
|
3748
|
-
gapMs: Date.now() - staleSince,
|
|
3749
|
-
epoch: newBinding.epoch
|
|
3750
|
-
});
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
},
|
|
3754
|
-
unregisterRuntimeConnection: (runtimeId) => {
|
|
3755
|
-
const timer = staleRuntimeTimers.get(runtimeId);
|
|
3756
|
-
if (timer) {
|
|
3757
|
-
clearTimeout(timer);
|
|
3758
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3759
|
-
}
|
|
3760
|
-
runtimeConnections.delete(runtimeId);
|
|
3761
|
-
writeGatewayTrace(`daemon unregistered runtime runtimeId=${runtimeId}`);
|
|
3762
|
-
}
|
|
3756
|
+
reloadChannels
|
|
3763
3757
|
});
|
|
3764
3758
|
let server;
|
|
3765
3759
|
let listener;
|
|
@@ -3793,39 +3787,7 @@ async function startDaemon(opts) {
|
|
|
3793
3787
|
reason: "closed",
|
|
3794
3788
|
durationMs
|
|
3795
3789
|
});
|
|
3796
|
-
|
|
3797
|
-
if (current && runtimeConnections.get(current.runtimeId)?.connectionId === ctx.connectionId) {
|
|
3798
|
-
writeGatewayTrace(
|
|
3799
|
-
`daemon runtime connection disconnected runtimeId=${current.runtimeId} connectionId=${ctx.connectionId}`
|
|
3800
|
-
);
|
|
3801
|
-
runtimeConnections.delete(current.runtimeId);
|
|
3802
|
-
const staleAt = Date.now();
|
|
3803
|
-
registry.markConnectionStale(ctx.connectionId);
|
|
3804
|
-
if (disconnectGracePeriodMs <= 0) {
|
|
3805
|
-
try {
|
|
3806
|
-
registry.unregister(current.runtimeId);
|
|
3807
|
-
} catch {
|
|
3808
|
-
}
|
|
3809
|
-
relayCoordinator.disposeAll("connection_lost");
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
const runtimeId = current.runtimeId;
|
|
3813
|
-
const timer = setTimeout(() => {
|
|
3814
|
-
staleRuntimeTimers.delete(runtimeId);
|
|
3815
|
-
const latest = registry.getCurrent();
|
|
3816
|
-
if (latest?.runtimeId === runtimeId && !registry.hasActiveBinding(runtimeId)) {
|
|
3817
|
-
try {
|
|
3818
|
-
registry.unregister(runtimeId);
|
|
3819
|
-
} catch {
|
|
3820
|
-
}
|
|
3821
|
-
relayCoordinator.disposeAll("connection_lost");
|
|
3822
|
-
trackGatewayRuntimeExpired({
|
|
3823
|
-
gapMs: Date.now() - staleAt
|
|
3824
|
-
});
|
|
3825
|
-
}
|
|
3826
|
-
}, disconnectGracePeriodMs);
|
|
3827
|
-
staleRuntimeTimers.set(runtimeId, timer);
|
|
3828
|
-
}
|
|
3790
|
+
pipeline.notifyConnectionClosed(ctx.connectionId);
|
|
3829
3791
|
}
|
|
3830
3792
|
});
|
|
3831
3793
|
if (listenSpec.kind === "tcp") {
|
|
@@ -3861,11 +3823,7 @@ async function startDaemon(opts) {
|
|
|
3861
3823
|
if (stopping) return;
|
|
3862
3824
|
stopping = true;
|
|
3863
3825
|
try {
|
|
3864
|
-
|
|
3865
|
-
for (const timer of staleRuntimeTimers.values()) {
|
|
3866
|
-
clearTimeout(timer);
|
|
3867
|
-
}
|
|
3868
|
-
staleRuntimeTimers.clear();
|
|
3826
|
+
await pipeline.stop();
|
|
3869
3827
|
relayCoordinator.disposeAll("auto_resolved");
|
|
3870
3828
|
await channelManager.stop();
|
|
3871
3829
|
await server.close();
|
|
@@ -3890,13 +3848,9 @@ async function startDaemon(opts) {
|
|
|
3890
3848
|
startedAt,
|
|
3891
3849
|
pid,
|
|
3892
3850
|
paths,
|
|
3893
|
-
|
|
3894
|
-
dispatcher,
|
|
3851
|
+
pipeline,
|
|
3895
3852
|
channelManager,
|
|
3896
3853
|
relayCoordinator,
|
|
3897
|
-
inboundQueue,
|
|
3898
|
-
outbox,
|
|
3899
|
-
outboundDispatcher,
|
|
3900
3854
|
listener,
|
|
3901
3855
|
stop
|
|
3902
3856
|
};
|