@triflux/remote 10.44.0 → 10.46.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 (40) hide show
  1. package/hub/pipe.mjs +10 -15
  2. package/hub/schema.sql +1 -0
  3. package/hub/server.mjs +172 -93
  4. package/hub/store.mjs +127 -61
  5. package/hub/team/backend.mjs +4 -2
  6. package/hub/team/claude-daemon-control.mjs +2 -0
  7. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  8. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  9. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  10. package/hub/team/conductor.mjs +80 -24
  11. package/hub/team/cto-auto-collect.mjs +2 -6
  12. package/hub/team/cto-pull.mjs +4 -0
  13. package/hub/team/dashboard-open.mjs +1 -0
  14. package/hub/team/execution-mode.mjs +38 -2
  15. package/hub/team/headless.mjs +493 -82
  16. package/hub/team/interactive-tui-transport.mjs +15 -1
  17. package/hub/team/launcher-template.mjs +17 -1
  18. package/hub/team/orchestrator.mjs +36 -9
  19. package/hub/team/pane.mjs +176 -14
  20. package/hub/team/psmux.mjs +79 -70
  21. package/hub/team/retry-state-machine.mjs +47 -16
  22. package/hub/team/session.mjs +57 -0
  23. package/hub/team/synapse-http.mjs +149 -4
  24. package/hub/team/terminal-opener.mjs +68 -13
  25. package/hub/team/tui-viewer.mjs +65 -14
  26. package/hub/team/uds-orchestrator.mjs +384 -83
  27. package/hub/tools.mjs +10 -1
  28. package/hub/workers/codex-mcp.mjs +9 -4
  29. package/hub/workers/delegator-mcp.mjs +1 -1
  30. package/package.json +1 -1
  31. package/scripts/lib/agent-route-policy.mjs +99 -38
  32. package/scripts/lib/cli-agy.mjs +24 -4
  33. package/scripts/lib/cli-codex.mjs +2 -0
  34. package/scripts/lib/codex-profile-config.mjs +45 -13
  35. package/scripts/lib/codex-recovery.sh +19 -0
  36. package/scripts/lib/gemini-profiles.mjs +113 -29
  37. package/scripts/lib/keyword-rules.mjs +12 -0
  38. package/scripts/lib/logger.mjs +60 -43
  39. package/scripts/lib/machine-profile.mjs +293 -0
  40. package/scripts/lib/mcp-health.mjs +44 -18
