@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.
Files changed (48) hide show
  1. package/cto/hygiene.mjs +1 -1
  2. package/hub/lib/cto-env.mjs +1 -0
  3. package/hub/pipe.mjs +10 -9
  4. package/hub/schema.sql +62 -2
  5. package/hub/server.mjs +276 -132
  6. package/hub/store.mjs +699 -41
  7. package/hub/team/backend.mjs +4 -2
  8. package/hub/team/claude-daemon-control.mjs +2 -0
  9. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  10. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  11. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  12. package/hub/team/conductor.mjs +101 -24
  13. package/hub/team/cto-auto-collect.mjs +2 -6
  14. package/hub/team/cto-pull.mjs +4 -0
  15. package/hub/team/dashboard-open.mjs +1 -0
  16. package/hub/team/execution-mode.mjs +50 -9
  17. package/hub/team/headless.mjs +539 -92
  18. package/hub/team/interactive-tui-transport.mjs +15 -1
  19. package/hub/team/intervention.mjs +1 -1
  20. package/hub/team/launcher-template.mjs +17 -1
  21. package/hub/team/notify.mjs +31 -11
  22. package/hub/team/orchestrator.mjs +36 -9
  23. package/hub/team/pane.mjs +176 -14
  24. package/hub/team/psmux.mjs +79 -70
  25. package/hub/team/retry-state-machine.mjs +41 -13
  26. package/hub/team/session.mjs +57 -0
  27. package/hub/team/swarm-hypervisor.mjs +4 -2
  28. package/hub/team/swarm-preflight.mjs +47 -0
  29. package/hub/team/synapse-http.mjs +149 -4
  30. package/hub/team/terminal-opener.mjs +68 -13
  31. package/hub/team/tui-viewer.mjs +65 -14
  32. package/hub/team/worktree-lifecycle.mjs +56 -59
  33. package/hub/tools.mjs +22 -1
  34. package/hub/workers/codex-mcp.mjs +26 -5
  35. package/hub/workers/delegator-mcp.mjs +3 -49
  36. package/package.json +1 -1
  37. package/scripts/lib/agent-route-policy.mjs +367 -0
  38. package/scripts/lib/cli-agy.mjs +24 -4
  39. package/scripts/lib/cli-codex.mjs +22 -160
  40. package/scripts/lib/codex-profile-config.mjs +48 -4
  41. package/scripts/lib/codex-recovery.sh +19 -0
  42. package/scripts/lib/gemini-profiles.mjs +113 -29
  43. package/scripts/lib/keyword-rules.mjs +24 -0
  44. package/scripts/lib/logger.mjs +60 -43
  45. package/scripts/lib/machine-profile.mjs +293 -0
  46. package/scripts/lib/mcp-health.mjs +44 -18
  47. package/cto/steward.mjs +0 -362
  48. 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 (portSpecified && !ephemeralPort && livePeer.alive && livePidPort && port !== livePidPort) {
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 ? null : await tryReuseExistingHub({
995
- port, portSpecified, host, detectPeer: () => livePeer,
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 ? null : await tryReuseExistingHub({
1035
- port,
1036
- portSpecified,
1037
- host,
1038
- detectPeer: () => detectLivePeer(),
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 createStoreAdapter(dbPath);
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(Number(body.timeout_sec));
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 { agent_id, ttl_ms } = body;
1763
- if (!agent_id) return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
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(ttl_ms, resolveWorkerLeaseTtlMs()),
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.updateAgentStatus(normalizedAgentId, statusForStore);
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 { agent_id } = body;
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
- router.startSweeper();
2361
+ let hitlTimer = null;
2362
+ let sessionTimer = null;
2363
+ let synapsePruneTimer = null;
2364
+ let orphanCleanupTimer = null;
2365
+ let rateLimitTimer = null;
2259
2366
 
2260
- const hitlTimer = setInterval(() => {
2261
- try {
2262
- hitl.checkTimeouts();
2263
- } catch (err) {
2264
- hubLog.warn({ err }, "hitl.timeout_check_failed");
2265
- }
2266
- }, 10000);
2267
- hitlTimer.unref();
2268
-
2269
- // MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
2270
- // Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
2271
- const SESSION_TTL_MS =
2272
- parseInt(process.env.TFX_SESSION_TTL_MS || "", 10) || 30 * 60 * 1000;
2273
- const sessionTimer = setInterval(() => {
2274
- const now = Date.now();
2275
- for (const [sid, session] of transports) {
2276
- if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS)
2277
- continue;
2278
- void closeMcpTransportSession(sid, session, "session_ttl");
2279
- }
2280
- }, 60000);
2281
- sessionTimer.unref();
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
- const synapsePruneTimer = setInterval(() => {
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 { killed, killedProcesses = [] } = cleanupOrphanNodeProcesses();
2300
- const {
2301
- killed: runtimeKilled,
2302
- killedProcesses: runtimeKilledProcesses = [],
2303
- } = cleanupOrphanRuntimeProcesses();
2304
- const totalKilled = killed + runtimeKilled;
2305
- if (totalKilled > 0) {
2306
- hubLog.info(
2307
- {
2308
- killed: totalKilled,
2309
- processes: [...killedProcesses, ...runtimeKilledProcesses],
2310
- caller: "timer",
2311
- },
2312
- "hub.orphan_cleanup",
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
- const { killed: fsmonitorKilled, stale } = cleanupStaleFsmonitorDaemons(
2317
- {
2318
- minAgeMs: 24 * 60 * 60 * 1000,
2319
- },
2320
- );
2321
- if (fsmonitorKilled > 0) {
2322
- hubLog.info(
2323
- { killed: fsmonitorKilled, stale: stale.length },
2324
- "hub.fsmonitor_cleanup",
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
- // stale tfx-spawn-* psmux 세션 정리 (30분 이상 idle)
2330
- try {
2331
- const staleKilled = cleanupStaleSpawnSessions(hubLog);
2332
- if (staleKilled > 0) {
2333
- hubLog.info({ killed: staleKilled }, "hub.stale_spawn_cleanup");
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
- }, RATE_LIMIT_WINDOW_MS);
2353
- rateLimitTimer.unref();
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") port = boundAddress.port;
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) writeState({
2470
- pid: process.pid,
2471
- port,
2472
- host,
2473
- auth_mode: HUB_TOKEN ? "token-required" : "localhost-only",
2474
- url: info.url,
2475
- pipe_path: pipe.path,
2476
- pipePath: pipe.path,
2477
- assign_callback_pipe_path: assignCallbacks.path,
2478
- assignCallbackPipePath: assignCallbacks.path,
2479
- authMode: HUB_TOKEN ? "token-required" : "localhost-only",
2480
- startedAt,
2481
- started: startedAtMs,
2482
- version,
2483
- sessionId,
2484
- session_id: sessionId,
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) void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2630
+ if (!ephemeralPort)
2631
+ void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2488
2632
 
2489
2633
  hubLog.info(
2490
2634
  {