@drisp/cli 0.4.2 → 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-NDWLVIFI.js → WorkflowInstallWizard-2MC5A7W4.js} +2 -2
- package/dist/athena-gateway.js +910 -870
- package/dist/{chunk-GE6PPB6Z.js → chunk-5VK2ZMVV.js} +96 -119
- package/dist/{chunk-WHELLVBL.js → chunk-JAPBSL7D.js} +1177 -640
- package/dist/cli.js +1004 -1060
- package/dist/dashboard-daemon.js +2 -2
- package/package.json +4 -2
package/dist/athena-gateway.js
CHANGED
|
@@ -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;
|
|
@@ -1873,8 +1923,7 @@ var ChannelManager = class {
|
|
|
1873
1923
|
// src/gateway/control/handlers.ts
|
|
1874
1924
|
import { createRequire } from "module";
|
|
1875
1925
|
|
|
1876
|
-
// src/gateway/
|
|
1877
|
-
import { randomUUID } from "crypto";
|
|
1926
|
+
// src/gateway/runtimeBindingStore.ts
|
|
1878
1927
|
var AlreadyRegisteredError = class extends Error {
|
|
1879
1928
|
code = "already_registered";
|
|
1880
1929
|
constructor(existing) {
|
|
@@ -1891,120 +1940,145 @@ var NotRegisteredError = class extends Error {
|
|
|
1891
1940
|
this.name = "NotRegisteredError";
|
|
1892
1941
|
}
|
|
1893
1942
|
};
|
|
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
1943
|
function maybeLastRebindAt(value) {
|
|
1902
1944
|
return value !== void 0 ? { lastRebindAt: value } : {};
|
|
1903
1945
|
}
|
|
1904
|
-
var
|
|
1946
|
+
var RuntimeBindingStore = class {
|
|
1905
1947
|
current = null;
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1948
|
+
bindingState = null;
|
|
1949
|
+
staleTimer = null;
|
|
1950
|
+
staleSince = null;
|
|
1951
|
+
gracePeriodMs;
|
|
1952
|
+
observers;
|
|
1909
1953
|
now;
|
|
1910
1954
|
constructor(opts = {}) {
|
|
1911
|
-
this.
|
|
1955
|
+
this.gracePeriodMs = opts.gracePeriodMs ?? 0;
|
|
1956
|
+
this.observers = opts.observers ?? {};
|
|
1912
1957
|
this.now = opts.now ?? Date.now;
|
|
1913
1958
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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 {
|
|
1924
1979
|
throw new AlreadyRegisteredError(this.current);
|
|
1925
1980
|
}
|
|
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
1981
|
const now = this.now();
|
|
1935
|
-
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== connectionId);
|
|
1982
|
+
const isRebind = previous !== null && (previous.state === "stale" || previous.connectionId !== input.connectionId);
|
|
1936
1983
|
const lastRebindAt = isRebind ? now : previous?.lastRebindAt;
|
|
1937
1984
|
const epoch = previous ? previous.epoch + (isRebind ? 1 : 0) : 1;
|
|
1938
|
-
this.
|
|
1985
|
+
this.bindingState = {
|
|
1939
1986
|
state: "active",
|
|
1940
|
-
connectionId,
|
|
1987
|
+
connectionId: input.connectionId,
|
|
1941
1988
|
boundAt: now,
|
|
1942
1989
|
epoch,
|
|
1943
1990
|
...maybeLastRebindAt(lastRebindAt)
|
|
1944
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();
|
|
1945
2010
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
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) {
|
|
1948
2018
|
return null;
|
|
1949
2019
|
}
|
|
1950
|
-
|
|
2020
|
+
const runtimeId = this.current.runtimeId;
|
|
2021
|
+
const now = this.now();
|
|
2022
|
+
this.bindingState = {
|
|
1951
2023
|
state: "stale",
|
|
1952
2024
|
connectionId,
|
|
1953
|
-
staleSince:
|
|
1954
|
-
epoch: this.
|
|
1955
|
-
...maybeLastRebindAt(this.
|
|
2025
|
+
staleSince: now,
|
|
2026
|
+
epoch: this.bindingState.epoch,
|
|
2027
|
+
...maybeLastRebindAt(this.bindingState.lastRebindAt)
|
|
1956
2028
|
};
|
|
1957
|
-
|
|
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();
|
|
1958
2043
|
}
|
|
2044
|
+
// ── reads ─────────────────────────────────────────────────
|
|
1959
2045
|
hasActiveBinding(runtimeId) {
|
|
1960
|
-
if (!this.current || !this.
|
|
2046
|
+
if (!this.current || !this.bindingState || this.bindingState.state !== "active") {
|
|
1961
2047
|
return false;
|
|
1962
2048
|
}
|
|
1963
2049
|
return runtimeId === void 0 || this.current.runtimeId === runtimeId;
|
|
1964
2050
|
}
|
|
2051
|
+
getCurrent() {
|
|
2052
|
+
return this.current;
|
|
2053
|
+
}
|
|
1965
2054
|
getBinding() {
|
|
1966
|
-
return this.
|
|
2055
|
+
return this.bindingState;
|
|
1967
2056
|
}
|
|
1968
2057
|
getRuntimeIdByConnection(connectionId) {
|
|
1969
|
-
if (!this.current || !this.
|
|
1970
|
-
return this.
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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;
|
|
1975
2068
|
}
|
|
1976
2069
|
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();
|
|
2070
|
+
this.bindingState = null;
|
|
2071
|
+
this.observers.onRuntimeConnectionLost?.({ runtimeId, graceful: false });
|
|
2072
|
+
if (since !== null) {
|
|
2073
|
+
this.observers.onRuntimeExpired?.({ runtimeId, gapMs: this.now() - since });
|
|
1986
2074
|
}
|
|
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
2075
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2076
|
+
clearStaleTimer() {
|
|
2077
|
+
if (this.staleTimer) {
|
|
2078
|
+
clearTimeout(this.staleTimer);
|
|
2079
|
+
this.staleTimer = null;
|
|
2002
2080
|
}
|
|
2003
|
-
this.
|
|
2004
|
-
return entry;
|
|
2005
|
-
}
|
|
2006
|
-
pendingDispatchCount() {
|
|
2007
|
-
return this.dispatches.size;
|
|
2081
|
+
this.staleSince = null;
|
|
2008
2082
|
}
|
|
2009
2083
|
};
|
|
2010
2084
|
|
|
@@ -2014,7 +2088,7 @@ var cachedVersion = null;
|
|
|
2014
2088
|
function readVersion() {
|
|
2015
2089
|
if (cachedVersion !== null) return cachedVersion;
|
|
2016
2090
|
try {
|
|
2017
|
-
const injected = "0.4.
|
|
2091
|
+
const injected = "0.4.4";
|
|
2018
2092
|
if (typeof injected === "string" && injected.length > 0) {
|
|
2019
2093
|
cachedVersion = injected;
|
|
2020
2094
|
return cachedVersion;
|
|
@@ -2029,273 +2103,258 @@ function readVersion() {
|
|
|
2029
2103
|
}
|
|
2030
2104
|
return cachedVersion;
|
|
2031
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
|
+
);
|
|
2032
2301
|
function createDispatcher(deps) {
|
|
2033
2302
|
const handle = async (envelope, connection) => {
|
|
2034
2303
|
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)
|
|
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) {
|
|
2260
2316
|
return error(
|
|
2261
2317
|
envelope,
|
|
2262
2318
|
ts,
|
|
2263
2319
|
"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"
|
|
2320
|
+
spec.unsupportedMessage ?? `${spec.kind} not configured`
|
|
2274
2321
|
);
|
|
2275
2322
|
}
|
|
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
2323
|
}
|
|
2284
|
-
|
|
2324
|
+
}
|
|
2325
|
+
let callerRuntimeId;
|
|
2326
|
+
if (spec.requireRegisteredRuntime) {
|
|
2327
|
+
callerRuntimeId = deps.pipeline?.getRuntimeIdByConnection(connection.connectionId) ?? void 0;
|
|
2328
|
+
if (deps.pipeline && callerRuntimeId === void 0) {
|
|
2285
2329
|
return error(
|
|
2286
2330
|
envelope,
|
|
2287
2331
|
ts,
|
|
2288
|
-
"
|
|
2289
|
-
|
|
2332
|
+
"not_registered",
|
|
2333
|
+
`${spec.kind} requires a registered runtime connection`
|
|
2290
2334
|
);
|
|
2335
|
+
}
|
|
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);
|
|
2348
|
+
}
|
|
2349
|
+
throw err;
|
|
2291
2350
|
}
|
|
2292
2351
|
};
|
|
2293
2352
|
return handle;
|
|
2294
2353
|
}
|
|
2295
|
-
function runtimeStatusEntries(
|
|
2296
|
-
const runtime =
|
|
2297
|
-
if (!runtime || !
|
|
2298
|
-
const binding =
|
|
2354
|
+
function runtimeStatusEntries(pipeline) {
|
|
2355
|
+
const runtime = pipeline?.getCurrentRuntime();
|
|
2356
|
+
if (!runtime || !pipeline) return [];
|
|
2357
|
+
const binding = pipeline.getBinding();
|
|
2299
2358
|
return [
|
|
2300
2359
|
{
|
|
2301
2360
|
runtimeId: runtime.runtimeId,
|
|
@@ -2313,7 +2372,7 @@ function runtimeStatusEntries(registry) {
|
|
|
2313
2372
|
epoch: binding.epoch,
|
|
2314
2373
|
...maybeLastRebindAt(binding.lastRebindAt)
|
|
2315
2374
|
} : { state: "none" },
|
|
2316
|
-
pendingDispatchCount:
|
|
2375
|
+
pendingDispatchCount: pipeline.pendingDispatchCount()
|
|
2317
2376
|
}
|
|
2318
2377
|
];
|
|
2319
2378
|
}
|
|
@@ -2436,256 +2495,8 @@ function handleConnection(connection, expectedToken, startedAt, handler, logErro
|
|
|
2436
2495
|
});
|
|
2437
2496
|
}
|
|
2438
2497
|
|
|
2439
|
-
// src/gateway/
|
|
2440
|
-
|
|
2441
|
-
const c = loc.channelId;
|
|
2442
|
-
const a = loc.accountId;
|
|
2443
|
-
if (loc.peer?.id) {
|
|
2444
|
-
const peer = loc.peer.id;
|
|
2445
|
-
if (loc.thread?.id) {
|
|
2446
|
-
return `peer:${c}:${a}:${peer}:${loc.thread.id}`;
|
|
2447
|
-
}
|
|
2448
|
-
return `peer:${c}:${a}:${peer}`;
|
|
2449
|
-
}
|
|
2450
|
-
if (loc.room?.id) {
|
|
2451
|
-
const room = loc.room.id;
|
|
2452
|
-
if (loc.thread?.id) {
|
|
2453
|
-
return `room:${c}:${a}:${room}:${loc.thread.id}`;
|
|
2454
|
-
}
|
|
2455
|
-
return `room:${c}:${a}:${room}`;
|
|
2456
|
-
}
|
|
2457
|
-
return `default:${c}:${a}`;
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
// src/gateway/dispatcher.ts
|
|
2461
|
-
var Dispatcher = class {
|
|
2462
|
-
registry;
|
|
2463
|
-
pushDispatch;
|
|
2464
|
-
sendOutbound;
|
|
2465
|
-
resolveAgent;
|
|
2466
|
-
inboundQueue;
|
|
2467
|
-
canDispatch;
|
|
2468
|
-
log;
|
|
2469
|
-
constructor(opts) {
|
|
2470
|
-
this.registry = opts.registry;
|
|
2471
|
-
this.pushDispatch = opts.pushDispatch;
|
|
2472
|
-
this.sendOutbound = opts.sendOutbound;
|
|
2473
|
-
this.resolveAgent = opts.resolveAgent ?? ((input) => input.defaultAgentId);
|
|
2474
|
-
this.inboundQueue = opts.inboundQueue;
|
|
2475
|
-
this.canDispatch = opts.canDispatch ?? (() => true);
|
|
2476
|
-
this.log = opts.log;
|
|
2477
|
-
}
|
|
2478
|
-
handleInbound(inbound) {
|
|
2479
|
-
const current = this.registry.getCurrent();
|
|
2480
|
-
if (!current || !this.canDispatch()) {
|
|
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
|
-
}
|
|
2488
|
-
const result = this.inboundQueue.enqueue(inbound);
|
|
2489
|
-
if (result.kind === "queued") {
|
|
2490
|
-
this.log?.(
|
|
2491
|
-
"info",
|
|
2492
|
-
`no runtime registered; parked inbound ${inbound.idempotencyKey} as queue#${result.id}`
|
|
2493
|
-
);
|
|
2494
|
-
return { kind: "queued", queueId: result.id };
|
|
2495
|
-
}
|
|
2496
|
-
if (result.kind === "duplicate") {
|
|
2497
|
-
this.log?.(
|
|
2498
|
-
"debug",
|
|
2499
|
-
`inbound ${inbound.idempotencyKey} already parked; ignoring duplicate`
|
|
2500
|
-
);
|
|
2501
|
-
return { kind: "dropped", reason: "no_runtime" };
|
|
2502
|
-
}
|
|
2503
|
-
this.log?.(
|
|
2504
|
-
"warn",
|
|
2505
|
-
`inbound queue full (>=${this.inboundQueue.size()}); dropping ${inbound.idempotencyKey}`
|
|
2506
|
-
);
|
|
2507
|
-
return { kind: "dropped", reason: "queue_full" };
|
|
2508
|
-
}
|
|
2509
|
-
const sessionKey = deriveSessionKey(inbound.location);
|
|
2510
|
-
const agentId = this.resolveAgent({
|
|
2511
|
-
sessionKey,
|
|
2512
|
-
channelId: inbound.location.channelId,
|
|
2513
|
-
defaultAgentId: current.defaultAgentId
|
|
2514
|
-
});
|
|
2515
|
-
if (!agentId) {
|
|
2516
|
-
this.log?.(
|
|
2517
|
-
"warn",
|
|
2518
|
-
`agent resolution returned empty for sessionKey=${sessionKey}`
|
|
2519
|
-
);
|
|
2520
|
-
return { kind: "dropped", reason: "no_default_agent" };
|
|
2521
|
-
}
|
|
2522
|
-
const entry = this.registry.beginDispatch({
|
|
2523
|
-
sessionKey,
|
|
2524
|
-
agentId,
|
|
2525
|
-
location: inbound.location
|
|
2526
|
-
});
|
|
2527
|
-
this.pushDispatch({
|
|
2528
|
-
dispatchId: entry.dispatchId,
|
|
2529
|
-
sessionKey,
|
|
2530
|
-
agentId,
|
|
2531
|
-
inbound
|
|
2532
|
-
});
|
|
2533
|
-
return {
|
|
2534
|
-
kind: "dispatched",
|
|
2535
|
-
dispatchId: entry.dispatchId,
|
|
2536
|
-
sessionKey
|
|
2537
|
-
};
|
|
2538
|
-
}
|
|
2539
|
-
/**
|
|
2540
|
-
* Drain parked inbound messages and dispatch them in FIFO order. Called by
|
|
2541
|
-
* the session.register handler after a runtime attaches. Safe to call when
|
|
2542
|
-
* no queue is configured (no-op).
|
|
2543
|
-
*/
|
|
2544
|
-
drainPending() {
|
|
2545
|
-
if (!this.inboundQueue) return { dispatched: 0, dropped: 0 };
|
|
2546
|
-
const current = this.registry.getCurrent();
|
|
2547
|
-
if (!current || !this.canDispatch()) return { dispatched: 0, dropped: 0 };
|
|
2548
|
-
const parked = this.inboundQueue.drain();
|
|
2549
|
-
let dispatched = 0;
|
|
2550
|
-
let dropped = 0;
|
|
2551
|
-
for (const { inbound } of parked) {
|
|
2552
|
-
const sessionKey = deriveSessionKey(inbound.location);
|
|
2553
|
-
const agentId = this.resolveAgent({
|
|
2554
|
-
sessionKey,
|
|
2555
|
-
channelId: inbound.location.channelId,
|
|
2556
|
-
defaultAgentId: current.defaultAgentId
|
|
2557
|
-
});
|
|
2558
|
-
if (!agentId) {
|
|
2559
|
-
dropped += 1;
|
|
2560
|
-
this.log?.(
|
|
2561
|
-
"warn",
|
|
2562
|
-
`drainPending: no agent for ${sessionKey}; dropping ${inbound.idempotencyKey}`
|
|
2563
|
-
);
|
|
2564
|
-
continue;
|
|
2565
|
-
}
|
|
2566
|
-
const entry = this.registry.beginDispatch({
|
|
2567
|
-
sessionKey,
|
|
2568
|
-
agentId,
|
|
2569
|
-
location: inbound.location
|
|
2570
|
-
});
|
|
2571
|
-
this.pushDispatch({
|
|
2572
|
-
dispatchId: entry.dispatchId,
|
|
2573
|
-
sessionKey,
|
|
2574
|
-
agentId,
|
|
2575
|
-
inbound
|
|
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 };
|
|
2586
|
-
}
|
|
2587
|
-
async handleTurnComplete(payload) {
|
|
2588
|
-
const current = this.registry.getCurrent();
|
|
2589
|
-
writeGatewayTrace(
|
|
2590
|
-
`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}`
|
|
2591
|
-
);
|
|
2592
|
-
if (!current || current.runtimeId !== payload.runtimeId) {
|
|
2593
|
-
throw new Error("runtime mismatch on session.turn.complete");
|
|
2594
|
-
}
|
|
2595
|
-
let entry;
|
|
2596
|
-
try {
|
|
2597
|
-
entry = this.registry.completeDispatch(payload.dispatchId);
|
|
2598
|
-
} catch (err) {
|
|
2599
|
-
if (err instanceof UnknownDispatchError) {
|
|
2600
|
-
writeGatewayTrace(
|
|
2601
|
-
`dispatcher turn.complete unknown dispatchId=${payload.dispatchId}`
|
|
2602
|
-
);
|
|
2603
|
-
return { delivered: false };
|
|
2604
|
-
}
|
|
2605
|
-
throw err;
|
|
2606
|
-
}
|
|
2607
|
-
writeGatewayTrace(
|
|
2608
|
-
`dispatcher sendOutbound channel=${entry.location.channelId} dispatchId=${payload.dispatchId} parkedAccount=${entry.location.accountId} parkedThread=${entry.location.thread?.id ?? ""}`
|
|
2609
|
-
);
|
|
2610
|
-
const result = await this.sendOutbound(entry.location.channelId, {
|
|
2611
|
-
location: payload.location,
|
|
2612
|
-
text: payload.text,
|
|
2613
|
-
idempotencyKey: payload.idempotencyKey
|
|
2614
|
-
});
|
|
2615
|
-
writeGatewayTrace(
|
|
2616
|
-
`dispatcher sendOutbound delivered dispatchId=${payload.dispatchId} providerMessageId=${result.providerMessageId}`
|
|
2617
|
-
);
|
|
2618
|
-
return {
|
|
2619
|
-
delivered: true,
|
|
2620
|
-
providerMessageId: result.providerMessageId
|
|
2621
|
-
};
|
|
2622
|
-
}
|
|
2623
|
-
};
|
|
2624
|
-
|
|
2625
|
-
// src/gateway/lock.ts
|
|
2626
|
-
import fs from "fs";
|
|
2627
|
-
import path from "path";
|
|
2628
|
-
var GatewayAlreadyRunningError = class extends Error {
|
|
2629
|
-
otherPid;
|
|
2630
|
-
constructor(otherPid) {
|
|
2631
|
-
super(`gateway already running (pid=${otherPid})`);
|
|
2632
|
-
this.name = "GatewayAlreadyRunningError";
|
|
2633
|
-
this.otherPid = otherPid;
|
|
2634
|
-
}
|
|
2635
|
-
};
|
|
2636
|
-
function isProcessAlive(pid) {
|
|
2637
|
-
try {
|
|
2638
|
-
process.kill(pid, 0);
|
|
2639
|
-
return true;
|
|
2640
|
-
} catch (err) {
|
|
2641
|
-
const code = err.code;
|
|
2642
|
-
return code === "EPERM";
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
function readPidFile(p) {
|
|
2646
|
-
try {
|
|
2647
|
-
const text = fs.readFileSync(p, "utf-8").trim();
|
|
2648
|
-
const pid = Number.parseInt(text, 10);
|
|
2649
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2650
|
-
} catch {
|
|
2651
|
-
return null;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
function acquireLock(lockPath) {
|
|
2655
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
2656
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2657
|
-
try {
|
|
2658
|
-
const fd = fs.openSync(lockPath, "wx", 384);
|
|
2659
|
-
fs.writeSync(fd, String(process.pid) + "\n");
|
|
2660
|
-
fs.closeSync(fd);
|
|
2661
|
-
return {
|
|
2662
|
-
path: lockPath,
|
|
2663
|
-
pid: process.pid,
|
|
2664
|
-
release: () => {
|
|
2665
|
-
try {
|
|
2666
|
-
const pidNow = readPidFile(lockPath);
|
|
2667
|
-
if (pidNow === process.pid) {
|
|
2668
|
-
fs.unlinkSync(lockPath);
|
|
2669
|
-
}
|
|
2670
|
-
} catch {
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
};
|
|
2674
|
-
} catch (err) {
|
|
2675
|
-
const code = err.code;
|
|
2676
|
-
if (code !== "EEXIST") throw err;
|
|
2677
|
-
const otherPid = readPidFile(lockPath);
|
|
2678
|
-
if (otherPid !== null && isProcessAlive(otherPid)) {
|
|
2679
|
-
throw new GatewayAlreadyRunningError(otherPid);
|
|
2680
|
-
}
|
|
2681
|
-
try {
|
|
2682
|
-
fs.unlinkSync(lockPath);
|
|
2683
|
-
} catch {
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
throw new Error(`failed to acquire gateway lock at ${lockPath}`);
|
|
2688
|
-
}
|
|
2498
|
+
// src/gateway/dispatchPipeline.ts
|
|
2499
|
+
import crypto from "crypto";
|
|
2689
2500
|
|
|
2690
2501
|
// src/gateway/outboundDispatcher.ts
|
|
2691
2502
|
var DEFAULT_BACKOFF = [
|
|
@@ -2784,43 +2595,504 @@ var OutboundDispatcher = class {
|
|
|
2784
2595
|
} finally {
|
|
2785
2596
|
this.draining = false;
|
|
2786
2597
|
}
|
|
2787
|
-
return { retried, succeeded, dropped };
|
|
2788
|
-
}
|
|
2789
|
-
async attempt(row) {
|
|
2790
|
-
try {
|
|
2791
|
-
await this.send(row.channelId, row.message);
|
|
2792
|
-
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) {
|
|
2793
3002
|
this.log?.(
|
|
2794
3003
|
"info",
|
|
2795
|
-
`
|
|
3004
|
+
`drainPending: dispatched=${dispatched} dropped=${dropped}`
|
|
2796
3005
|
);
|
|
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";
|
|
2816
3006
|
}
|
|
2817
3007
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
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;
|
|
2822
3041
|
}
|
|
2823
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
|
+
}
|
|
2824
3096
|
|
|
2825
3097
|
// src/gateway/relay/coordinator.ts
|
|
2826
3098
|
var DEFAULT_RELAY_TTL_MS = 5 * 6e4;
|
|
@@ -2874,10 +3146,7 @@ var RelayCoordinator = class {
|
|
|
2874
3146
|
resolveFn = resolve;
|
|
2875
3147
|
});
|
|
2876
3148
|
const timer = setTimeout(() => {
|
|
2877
|
-
this.
|
|
2878
|
-
kind: "cancelled",
|
|
2879
|
-
reason: "timeout"
|
|
2880
|
-
});
|
|
3149
|
+
this.settle(channelRequestId, { kind: "cancelled", reason: "timeout" });
|
|
2881
3150
|
}, ttlMs);
|
|
2882
3151
|
if (typeof timer.unref === "function") timer.unref();
|
|
2883
3152
|
const entry = {
|
|
@@ -2902,10 +3171,7 @@ var RelayCoordinator = class {
|
|
|
2902
3171
|
const ctrl = controllers[idx];
|
|
2903
3172
|
Promise.resolve().then(() => adapter.requestPermissionVerdict(fullReq, ctrl.signal)).then((res) => {
|
|
2904
3173
|
if (res.kind === "verdict") {
|
|
2905
|
-
this.
|
|
2906
|
-
...res,
|
|
2907
|
-
channelId: adapter.id
|
|
2908
|
-
});
|
|
3174
|
+
this.settle(channelRequestId, { ...res, channelId: adapter.id });
|
|
2909
3175
|
}
|
|
2910
3176
|
}).catch((err) => {
|
|
2911
3177
|
this.log?.(
|
|
@@ -2954,10 +3220,7 @@ var RelayCoordinator = class {
|
|
|
2954
3220
|
resolveFn = resolve;
|
|
2955
3221
|
});
|
|
2956
3222
|
const timer = setTimeout(() => {
|
|
2957
|
-
this.
|
|
2958
|
-
kind: "cancelled",
|
|
2959
|
-
reason: "timeout"
|
|
2960
|
-
});
|
|
3223
|
+
this.settle(channelRequestId, { kind: "cancelled", reason: "timeout" });
|
|
2961
3224
|
}, ttlMs);
|
|
2962
3225
|
if (typeof timer.unref === "function") timer.unref();
|
|
2963
3226
|
const entry = {
|
|
@@ -2981,10 +3244,7 @@ var RelayCoordinator = class {
|
|
|
2981
3244
|
const ctrl = controllers[idx];
|
|
2982
3245
|
Promise.resolve().then(() => adapter.requestQuestionAnswer(fullReq, ctrl.signal)).then((res) => {
|
|
2983
3246
|
if (res.kind === "answer") {
|
|
2984
|
-
this.
|
|
2985
|
-
...res,
|
|
2986
|
-
channelId: adapter.id
|
|
2987
|
-
});
|
|
3247
|
+
this.settle(channelRequestId, { ...res, channelId: adapter.id });
|
|
2988
3248
|
}
|
|
2989
3249
|
}).catch((err) => {
|
|
2990
3250
|
this.log?.(
|
|
@@ -3001,11 +3261,7 @@ var RelayCoordinator = class {
|
|
|
3001
3261
|
if (expectedRuntimeId !== void 0 && entry.runtimeId !== void 0 && entry.runtimeId !== expectedRuntimeId) {
|
|
3002
3262
|
return false;
|
|
3003
3263
|
}
|
|
3004
|
-
|
|
3005
|
-
this.settlePermission(channelRequestId, { kind: "cancelled", reason });
|
|
3006
|
-
} else {
|
|
3007
|
-
this.settleQuestion(channelRequestId, { kind: "cancelled", reason });
|
|
3008
|
-
}
|
|
3264
|
+
this.settle(channelRequestId, { kind: "cancelled", reason });
|
|
3009
3265
|
return true;
|
|
3010
3266
|
}
|
|
3011
3267
|
pendingCount() {
|
|
@@ -3016,27 +3272,18 @@ var RelayCoordinator = class {
|
|
|
3016
3272
|
this.cancel(id, reason);
|
|
3017
3273
|
}
|
|
3018
3274
|
}
|
|
3019
|
-
|
|
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) {
|
|
3275
|
+
settle(channelRequestId, result) {
|
|
3031
3276
|
const entry = this.pending.get(channelRequestId);
|
|
3032
|
-
if (!entry || entry.
|
|
3277
|
+
if (!entry || entry.settled) return;
|
|
3033
3278
|
entry.settled = true;
|
|
3034
3279
|
this.pending.delete(channelRequestId);
|
|
3035
3280
|
clearTimeout(entry.timer);
|
|
3036
3281
|
for (const ctrl of entry.controllers) {
|
|
3037
3282
|
if (!ctrl.signal.aborted) ctrl.abort();
|
|
3038
3283
|
}
|
|
3039
|
-
entry.resolve(
|
|
3284
|
+
entry.resolve(
|
|
3285
|
+
result
|
|
3286
|
+
);
|
|
3040
3287
|
}
|
|
3041
3288
|
};
|
|
3042
3289
|
function permissionFingerprint(req) {
|
|
@@ -3116,112 +3363,6 @@ function initGatewayStateSchema(db) {
|
|
|
3116
3363
|
}
|
|
3117
3364
|
}
|
|
3118
3365
|
|
|
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
3366
|
// src/gateway/transport/tlsWs.ts
|
|
3226
3367
|
import { WebSocketServer } from "ws";
|
|
3227
3368
|
import { createServer as createHttpsServer } from "https";
|
|
@@ -3466,62 +3607,35 @@ async function startDaemon(opts) {
|
|
|
3466
3607
|
loopback: listenSpec.kind === "uds" || isLoopbackHost(listenSpec.host)
|
|
3467
3608
|
};
|
|
3468
3609
|
const stateDb = openGatewayState(paths.statePath);
|
|
3469
|
-
const inboundQueue = new InboundQueue(stateDb);
|
|
3470
|
-
const outbox = new Outbox(stateDb);
|
|
3471
|
-
const registry = new SessionRegistry();
|
|
3472
3610
|
const channelManager = new ChannelManager();
|
|
3473
3611
|
const relayCoordinator = new RelayCoordinator({
|
|
3474
3612
|
adapters: () => channelManager.listAdapters()
|
|
3475
3613
|
});
|
|
3476
|
-
const runtimeConnections = /* @__PURE__ */ new Map();
|
|
3477
|
-
const staleRuntimeTimers = /* @__PURE__ */ new Map();
|
|
3478
3614
|
const connectionOpenedAt = /* @__PURE__ */ new Map();
|
|
3479
3615
|
const disconnectGracePeriodMs = opts.disconnectGracePeriodMs ?? 0;
|
|
3480
3616
|
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
3617
|
const log = (level, message) => {
|
|
3494
3618
|
if (opts.silent) return;
|
|
3495
3619
|
const stream = level === "error" || level === "warn" ? "stderr" : "stdout";
|
|
3496
3620
|
process[stream].write(`athena-gateway: [${level}] ${message}
|
|
3497
3621
|
`);
|
|
3498
3622
|
};
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3623
|
+
const pipeline = new DispatchPipeline({
|
|
3624
|
+
stateDb,
|
|
3501
3625
|
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
|
|
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
|
+
}
|
|
3522
3635
|
});
|
|
3636
|
+
pipeline.start();
|
|
3523
3637
|
channelManager.setInboundSink((inbound) => {
|
|
3524
|
-
|
|
3638
|
+
pipeline.handleInbound(inbound);
|
|
3525
3639
|
});
|
|
3526
3640
|
const channelConfigHome = opts.env?.HOME;
|
|
3527
3641
|
const reloadChannels = async () => {
|
|
@@ -3635,45 +3749,11 @@ async function startDaemon(opts) {
|
|
|
3635
3749
|
}
|
|
3636
3750
|
const handler = createDispatcher({
|
|
3637
3751
|
startedAt,
|
|
3638
|
-
|
|
3639
|
-
dispatcher,
|
|
3752
|
+
pipeline,
|
|
3640
3753
|
channelManager,
|
|
3641
3754
|
relayCoordinator,
|
|
3642
3755
|
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
|
-
}
|
|
3756
|
+
reloadChannels
|
|
3677
3757
|
});
|
|
3678
3758
|
let server;
|
|
3679
3759
|
let listener;
|
|
@@ -3707,39 +3787,7 @@ async function startDaemon(opts) {
|
|
|
3707
3787
|
reason: "closed",
|
|
3708
3788
|
durationMs
|
|
3709
3789
|
});
|
|
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
|
-
}
|
|
3790
|
+
pipeline.notifyConnectionClosed(ctx.connectionId);
|
|
3743
3791
|
}
|
|
3744
3792
|
});
|
|
3745
3793
|
if (listenSpec.kind === "tcp") {
|
|
@@ -3775,11 +3823,7 @@ async function startDaemon(opts) {
|
|
|
3775
3823
|
if (stopping) return;
|
|
3776
3824
|
stopping = true;
|
|
3777
3825
|
try {
|
|
3778
|
-
|
|
3779
|
-
for (const timer of staleRuntimeTimers.values()) {
|
|
3780
|
-
clearTimeout(timer);
|
|
3781
|
-
}
|
|
3782
|
-
staleRuntimeTimers.clear();
|
|
3826
|
+
await pipeline.stop();
|
|
3783
3827
|
relayCoordinator.disposeAll("auto_resolved");
|
|
3784
3828
|
await channelManager.stop();
|
|
3785
3829
|
await server.close();
|
|
@@ -3804,13 +3848,9 @@ async function startDaemon(opts) {
|
|
|
3804
3848
|
startedAt,
|
|
3805
3849
|
pid,
|
|
3806
3850
|
paths,
|
|
3807
|
-
|
|
3808
|
-
dispatcher,
|
|
3851
|
+
pipeline,
|
|
3809
3852
|
channelManager,
|
|
3810
3853
|
relayCoordinator,
|
|
3811
|
-
inboundQueue,
|
|
3812
|
-
outbox,
|
|
3813
|
-
outboundDispatcher,
|
|
3814
3854
|
listener,
|
|
3815
3855
|
stop
|
|
3816
3856
|
};
|