package/hub/pipe.mjs CHANGED
@@ -267,18 +267,11 @@ export function createPipeServer({
267
267
  const result = router.refreshAgentLease(
268
268
  agentId,
269
269
  payload.heartbeat_ttl_ms || heartbeatTtlMs,
270
+ payload.presence_generation,
271
+ payload,
270
272
  );
271
273
  if (client) touchClient(client);
272
- if (result?.effective?.updated === false) {
273
- return {
274
- ok: false,
275
- error: {
276
- code: "AGENT_NOT_FOUND",
277
- message: `heartbeat target not registered: ${agentId}`,
278
- },
279
- };
280
- }
281
- return { ok: true, data: result };
274
+ return result;
282
275
  }
283
276
 
284
277
  case "publish": {
@@ -357,12 +350,14 @@ export function createPipeServer({
357
350
 
358
351
  case "deregister": {
359
352
  const agentId = resolveAgentId(client, payload);
360
- router.updateAgentStatus(agentId, "offline");
353
+ const result = router.transitionAgentStatus(
354
+ agentId,
355
+ "offline",
356
+ payload.presence_generation,
357
+ payload,
358
+ );
361
359
  if (client) touchClient(client);
362
- return {
363
- ok: true,
364
- data: { agent_id: agentId, status: "offline" },
365
- };
360
+ return result;
366
361
  }
367
362
 
368
363
  case "send_input": {
package/hub/schema.sql CHANGED
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS agents (
10
10
  topics_json TEXT NOT NULL DEFAULT '[]',
11
11
  last_seen_ms INTEGER NOT NULL,
12
12
  lease_expires_ms INTEGER NOT NULL,
13
+ presence_generation INTEGER NOT NULL DEFAULT 0,
13
14
  status TEXT NOT NULL CHECK (status IN ('online','stale','offline')),
14
15
  metadata_json TEXT NOT NULL DEFAULT '{}',
15
16
  project_id TEXT,
package/hub/server.mjs CHANGED
@@ -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);
@@ -1062,7 +1064,7 @@ export async function startHub({
1062
1064
  cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
1063
1065
  }
1064
1066
 
1065
- const store = await createStoreAdapter(dbPath);
1067
+ const store = await createStoreAdapterFn(dbPath);
1066
1068
  const router = createRouter(store);
1067
1069
  const fingerprintService = createAdaptiveFingerprintService({ store });
1068
1070
 
@@ -1720,6 +1722,8 @@ export async function startHub({
1720
1722
  agent_id,
1721
1723
  reason = "manual",
1722
1724
  requested_by = "http",
1725
+ hub_instance_id,
1726
+ store_fingerprint,
1723
1727
  } = body;
1724
1728
  if (!role || !agent_id) {
1725
1729
  return writeJson(res, 400, {
@@ -1732,6 +1736,8 @@ export async function startHub({
1732
1736
  agent_id,
1733
1737
  reason,
1734
1738
  requested_by,
1739
+ hub_instance_id,
1740
+ store_fingerprint,
1735
1741
  });
1736
1742
  return writeJson(res, result.ok ? 200 : 409, result);
1737
1743
  }
@@ -1748,6 +1754,8 @@ export async function startHub({
1748
1754
  session_id,
1749
1755
  host_id,
1750
1756
  transport_locators_json,
1757
+ hub_instance_id,
1758
+ store_fingerprint,
1751
1759
  } = body;
1752
1760
  if (!agent_id || !cli) {
1753
1761
  return writeJson(res, 400, {
@@ -1782,6 +1790,8 @@ export async function startHub({
1782
1790
  ...(transport_locators_json === undefined
1783
1791
  ? {}
1784
1792
  : { transport_locators_json }),
1793
+ ...(hub_instance_id === undefined ? {} : { hub_instance_id }),
1794
+ ...(store_fingerprint === undefined ? {} : { store_fingerprint }),
1785
1795
  });
1786
1796
  if (result.ok) {
1787
1797
  workerSpans.set(agent_id, {
@@ -1816,7 +1826,13 @@ export async function startHub({
1816
1826
  }
1817
1827
 
1818
1828
  if (path === "/bridge/heartbeat" && req.method === "POST") {
1819
- const { agent_id, ttl_ms } = body;
1829
+ const {
1830
+ agent_id,
1831
+ ttl_ms,
1832
+ presence_generation,
1833
+ hub_instance_id,
1834
+ store_fingerprint,
1835
+ } = body;
1820
1836
  if (!agent_id)
1821
1837
  return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
1822
1838
  const result = await pipe.executeCommand("heartbeat", {
@@ -1825,6 +1841,9 @@ export async function startHub({
1825
1841
  ttl_ms,
1826
1842
  resolveWorkerLeaseTtlMs(),
1827
1843
  ),
1844
+ presence_generation,
1845
+ hub_instance_id,
1846
+ store_fingerprint,
1828
1847
  });
1829
1848
  return writeJson(res, result.ok ? 200 : 404, result);
1830
1849
  }
@@ -1905,6 +1924,9 @@ export async function startHub({
1905
1924
  status,
1906
1925
  include_metrics = true,
1907
1926
  trace_id,
1927
+ presence_generation,
1928
+ hub_instance_id,
1929
+ store_fingerprint,
1908
1930
  } = body;
1909
1931
 
1910
1932
  if (agent_id && status) {
@@ -1925,7 +1947,15 @@ export async function startHub({
1925
1947
  ]).has(normalizedStatus)
1926
1948
  ? normalizedStatus
1927
1949
  : "online";
1928
- 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
+ }
1929
1959
  const snapshot = await pipe.executeQuery("status", {
1930
1960
  scope: "agent",
1931
1961
  agent_id: normalizedAgentId,
@@ -1939,6 +1969,8 @@ export async function startHub({
1939
1969
  reported_status: normalizedStatus,
1940
1970
  reported_at_ms: Date.now(),
1941
1971
  snapshot: snapshot?.data?.agent || null,
1972
+ hub_instance_id: transition.data.hub_instance_id,
1973
+ store_fingerprint: transition.data.store_fingerprint,
1942
1974
  },
1943
1975
  });
1944
1976
  }
@@ -1948,6 +1980,8 @@ export async function startHub({
1948
1980
  agent_id,
1949
1981
  include_metrics,
1950
1982
  trace_id,
1983
+ hub_instance_id,
1984
+ store_fingerprint,
1951
1985
  });
1952
1986
  return writeJson(res, 200, result);
1953
1987
  }
@@ -2167,12 +2201,20 @@ export async function startHub({
2167
2201
  }
2168
2202
 
2169
2203
  if (path === "/bridge/deregister" && req.method === "POST") {
2170
- const { agent_id } = body;
2204
+ const {
2205
+ agent_id,
2206
+ presence_generation,
2207
+ hub_instance_id,
2208
+ store_fingerprint,
2209
+ } = body;
2171
2210
  if (!agent_id) {
2172
2211
  return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
2173
2212
  }
2174
2213
  const result = await pipe.executeCommand("deregister", {
2175
2214
  agent_id,
2215
+ presence_generation,
2216
+ hub_instance_id,
2217
+ store_fingerprint,
2176
2218
  });
2177
2219
  if (result.ok) {
2178
2220
  finishWorkerTrace(agent_id, "deregister");
@@ -2316,102 +2358,107 @@ export async function startHub({
2316
2358
  httpServer.requestTimeout = 30000;
2317
2359
  httpServer.headersTimeout = 10000;
2318
2360
 
2319
- router.startSweeper();
2361
+ let hitlTimer = null;
2362
+ let sessionTimer = null;
2363
+ let synapsePruneTimer = null;
2364
+ let orphanCleanupTimer = null;
2365
+ let rateLimitTimer = null;
2320
2366
 
2321
- const hitlTimer = setInterval(() => {
2322
- try {
2323
- hitl.checkTimeouts();
2324
- } catch (err) {
2325
- hubLog.warn({ err }, "hitl.timeout_check_failed");
2326
- }
2327
- }, 10000);
2328
- hitlTimer.unref();
2329
-
2330
- // MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
2331
- // Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
2332
- const SESSION_TTL_MS =
2333
- parseInt(process.env.TFX_SESSION_TTL_MS || "", 10) || 30 * 60 * 1000;
2334
- const sessionTimer = setInterval(() => {
2335
- const now = Date.now();
2336
- for (const [sid, session] of transports) {
2337
- if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS)
2338
- continue;
2339
- void closeMcpTransportSession(sid, session, "session_ttl");
2340
- }
2341
- }, 60000);
2342
- 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();
2343
2390
 
2344
- const synapsePruneTimer = setInterval(() => {
2345
- try {
2346
- const { count } = synapseRegistry.pruneExpired();
2347
- if (count > 0) hubLog.info({ count }, "synapse.prune");
2348
- } catch (err) {
2349
- hubLog.warn({ err }, "synapse.prune_failed");
2350
- }
2351
- }, 60000);
2352
- synapsePruneTimer.unref();
2353
-
2354
- // 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
2355
- // TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
2356
- const orphanCleanupTimer = setInterval(
2357
- () => {
2358
- if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
2391
+ synapsePruneTimer = setInterval(() => {
2359
2392
  try {
2360
- const { killed, killedProcesses = [] } = cleanupOrphanNodeProcesses();
2361
- const {
2362
- killed: runtimeKilled,
2363
- killedProcesses: runtimeKilledProcesses = [],
2364
- } = cleanupOrphanRuntimeProcesses();
2365
- const totalKilled = killed + runtimeKilled;
2366
- if (totalKilled > 0) {
2367
- hubLog.info(
2368
- {
2369
- killed: totalKilled,
2370
- processes: [...killedProcesses, ...runtimeKilledProcesses],
2371
- caller: "timer",
2372
- },
2373
- "hub.orphan_cleanup",
2374
- );
2375
- }
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
+ }
2376
2423
 
2377
- const { killed: fsmonitorKilled, stale } = cleanupStaleFsmonitorDaemons(
2378
- {
2379
- minAgeMs: 24 * 60 * 60 * 1000,
2380
- },
2381
- );
2382
- if (fsmonitorKilled > 0) {
2383
- hubLog.info(
2384
- { killed: fsmonitorKilled, stale: stale.length },
2385
- "hub.fsmonitor_cleanup",
2386
- );
2387
- }
2388
- } 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 {}
2389
2435
 
2390
- // stale tfx-spawn-* psmux 세션 정리 (30분 이상 idle)
2391
- try {
2392
- const staleKilled = cleanupStaleSpawnSessions(hubLog);
2393
- if (staleKilled > 0) {
2394
- 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);
2395
2457
  }
2396
- } catch {}
2397
- },
2398
- 5 * 60 * 1000,
2399
- );
2400
- orphanCleanupTimer.unref();
2401
-
2402
- // Evict stale rate-limit buckets once per minute to bound memory usage.
2403
- const rateLimitTimer = setInterval(() => {
2404
- const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
2405
- for (const [ip, timestamps] of rateLimitMap) {
2406
- const fresh = timestamps.filter((t) => t >= cutoff);
2407
- if (fresh.length === 0) {
2408
- rateLimitMap.delete(ip);
2409
- } else {
2410
- rateLimitMap.set(ip, fresh);
2411
2458
  }
2412
- }
2413
- }, RATE_LIMIT_WINDOW_MS);
2414
- rateLimitTimer.unref();
2459
+ }, RATE_LIMIT_WINDOW_MS);
2460
+ rateLimitTimer.unref();
2461
+ };
2415
2462
 
2416
2463
  mkdirSync(PID_DIR, { recursive: true });
2417
2464
 
@@ -2419,6 +2466,19 @@ export async function startHub({
2419
2466
  try {
2420
2467
  router.stopSweeper();
2421
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();
2422
2482
  try {
2423
2483
  await pipe.stop();
2424
2484
  } catch {}
@@ -2428,6 +2488,9 @@ export async function startHub({
2428
2488
  try {
2429
2489
  await delegatorWorker.stop();
2430
2490
  } catch {}
2491
+ try {
2492
+ synapseRegistry.destroy();
2493
+ } catch {}
2431
2494
  try {
2432
2495
  store.close();
2433
2496
  } catch {}
@@ -2438,6 +2501,22 @@ export async function startHub({
2438
2501
  };
2439
2502
 
2440
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();
2441
2520
  await pipe.start();
2442
2521
  await assignCallbacks.start();
2443
2522
  } catch (error) {