@triflux/remote 10.43.0 → 10.45.0
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/cto/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +10 -9
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +276 -132
- package/hub/store.mjs +699 -41
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +101 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +50 -9
- package/hub/team/headless.mjs +539 -92
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/session.mjs +57 -0
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +22 -1
- package/hub/workers/codex-mcp.mjs +26 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
package/hub/server.mjs
CHANGED
|
@@ -32,13 +32,6 @@ import {
|
|
|
32
32
|
import { broker as brokerInstance, reloadBroker } from "@triflux/core/hub/account-broker.mjs";
|
|
33
33
|
import { createAdaptiveEngine } from "@triflux/core/hub/adaptive.mjs";
|
|
34
34
|
import { createAssignCallbackServer } from "@triflux/core/hub/assign-callbacks.mjs";
|
|
35
|
-
import {
|
|
36
|
-
clampDurationMs,
|
|
37
|
-
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
38
|
-
DEFAULT_ASSIGN_TTL_MS,
|
|
39
|
-
DEFAULT_REGISTER_TIMEOUT_SEC,
|
|
40
|
-
resolveWorkerLeaseTtlMs,
|
|
41
|
-
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
42
35
|
import { DelegatorService } from "@triflux/core/hub/delegator/index.mjs";
|
|
43
36
|
import { createHitlManager } from "@triflux/core/hub/hitl.mjs";
|
|
44
37
|
import {
|
|
@@ -51,6 +44,13 @@ import {
|
|
|
51
44
|
cleanupStaleFsmonitorDaemons,
|
|
52
45
|
} from "@triflux/core/hub/lib/process-utils.mjs";
|
|
53
46
|
import * as spawnTrace from "@triflux/core/hub/lib/spawn-trace.mjs";
|
|
47
|
+
import {
|
|
48
|
+
clampDurationMs,
|
|
49
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
50
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
51
|
+
DEFAULT_REGISTER_TIMEOUT_SEC,
|
|
52
|
+
resolveWorkerLeaseTtlMs,
|
|
53
|
+
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
54
54
|
import {
|
|
55
55
|
recordRequest,
|
|
56
56
|
recordWorker,
|
|
@@ -961,6 +961,7 @@ function servePublicFile(res, path) {
|
|
|
961
961
|
* @param {string} [opts.host]
|
|
962
962
|
* @param {string|number} [opts.sessionId]
|
|
963
963
|
* @param {(options: { cwd: string }) => object} [opts.createDelegatorWorker]
|
|
964
|
+
* @param {(dbPath: string) => object|Promise<object>} [opts.createStoreAdapterFn] test seam
|
|
964
965
|
*/
|
|
965
966
|
export async function startHub({
|
|
966
967
|
port: portOpt,
|
|
@@ -968,6 +969,7 @@ export async function startHub({
|
|
|
968
969
|
host = "127.0.0.1",
|
|
969
970
|
sessionId = process.pid,
|
|
970
971
|
createDelegatorWorker = createDelegatorMcpWorker,
|
|
972
|
+
createStoreAdapterFn = createStoreAdapter,
|
|
971
973
|
} = {}) {
|
|
972
974
|
const resolvedPort = parseHubPort(portOpt);
|
|
973
975
|
const portSpecified = Number.isFinite(resolvedPort);
|
|
@@ -980,7 +982,13 @@ export async function startHub({
|
|
|
980
982
|
detectPeer: () => livePeer,
|
|
981
983
|
});
|
|
982
984
|
|
|
983
|
-
if (
|
|
985
|
+
if (
|
|
986
|
+
portSpecified &&
|
|
987
|
+
!ephemeralPort &&
|
|
988
|
+
livePeer.alive &&
|
|
989
|
+
livePidPort &&
|
|
990
|
+
port !== livePidPort
|
|
991
|
+
) {
|
|
984
992
|
hubLog.warn(
|
|
985
993
|
{
|
|
986
994
|
requestedPort: port,
|
|
@@ -991,9 +999,14 @@ export async function startHub({
|
|
|
991
999
|
);
|
|
992
1000
|
}
|
|
993
1001
|
|
|
994
|
-
const existingHub = ephemeralPort
|
|
995
|
-
|
|
996
|
-
|
|
1002
|
+
const existingHub = ephemeralPort
|
|
1003
|
+
? null
|
|
1004
|
+
: await tryReuseExistingHub({
|
|
1005
|
+
port,
|
|
1006
|
+
portSpecified,
|
|
1007
|
+
host,
|
|
1008
|
+
detectPeer: () => livePeer,
|
|
1009
|
+
});
|
|
997
1010
|
if (existingHub) return existingHub;
|
|
998
1011
|
|
|
999
1012
|
attachBrokerDiagnostics(brokerInstance);
|
|
@@ -1031,12 +1044,14 @@ export async function startHub({
|
|
|
1031
1044
|
lockHeld = false;
|
|
1032
1045
|
};
|
|
1033
1046
|
|
|
1034
|
-
const lockedExistingHub = ephemeralPort
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1047
|
+
const lockedExistingHub = ephemeralPort
|
|
1048
|
+
? null
|
|
1049
|
+
: await tryReuseExistingHub({
|
|
1050
|
+
port,
|
|
1051
|
+
portSpecified,
|
|
1052
|
+
host,
|
|
1053
|
+
detectPeer: () => detectLivePeer(),
|
|
1054
|
+
});
|
|
1040
1055
|
if (lockedExistingHub) {
|
|
1041
1056
|
releaseStartupLock();
|
|
1042
1057
|
return lockedExistingHub;
|
|
@@ -1049,7 +1064,7 @@ export async function startHub({
|
|
|
1049
1064
|
cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
1050
1065
|
}
|
|
1051
1066
|
|
|
1052
|
-
const store = await
|
|
1067
|
+
const store = await createStoreAdapterFn(dbPath);
|
|
1053
1068
|
const router = createRouter(store);
|
|
1054
1069
|
const fingerprintService = createAdaptiveFingerprintService({ store });
|
|
1055
1070
|
|
|
@@ -1545,6 +1560,38 @@ export async function startHub({
|
|
|
1545
1560
|
try {
|
|
1546
1561
|
const body = await parseBody(req);
|
|
1547
1562
|
const { sessionId } = body || {};
|
|
1563
|
+
if (body?.agent_id && body?.cli) {
|
|
1564
|
+
const registered = router.registerAgent({
|
|
1565
|
+
agent_id: body.agent_id,
|
|
1566
|
+
cli: body.cli,
|
|
1567
|
+
capabilities: Array.isArray(body.capabilities)
|
|
1568
|
+
? body.capabilities
|
|
1569
|
+
: [],
|
|
1570
|
+
topics: Array.isArray(body.topics) ? body.topics : [],
|
|
1571
|
+
heartbeat_ttl_ms: body.heartbeat_ttl_ms ?? 300000,
|
|
1572
|
+
metadata:
|
|
1573
|
+
body.metadata && typeof body.metadata === "object"
|
|
1574
|
+
? body.metadata
|
|
1575
|
+
: { source: "session_start" },
|
|
1576
|
+
...(Object.hasOwn(body, "project_id")
|
|
1577
|
+
? { project_id: body.project_id }
|
|
1578
|
+
: {}),
|
|
1579
|
+
...(Object.hasOwn(body, "session_id")
|
|
1580
|
+
? { session_id: body.session_id }
|
|
1581
|
+
: {}),
|
|
1582
|
+
...(Object.hasOwn(body, "host_id")
|
|
1583
|
+
? { host_id: body.host_id }
|
|
1584
|
+
: {}),
|
|
1585
|
+
...(Object.hasOwn(body, "transport_locators_json")
|
|
1586
|
+
? { transport_locators_json: body.transport_locators_json }
|
|
1587
|
+
: {}),
|
|
1588
|
+
});
|
|
1589
|
+
if (!registered?.ok) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
registered?.error?.message || "hub agent register failed",
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1548
1595
|
const result = synapseRegistry.register(sessionId, body);
|
|
1549
1596
|
if (!result?.ok) {
|
|
1550
1597
|
throw new Error(result?.reason || "register failed");
|
|
@@ -1675,6 +1722,8 @@ export async function startHub({
|
|
|
1675
1722
|
agent_id,
|
|
1676
1723
|
reason = "manual",
|
|
1677
1724
|
requested_by = "http",
|
|
1725
|
+
hub_instance_id,
|
|
1726
|
+
store_fingerprint,
|
|
1678
1727
|
} = body;
|
|
1679
1728
|
if (!role || !agent_id) {
|
|
1680
1729
|
return writeJson(res, 400, {
|
|
@@ -1687,6 +1736,8 @@ export async function startHub({
|
|
|
1687
1736
|
agent_id,
|
|
1688
1737
|
reason,
|
|
1689
1738
|
requested_by,
|
|
1739
|
+
hub_instance_id,
|
|
1740
|
+
store_fingerprint,
|
|
1690
1741
|
});
|
|
1691
1742
|
return writeJson(res, result.ok ? 200 : 409, result);
|
|
1692
1743
|
}
|
|
@@ -1699,6 +1750,12 @@ export async function startHub({
|
|
|
1699
1750
|
topics = [],
|
|
1700
1751
|
capabilities = [],
|
|
1701
1752
|
metadata = {},
|
|
1753
|
+
project_id,
|
|
1754
|
+
session_id,
|
|
1755
|
+
host_id,
|
|
1756
|
+
transport_locators_json,
|
|
1757
|
+
hub_instance_id,
|
|
1758
|
+
store_fingerprint,
|
|
1702
1759
|
} = body;
|
|
1703
1760
|
if (!agent_id || !cli) {
|
|
1704
1761
|
return writeJson(res, 400, {
|
|
@@ -1714,7 +1771,9 @@ export async function startHub({
|
|
|
1714
1771
|
metadata?.task ||
|
|
1715
1772
|
cli;
|
|
1716
1773
|
const workerStartedAt = performance.now();
|
|
1717
|
-
const hasExplicitTimeout = Number.isFinite(
|
|
1774
|
+
const hasExplicitTimeout = Number.isFinite(
|
|
1775
|
+
Number(body.timeout_sec),
|
|
1776
|
+
);
|
|
1718
1777
|
const heartbeat_ttl_ms = hasExplicitTimeout
|
|
1719
1778
|
? (timeout_sec + 120) * 1000
|
|
1720
1779
|
: Math.max((timeout_sec + 120) * 1000, resolveWorkerLeaseTtlMs());
|
|
@@ -1725,6 +1784,14 @@ export async function startHub({
|
|
|
1725
1784
|
topics,
|
|
1726
1785
|
heartbeat_ttl_ms,
|
|
1727
1786
|
metadata,
|
|
1787
|
+
...(project_id === undefined ? {} : { project_id }),
|
|
1788
|
+
...(session_id === undefined ? {} : { session_id }),
|
|
1789
|
+
...(host_id === undefined ? {} : { host_id }),
|
|
1790
|
+
...(transport_locators_json === undefined
|
|
1791
|
+
? {}
|
|
1792
|
+
: { transport_locators_json }),
|
|
1793
|
+
...(hub_instance_id === undefined ? {} : { hub_instance_id }),
|
|
1794
|
+
...(store_fingerprint === undefined ? {} : { store_fingerprint }),
|
|
1728
1795
|
});
|
|
1729
1796
|
if (result.ok) {
|
|
1730
1797
|
workerSpans.set(agent_id, {
|
|
@@ -1759,11 +1826,24 @@ export async function startHub({
|
|
|
1759
1826
|
}
|
|
1760
1827
|
|
|
1761
1828
|
if (path === "/bridge/heartbeat" && req.method === "POST") {
|
|
1762
|
-
const {
|
|
1763
|
-
|
|
1829
|
+
const {
|
|
1830
|
+
agent_id,
|
|
1831
|
+
ttl_ms,
|
|
1832
|
+
presence_generation,
|
|
1833
|
+
hub_instance_id,
|
|
1834
|
+
store_fingerprint,
|
|
1835
|
+
} = body;
|
|
1836
|
+
if (!agent_id)
|
|
1837
|
+
return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
|
|
1764
1838
|
const result = await pipe.executeCommand("heartbeat", {
|
|
1765
1839
|
agent_id,
|
|
1766
|
-
heartbeat_ttl_ms: clampDurationMs(
|
|
1840
|
+
heartbeat_ttl_ms: clampDurationMs(
|
|
1841
|
+
ttl_ms,
|
|
1842
|
+
resolveWorkerLeaseTtlMs(),
|
|
1843
|
+
),
|
|
1844
|
+
presence_generation,
|
|
1845
|
+
hub_instance_id,
|
|
1846
|
+
store_fingerprint,
|
|
1767
1847
|
});
|
|
1768
1848
|
return writeJson(res, result.ok ? 200 : 404, result);
|
|
1769
1849
|
}
|
|
@@ -1844,6 +1924,9 @@ export async function startHub({
|
|
|
1844
1924
|
status,
|
|
1845
1925
|
include_metrics = true,
|
|
1846
1926
|
trace_id,
|
|
1927
|
+
presence_generation,
|
|
1928
|
+
hub_instance_id,
|
|
1929
|
+
store_fingerprint,
|
|
1847
1930
|
} = body;
|
|
1848
1931
|
|
|
1849
1932
|
if (agent_id && status) {
|
|
@@ -1864,7 +1947,15 @@ export async function startHub({
|
|
|
1864
1947
|
]).has(normalizedStatus)
|
|
1865
1948
|
? normalizedStatus
|
|
1866
1949
|
: "online";
|
|
1867
|
-
router.
|
|
1950
|
+
const transition = router.transitionAgentStatus(
|
|
1951
|
+
normalizedAgentId,
|
|
1952
|
+
statusForStore,
|
|
1953
|
+
presence_generation,
|
|
1954
|
+
{ hub_instance_id, store_fingerprint },
|
|
1955
|
+
);
|
|
1956
|
+
if (!transition.ok) {
|
|
1957
|
+
return writeJson(res, 409, transition);
|
|
1958
|
+
}
|
|
1868
1959
|
const snapshot = await pipe.executeQuery("status", {
|
|
1869
1960
|
scope: "agent",
|
|
1870
1961
|
agent_id: normalizedAgentId,
|
|
@@ -1878,6 +1969,8 @@ export async function startHub({
|
|
|
1878
1969
|
reported_status: normalizedStatus,
|
|
1879
1970
|
reported_at_ms: Date.now(),
|
|
1880
1971
|
snapshot: snapshot?.data?.agent || null,
|
|
1972
|
+
hub_instance_id: transition.data.hub_instance_id,
|
|
1973
|
+
store_fingerprint: transition.data.store_fingerprint,
|
|
1881
1974
|
},
|
|
1882
1975
|
});
|
|
1883
1976
|
}
|
|
@@ -1887,6 +1980,8 @@ export async function startHub({
|
|
|
1887
1980
|
agent_id,
|
|
1888
1981
|
include_metrics,
|
|
1889
1982
|
trace_id,
|
|
1983
|
+
hub_instance_id,
|
|
1984
|
+
store_fingerprint,
|
|
1890
1985
|
});
|
|
1891
1986
|
return writeJson(res, 200, result);
|
|
1892
1987
|
}
|
|
@@ -2106,12 +2201,20 @@ export async function startHub({
|
|
|
2106
2201
|
}
|
|
2107
2202
|
|
|
2108
2203
|
if (path === "/bridge/deregister" && req.method === "POST") {
|
|
2109
|
-
const {
|
|
2204
|
+
const {
|
|
2205
|
+
agent_id,
|
|
2206
|
+
presence_generation,
|
|
2207
|
+
hub_instance_id,
|
|
2208
|
+
store_fingerprint,
|
|
2209
|
+
} = body;
|
|
2110
2210
|
if (!agent_id) {
|
|
2111
2211
|
return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
|
|
2112
2212
|
}
|
|
2113
2213
|
const result = await pipe.executeCommand("deregister", {
|
|
2114
2214
|
agent_id,
|
|
2215
|
+
presence_generation,
|
|
2216
|
+
hub_instance_id,
|
|
2217
|
+
store_fingerprint,
|
|
2115
2218
|
});
|
|
2116
2219
|
if (result.ok) {
|
|
2117
2220
|
finishWorkerTrace(agent_id, "deregister");
|
|
@@ -2255,102 +2358,107 @@ export async function startHub({
|
|
|
2255
2358
|
httpServer.requestTimeout = 30000;
|
|
2256
2359
|
httpServer.headersTimeout = 10000;
|
|
2257
2360
|
|
|
2258
|
-
|
|
2361
|
+
let hitlTimer = null;
|
|
2362
|
+
let sessionTimer = null;
|
|
2363
|
+
let synapsePruneTimer = null;
|
|
2364
|
+
let orphanCleanupTimer = null;
|
|
2365
|
+
let rateLimitTimer = null;
|
|
2259
2366
|
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2367
|
+
const startBackgroundTimers = () => {
|
|
2368
|
+
hitlTimer = setInterval(() => {
|
|
2369
|
+
try {
|
|
2370
|
+
hitl.checkTimeouts();
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
hubLog.warn({ err }, "hitl.timeout_check_failed");
|
|
2373
|
+
}
|
|
2374
|
+
}, 10000);
|
|
2375
|
+
hitlTimer.unref();
|
|
2376
|
+
|
|
2377
|
+
// MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
|
|
2378
|
+
// Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
|
|
2379
|
+
const SESSION_TTL_MS =
|
|
2380
|
+
parseInt(process.env.TFX_SESSION_TTL_MS || "", 10) || 30 * 60 * 1000;
|
|
2381
|
+
sessionTimer = setInterval(() => {
|
|
2382
|
+
const now = Date.now();
|
|
2383
|
+
for (const [sid, session] of transports) {
|
|
2384
|
+
if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS)
|
|
2385
|
+
continue;
|
|
2386
|
+
void closeMcpTransportSession(sid, session, "session_ttl");
|
|
2387
|
+
}
|
|
2388
|
+
}, 60000);
|
|
2389
|
+
sessionTimer.unref();
|
|
2282
2390
|
|
|
2283
|
-
|
|
2284
|
-
try {
|
|
2285
|
-
const { count } = synapseRegistry.pruneExpired();
|
|
2286
|
-
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2287
|
-
} catch (err) {
|
|
2288
|
-
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2289
|
-
}
|
|
2290
|
-
}, 60000);
|
|
2291
|
-
synapsePruneTimer.unref();
|
|
2292
|
-
|
|
2293
|
-
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2294
|
-
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2295
|
-
const orphanCleanupTimer = setInterval(
|
|
2296
|
-
() => {
|
|
2297
|
-
if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
|
|
2391
|
+
synapsePruneTimer = setInterval(() => {
|
|
2298
2392
|
try {
|
|
2299
|
-
const {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
);
|
|
2314
|
-
|
|
2393
|
+
const { count } = synapseRegistry.pruneExpired();
|
|
2394
|
+
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2397
|
+
}
|
|
2398
|
+
}, 60000);
|
|
2399
|
+
synapsePruneTimer.unref();
|
|
2400
|
+
|
|
2401
|
+
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2402
|
+
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2403
|
+
orphanCleanupTimer = setInterval(
|
|
2404
|
+
() => {
|
|
2405
|
+
if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
|
|
2406
|
+
try {
|
|
2407
|
+
const { killed, killedProcesses = [] } = cleanupOrphanNodeProcesses();
|
|
2408
|
+
const {
|
|
2409
|
+
killed: runtimeKilled,
|
|
2410
|
+
killedProcesses: runtimeKilledProcesses = [],
|
|
2411
|
+
} = cleanupOrphanRuntimeProcesses();
|
|
2412
|
+
const totalKilled = killed + runtimeKilled;
|
|
2413
|
+
if (totalKilled > 0) {
|
|
2414
|
+
hubLog.info(
|
|
2415
|
+
{
|
|
2416
|
+
killed: totalKilled,
|
|
2417
|
+
processes: [...killedProcesses, ...runtimeKilledProcesses],
|
|
2418
|
+
caller: "timer",
|
|
2419
|
+
},
|
|
2420
|
+
"hub.orphan_cleanup",
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2315
2423
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
}
|
|
2327
|
-
} catch {}
|
|
2424
|
+
const { killed: fsmonitorKilled, stale } =
|
|
2425
|
+
cleanupStaleFsmonitorDaemons({
|
|
2426
|
+
minAgeMs: 24 * 60 * 60 * 1000,
|
|
2427
|
+
});
|
|
2428
|
+
if (fsmonitorKilled > 0) {
|
|
2429
|
+
hubLog.info(
|
|
2430
|
+
{ killed: fsmonitorKilled, stale: stale.length },
|
|
2431
|
+
"hub.fsmonitor_cleanup",
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
} catch {}
|
|
2328
2435
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2436
|
+
// stale tfx-spawn-* psmux 세션 정리 (30분 이상 idle)
|
|
2437
|
+
try {
|
|
2438
|
+
const staleKilled = cleanupStaleSpawnSessions(hubLog);
|
|
2439
|
+
if (staleKilled > 0) {
|
|
2440
|
+
hubLog.info({ killed: staleKilled }, "hub.stale_spawn_cleanup");
|
|
2441
|
+
}
|
|
2442
|
+
} catch {}
|
|
2443
|
+
},
|
|
2444
|
+
5 * 60 * 1000,
|
|
2445
|
+
);
|
|
2446
|
+
orphanCleanupTimer.unref();
|
|
2447
|
+
|
|
2448
|
+
// Evict stale rate-limit buckets once per minute to bound memory usage.
|
|
2449
|
+
rateLimitTimer = setInterval(() => {
|
|
2450
|
+
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
2451
|
+
for (const [ip, timestamps] of rateLimitMap) {
|
|
2452
|
+
const fresh = timestamps.filter((t) => t >= cutoff);
|
|
2453
|
+
if (fresh.length === 0) {
|
|
2454
|
+
rateLimitMap.delete(ip);
|
|
2455
|
+
} else {
|
|
2456
|
+
rateLimitMap.set(ip, fresh);
|
|
2334
2457
|
}
|
|
2335
|
-
} catch {}
|
|
2336
|
-
},
|
|
2337
|
-
5 * 60 * 1000,
|
|
2338
|
-
);
|
|
2339
|
-
orphanCleanupTimer.unref();
|
|
2340
|
-
|
|
2341
|
-
// Evict stale rate-limit buckets once per minute to bound memory usage.
|
|
2342
|
-
const rateLimitTimer = setInterval(() => {
|
|
2343
|
-
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
2344
|
-
for (const [ip, timestamps] of rateLimitMap) {
|
|
2345
|
-
const fresh = timestamps.filter((t) => t >= cutoff);
|
|
2346
|
-
if (fresh.length === 0) {
|
|
2347
|
-
rateLimitMap.delete(ip);
|
|
2348
|
-
} else {
|
|
2349
|
-
rateLimitMap.set(ip, fresh);
|
|
2350
2458
|
}
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2459
|
+
}, RATE_LIMIT_WINDOW_MS);
|
|
2460
|
+
rateLimitTimer.unref();
|
|
2461
|
+
};
|
|
2354
2462
|
|
|
2355
2463
|
mkdirSync(PID_DIR, { recursive: true });
|
|
2356
2464
|
|
|
@@ -2358,6 +2466,19 @@ export async function startHub({
|
|
|
2358
2466
|
try {
|
|
2359
2467
|
router.stopSweeper();
|
|
2360
2468
|
} catch {}
|
|
2469
|
+
for (const timer of [
|
|
2470
|
+
hitlTimer,
|
|
2471
|
+
sessionTimer,
|
|
2472
|
+
synapsePruneTimer,
|
|
2473
|
+
orphanCleanupTimer,
|
|
2474
|
+
rateLimitTimer,
|
|
2475
|
+
]) {
|
|
2476
|
+
if (timer) clearInterval(timer);
|
|
2477
|
+
}
|
|
2478
|
+
for (const [sid, session] of Array.from(transports)) {
|
|
2479
|
+
await closeMcpTransportSession(sid, session, "hub.startup_failure");
|
|
2480
|
+
}
|
|
2481
|
+
transports.clear();
|
|
2361
2482
|
try {
|
|
2362
2483
|
await pipe.stop();
|
|
2363
2484
|
} catch {}
|
|
@@ -2367,6 +2488,9 @@ export async function startHub({
|
|
|
2367
2488
|
try {
|
|
2368
2489
|
await delegatorWorker.stop();
|
|
2369
2490
|
} catch {}
|
|
2491
|
+
try {
|
|
2492
|
+
synapseRegistry.destroy();
|
|
2493
|
+
} catch {}
|
|
2370
2494
|
try {
|
|
2371
2495
|
store.close();
|
|
2372
2496
|
} catch {}
|
|
@@ -2377,6 +2501,22 @@ export async function startHub({
|
|
|
2377
2501
|
};
|
|
2378
2502
|
|
|
2379
2503
|
try {
|
|
2504
|
+
router.sweepExpired({ now_ms: Date.now(), source: "startup" });
|
|
2505
|
+
router.startSweeper({
|
|
2506
|
+
run_immediately: false,
|
|
2507
|
+
interval_ms: 10000,
|
|
2508
|
+
onSweepError: ({ source, error, consecutive_failures }) => {
|
|
2509
|
+
hubLog.warn(
|
|
2510
|
+
{
|
|
2511
|
+
source,
|
|
2512
|
+
code: error?.code || "UNKNOWN",
|
|
2513
|
+
consecutive_failures,
|
|
2514
|
+
},
|
|
2515
|
+
"hub.sweep_failed",
|
|
2516
|
+
);
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
startBackgroundTimers();
|
|
2380
2520
|
await pipe.start();
|
|
2381
2521
|
await assignCallbacks.start();
|
|
2382
2522
|
} catch (error) {
|
|
@@ -2445,7 +2585,8 @@ export async function startHub({
|
|
|
2445
2585
|
httpServer.off("error", onError);
|
|
2446
2586
|
try {
|
|
2447
2587
|
const boundAddress = httpServer.address();
|
|
2448
|
-
if (boundAddress && typeof boundAddress === "object")
|
|
2588
|
+
if (boundAddress && typeof boundAddress === "object")
|
|
2589
|
+
port = boundAddress.port;
|
|
2449
2590
|
let idleTimer = null;
|
|
2450
2591
|
let stopPromise = null;
|
|
2451
2592
|
|
|
@@ -2466,25 +2607,28 @@ export async function startHub({
|
|
|
2466
2607
|
idleTimeoutMs: hubIdleTimeoutMs,
|
|
2467
2608
|
};
|
|
2468
2609
|
|
|
2469
|
-
if (!ephemeralPort)
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2610
|
+
if (!ephemeralPort) {
|
|
2611
|
+
writeState({
|
|
2612
|
+
pid: process.pid,
|
|
2613
|
+
port,
|
|
2614
|
+
host,
|
|
2615
|
+
auth_mode: HUB_TOKEN ? "token-required" : "localhost-only",
|
|
2616
|
+
url: info.url,
|
|
2617
|
+
pipe_path: pipe.path,
|
|
2618
|
+
pipePath: pipe.path,
|
|
2619
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
2620
|
+
assignCallbackPipePath: assignCallbacks.path,
|
|
2621
|
+
authMode: HUB_TOKEN ? "token-required" : "localhost-only",
|
|
2622
|
+
startedAt,
|
|
2623
|
+
started: startedAtMs,
|
|
2624
|
+
version,
|
|
2625
|
+
sessionId,
|
|
2626
|
+
session_id: sessionId,
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2486
2629
|
releaseStartupLock();
|
|
2487
|
-
if (!ephemeralPort)
|
|
2630
|
+
if (!ephemeralPort)
|
|
2631
|
+
void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
|
|
2488
2632
|
|
|
2489
2633
|
hubLog.info(
|
|
2490
2634
|
{
|