triflux 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 (209) hide show
  1. package/README.md +3 -1
  2. package/adapters/codex/skills/tfx-harness/SKILL.md +66 -0
  3. package/adapters/codex/skills/tfx-harness/skill.json +5 -0
  4. package/bin/tfx-live.mjs +752 -77
  5. package/bin/triflux.mjs +20 -3
  6. package/hooks/codex-session-hook.mjs +67 -1
  7. package/hooks/cto-north-star-brief.mjs +3 -16
  8. package/hooks/error-context.mjs +3 -6
  9. package/hooks/hook-manager.mjs +1 -1
  10. package/hooks/hook-registry.json +0 -11
  11. package/hooks/keyword-rules.json +147 -23
  12. package/hooks/permission-safe-allow.mjs +36 -2
  13. package/hooks/session-start-fast.mjs +50 -1
  14. package/hooks/session-start-lake.mjs +3 -16
  15. package/hub/bridge.mjs +111 -5
  16. package/hub/cli-adapter-base.mjs +11 -2
  17. package/hub/codex-adapter.mjs +3 -0
  18. package/hub/gemini-adapter.mjs +29 -11
  19. package/hub/intent.mjs +24 -32
  20. package/hub/lib/memory-store.mjs +66 -30
  21. package/hub/lib/tfx-route-args.mjs +41 -4
  22. package/hub/pipe.mjs +10 -15
  23. package/hub/reflexion.mjs +0 -57
  24. package/hub/router.mjs +474 -24
  25. package/hub/schema.sql +62 -2
  26. package/hub/server.mjs +214 -93
  27. package/hub/store.mjs +682 -37
  28. package/hub/team/backend.mjs +4 -2
  29. package/hub/team/claude-daemon-control.mjs +2 -0
  30. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  31. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  32. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  33. package/hub/team/codex-review.mjs +2 -3
  34. package/hub/team/conductor.mjs +101 -24
  35. package/hub/team/cto-auto-collect.mjs +2 -6
  36. package/hub/team/cto-pull.mjs +4 -0
  37. package/hub/team/dashboard-open.mjs +1 -0
  38. package/hub/team/execution-mode.mjs +50 -4
  39. package/hub/team/headless.mjs +505 -78
  40. package/hub/team/interactive-tui-transport.mjs +15 -1
  41. package/hub/team/launcher-template.mjs +19 -2
  42. package/hub/team/orchestrator.mjs +36 -9
  43. package/hub/team/pane.mjs +176 -14
  44. package/hub/team/psmux.mjs +79 -70
  45. package/hub/team/retry-state-machine.mjs +41 -13
  46. package/hub/team/session.mjs +57 -0
  47. package/hub/team/swarm-hypervisor.mjs +1 -0
  48. package/hub/team/swarm-preflight.mjs +47 -0
  49. package/hub/team/synapse-http.mjs +149 -4
  50. package/hub/team/terminal-opener.mjs +68 -13
  51. package/hub/team/tui-viewer.mjs +65 -14
  52. package/hub/team/worktree-lifecycle.mjs +56 -59
  53. package/hub/tools.mjs +17 -0
  54. package/hub/workers/codex-mcp.mjs +52 -7
  55. package/hub/workers/delegator-mcp.mjs +3 -49
  56. package/hud/cli-policy.mjs +69 -0
  57. package/hud/constants.mjs +20 -17
  58. package/hud/context-monitor.mjs +31 -21
  59. package/hud/hud-qos-status.mjs +81 -91
  60. package/hud/providers/claude.mjs +150 -84
  61. package/hud/providers/codex.mjs +10 -1
  62. package/hud/renderers.mjs +21 -59
  63. package/hud/utils.mjs +1 -15
  64. package/package.json +2 -1
  65. package/scripts/__tests__/keyword-detector.test.mjs +89 -1
  66. package/scripts/__tests__/lint-skills.test.mjs +68 -0
  67. package/scripts/__tests__/release-governance.test.mjs +94 -1
  68. package/scripts/__tests__/skill-surface.test.mjs +6 -4
  69. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +6 -4
  70. package/scripts/__tests__/tfx-route-node-entry.test.mjs +53 -11
  71. package/scripts/claude-login-detect.mjs +107 -25
  72. package/scripts/ensure-agy-hooks.mjs +31 -1
  73. package/scripts/keyword-detector.mjs +66 -9
  74. package/scripts/lib/agent-route-policy.mjs +367 -0
  75. package/scripts/lib/cli-agy.mjs +24 -4
  76. package/scripts/lib/cli-codex.mjs +22 -160
  77. package/scripts/lib/codex-profile-config.mjs +48 -4
  78. package/scripts/lib/codex-recovery.sh +19 -0
  79. package/scripts/lib/gemini-profiles.mjs +113 -29
  80. package/scripts/lib/keyword-rules.mjs +24 -0
  81. package/scripts/lib/logger.mjs +60 -43
  82. package/scripts/lib/machine-profile.mjs +293 -0
  83. package/scripts/lib/mcp-health.mjs +44 -18
  84. package/scripts/lint-skills.mjs +65 -0
  85. package/scripts/notion-read.mjs +2 -1
  86. package/scripts/pack.mjs +13 -0
  87. package/scripts/release/check-packages-mirror.mjs +58 -1
  88. package/scripts/release/lib.mjs +1 -2
  89. package/scripts/release/prepare.mjs +24 -9
  90. package/scripts/remote-spawn.mjs +830 -32
  91. package/scripts/session-stale-cleanup.mjs +0 -1
  92. package/scripts/setup.mjs +548 -125
  93. package/scripts/sync-hub-mcp-settings.mjs +11 -3
  94. package/scripts/test-lock.mjs +15 -1
  95. package/scripts/tfx-gate-activate.mjs +4 -4
  96. package/scripts/tfx-route-post.mjs +90 -13
  97. package/scripts/tfx-route.mjs +14 -1
  98. package/scripts/tfx-route.sh +716 -177
  99. package/skills/merge-worktree/SKILL.md +3 -3
  100. package/skills/star-prompt/skill.json +1 -1
  101. package/skills/tfx-analysis/SKILL.md +8 -9
  102. package/skills/tfx-analysis/skill.json +1 -10
  103. package/skills/tfx-auto/SKILL.md +203 -46
  104. package/skills/tfx-auto/skill.json +2 -23
  105. package/skills/tfx-doctor/skill.json +0 -3
  106. package/skills/tfx-find/skill.json +0 -7
  107. package/skills/tfx-forge/SKILL.md +2 -2
  108. package/skills/tfx-forge/skill.json +0 -7
  109. package/skills/tfx-goal-clarify/skill.json +5 -0
  110. package/skills/tfx-harness/SKILL.md +36 -70
  111. package/skills/tfx-harness/skill.json +5 -0
  112. package/skills/tfx-hooks/SKILL.md +0 -2
  113. package/skills/tfx-hooks/skill.json +0 -3
  114. package/skills/tfx-hub/SKILL.md +2 -7
  115. package/skills/tfx-hub/skill.json +0 -3
  116. package/skills/tfx-index/skill.json +0 -6
  117. package/skills/tfx-interview/SKILL.md +3 -3
  118. package/skills/tfx-interview/skill.json +1 -13
  119. package/skills/tfx-live/SKILL.md +22 -1
  120. package/skills/tfx-live/skill.json +0 -5
  121. package/skills/tfx-plan/SKILL.md +7 -8
  122. package/skills/tfx-plan/skill.json +1 -12
  123. package/skills/tfx-profile/SKILL.md +25 -15
  124. package/skills/tfx-profile/skill.json +0 -3
  125. package/skills/tfx-prune/SKILL.md +7 -7
  126. package/skills/tfx-prune/skill.json +1 -8
  127. package/skills/tfx-qa/SKILL.md +7 -7
  128. package/skills/tfx-qa/skill.json +1 -11
  129. package/skills/tfx-ralph/SKILL.md +8 -11
  130. package/skills/tfx-remote/SKILL.md +2 -2
  131. package/skills/tfx-remote/skill.json +0 -3
  132. package/skills/tfx-research/SKILL.md +2 -2
  133. package/skills/tfx-research/skill.json +1 -16
  134. package/skills/tfx-review/SKILL.md +11 -11
  135. package/skills/tfx-review/skill.json +1 -11
  136. package/skills/tfx-setup/SKILL.md +5 -6
  137. package/skills/tfx-setup/skill.json +0 -3
  138. package/skills/tfx-ship/skill.json +1 -9
  139. package/skills/tfx-wt/skill.json +0 -7
  140. package/tui/codex-profile.mjs +13 -3
  141. package/tui/gemini-profile.mjs +11 -24
  142. package/tui/monitor.mjs +1 -0
  143. package/tui/setup.mjs +3 -1
  144. package/scripts/headless-guard-fast.sh +0 -29
  145. package/scripts/headless-guard.mjs +0 -523
  146. package/skills/tfx-autopilot/SKILL.md +0 -51
  147. package/skills/tfx-autopilot/skill.json +0 -12
  148. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +0 -75
  149. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +0 -42
  150. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +0 -39
  151. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +0 -87
  152. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +0 -35
  153. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +0 -275
  154. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +0 -56
  155. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +0 -5
  156. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +0 -39
  157. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +0 -92
  158. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +0 -71
  159. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +0 -264
  160. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +0 -113
  161. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +0 -5
  162. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +0 -32
  163. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +0 -29
  164. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +0 -96
  165. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +0 -38
  166. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +0 -151
  167. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +0 -51
  168. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +0 -5
  169. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +0 -29
  170. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +0 -127
  171. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +0 -57
  172. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +0 -129
  173. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +0 -84
  174. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +0 -5
  175. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +0 -27
  176. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +0 -24
  177. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +0 -98
  178. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +0 -65
  179. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +0 -123
  180. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +0 -66
  181. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +0 -5
  182. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +0 -20
  183. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +0 -88
  184. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +0 -40
  185. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +0 -130
  186. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +0 -61
  187. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +0 -5
  188. package/skills/tfx-consensus/SKILL.md +0 -56
  189. package/skills/tfx-consensus/skill.json +0 -8
  190. package/skills/tfx-debate/SKILL.md +0 -60
  191. package/skills/tfx-debate/skill.json +0 -14
  192. package/skills/tfx-fullcycle/SKILL.md +0 -47
  193. package/skills/tfx-fullcycle/skill.json +0 -13
  194. package/skills/tfx-multi/SKILL.md +0 -45
  195. package/skills/tfx-multi/references/agent-wrapper-rules.md +0 -94
  196. package/skills/tfx-multi/references/thorough-pipeline.md +0 -105
  197. package/skills/tfx-multi/skill.json +0 -10
  198. package/skills/tfx-panel/SKILL.md +0 -60
  199. package/skills/tfx-panel/skill.json +0 -14
  200. package/skills/tfx-persist/SKILL.md +0 -63
  201. package/skills/tfx-persist/skill.json +0 -14
  202. package/skills/tfx-psmux-rules/SKILL.md +0 -41
  203. package/skills/tfx-psmux-rules/skill.json +0 -10
  204. package/skills/tfx-remote-setup/SKILL.md +0 -42
  205. package/skills/tfx-remote-setup/skill.json +0 -10
  206. package/skills/tfx-remote-spawn/SKILL.md +0 -52
  207. package/skills/tfx-remote-spawn/skill.json +0 -10
  208. package/skills/tfx-swarm/SKILL.md +0 -49
  209. package/skills/tfx-swarm/skill.json +0 -13
package/hub/schema.sql CHANGED
@@ -10,8 +10,13 @@ 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
- metadata_json TEXT NOT NULL DEFAULT '{}'
15
+ metadata_json TEXT NOT NULL DEFAULT '{}',
16
+ project_id TEXT,
17
+ session_id TEXT,
18
+ host_id TEXT,
19
+ transport_locators_json TEXT NOT NULL DEFAULT '[]'
15
20
  );
16
21
 
17
22
  -- 메시지 테이블
@@ -28,7 +33,61 @@ CREATE TABLE IF NOT EXISTS messages (
28
33
  correlation_id TEXT NOT NULL,
29
34
  trace_id TEXT NOT NULL,
30
35
  payload_json TEXT NOT NULL DEFAULT '{}',
31
- status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter'))
36
+ status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter')),
37
+ role_key_wire TEXT
38
+ );
39
+
40
+ -- 동적 역할 레지스트리 (schema v6)
41
+ CREATE TABLE IF NOT EXISTS role_registry (
42
+ role_key_wire TEXT PRIMARY KEY,
43
+ project_id TEXT NOT NULL,
44
+ role_kind TEXT NOT NULL CHECK (role_kind IN ('cto','lead')),
45
+ scope_id TEXT NOT NULL DEFAULT '',
46
+ state TEXT NOT NULL CHECK (
47
+ state IN (
48
+ 'electable',
49
+ 'elected-probing',
50
+ 'active',
51
+ 'unreachable',
52
+ 'quarantined'
53
+ )
54
+ ),
55
+ holder_agent_id TEXT,
56
+ previous_holder_agent_id TEXT,
57
+ epoch INTEGER NOT NULL DEFAULT 0 CHECK (epoch >= 0),
58
+ activation_seq INTEGER NOT NULL DEFAULT 0 CHECK (activation_seq >= 0),
59
+ activation_id TEXT,
60
+ activation_deadline_ms INTEGER,
61
+ holder_lease_expires_ms INTEGER,
62
+ charter_version TEXT,
63
+ charter_acked_epoch INTEGER,
64
+ retry_count INTEGER NOT NULL DEFAULT 0,
65
+ next_probe_ms INTEGER,
66
+ blocked_reason TEXT,
67
+ last_transition_ms INTEGER NOT NULL,
68
+ last_reason TEXT NOT NULL,
69
+ version INTEGER NOT NULL DEFAULT 0,
70
+ CHECK (
71
+ (role_kind = 'cto' AND scope_id = '') OR
72
+ (role_kind = 'lead' AND scope_id <> '')
73
+ ),
74
+ UNIQUE(project_id, role_kind, scope_id)
75
+ );
76
+
77
+ CREATE TABLE IF NOT EXISTS role_candidates (
78
+ role_key_wire TEXT NOT NULL,
79
+ agent_id TEXT NOT NULL,
80
+ source TEXT NOT NULL CHECK (source IN ('grant','standup','operator')),
81
+ priority INTEGER NOT NULL DEFAULT 0,
82
+ transport_locators_json TEXT NOT NULL DEFAULT '[]',
83
+ consecutive_probe_failures INTEGER NOT NULL DEFAULT 0,
84
+ excluded_until_ms INTEGER,
85
+ last_probe_ms INTEGER,
86
+ last_probe_code TEXT,
87
+ updated_at_ms INTEGER NOT NULL,
88
+ PRIMARY KEY(role_key_wire, agent_id),
89
+ FOREIGN KEY(role_key_wire)
90
+ REFERENCES role_registry(role_key_wire) ON DELETE CASCADE
32
91
  );
33
92
 
34
93
  -- 메시지 수신함 (배달 추적)
@@ -73,6 +132,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_correlation ON messages(correlation_id);
73
132
  CREATE INDEX IF NOT EXISTS idx_messages_trace ON messages(trace_id);
74
133
  CREATE INDEX IF NOT EXISTS idx_messages_expires ON messages(expires_at_ms);
75
134
  CREATE INDEX IF NOT EXISTS idx_messages_priority ON messages(priority DESC, created_at_ms ASC);
135
+ CREATE INDEX IF NOT EXISTS idx_messages_role_key ON messages(role_key_wire, status, expires_at_ms);
76
136
  CREATE INDEX IF NOT EXISTS idx_inbox_agent ON message_inbox(agent_id, delivered_at_ms);
77
137
  CREATE INDEX IF NOT EXISTS idx_inbox_message ON message_inbox(message_id);
78
138
  CREATE INDEX IF NOT EXISTS idx_human_requests_state ON human_requests(state);
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
 
@@ -1558,6 +1560,38 @@ export async function startHub({
1558
1560
  try {
1559
1561
  const body = await parseBody(req);
1560
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
+ }
1561
1595
  const result = synapseRegistry.register(sessionId, body);
1562
1596
  if (!result?.ok) {
1563
1597
  throw new Error(result?.reason || "register failed");
@@ -1688,6 +1722,8 @@ export async function startHub({
1688
1722
  agent_id,
1689
1723
  reason = "manual",
1690
1724
  requested_by = "http",
1725
+ hub_instance_id,
1726
+ store_fingerprint,
1691
1727
  } = body;
1692
1728
  if (!role || !agent_id) {
1693
1729
  return writeJson(res, 400, {
@@ -1700,6 +1736,8 @@ export async function startHub({
1700
1736
  agent_id,
1701
1737
  reason,
1702
1738
  requested_by,
1739
+ hub_instance_id,
1740
+ store_fingerprint,
1703
1741
  });
1704
1742
  return writeJson(res, result.ok ? 200 : 409, result);
1705
1743
  }
@@ -1712,6 +1750,12 @@ export async function startHub({
1712
1750
  topics = [],
1713
1751
  capabilities = [],
1714
1752
  metadata = {},
1753
+ project_id,
1754
+ session_id,
1755
+ host_id,
1756
+ transport_locators_json,
1757
+ hub_instance_id,
1758
+ store_fingerprint,
1715
1759
  } = body;
1716
1760
  if (!agent_id || !cli) {
1717
1761
  return writeJson(res, 400, {
@@ -1740,6 +1784,14 @@ export async function startHub({
1740
1784
  topics,
1741
1785
  heartbeat_ttl_ms,
1742
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 }),
1743
1795
  });
1744
1796
  if (result.ok) {
1745
1797
  workerSpans.set(agent_id, {
@@ -1774,7 +1826,13 @@ export async function startHub({
1774
1826
  }
1775
1827
 
1776
1828
  if (path === "/bridge/heartbeat" && req.method === "POST") {
1777
- 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;
1778
1836
  if (!agent_id)
1779
1837
  return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
1780
1838
  const result = await pipe.executeCommand("heartbeat", {
@@ -1783,6 +1841,9 @@ export async function startHub({
1783
1841
  ttl_ms,
1784
1842
  resolveWorkerLeaseTtlMs(),
1785
1843
  ),
1844
+ presence_generation,
1845
+ hub_instance_id,
1846
+ store_fingerprint,
1786
1847
  });
1787
1848
  return writeJson(res, result.ok ? 200 : 404, result);
1788
1849
  }
@@ -1863,6 +1924,9 @@ export async function startHub({
1863
1924
  status,
1864
1925
  include_metrics = true,
1865
1926
  trace_id,
1927
+ presence_generation,
1928
+ hub_instance_id,
1929
+ store_fingerprint,
1866
1930
  } = body;
1867
1931
 
1868
1932
  if (agent_id && status) {
@@ -1883,7 +1947,15 @@ export async function startHub({
1883
1947
  ]).has(normalizedStatus)
1884
1948
  ? normalizedStatus
1885
1949
  : "online";
1886
- 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
+ }
1887
1959
  const snapshot = await pipe.executeQuery("status", {
1888
1960
  scope: "agent",
1889
1961
  agent_id: normalizedAgentId,
@@ -1897,6 +1969,8 @@ export async function startHub({
1897
1969
  reported_status: normalizedStatus,
1898
1970
  reported_at_ms: Date.now(),
1899
1971
  snapshot: snapshot?.data?.agent || null,
1972
+ hub_instance_id: transition.data.hub_instance_id,
1973
+ store_fingerprint: transition.data.store_fingerprint,
1900
1974
  },
1901
1975
  });
1902
1976
  }
@@ -1906,6 +1980,8 @@ export async function startHub({
1906
1980
  agent_id,
1907
1981
  include_metrics,
1908
1982
  trace_id,
1983
+ hub_instance_id,
1984
+ store_fingerprint,
1909
1985
  });
1910
1986
  return writeJson(res, 200, result);
1911
1987
  }
@@ -2125,12 +2201,20 @@ export async function startHub({
2125
2201
  }
2126
2202
 
2127
2203
  if (path === "/bridge/deregister" && req.method === "POST") {
2128
- const { agent_id } = body;
2204
+ const {
2205
+ agent_id,
2206
+ presence_generation,
2207
+ hub_instance_id,
2208
+ store_fingerprint,
2209
+ } = body;
2129
2210
  if (!agent_id) {
2130
2211
  return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
2131
2212
  }
2132
2213
  const result = await pipe.executeCommand("deregister", {
2133
2214
  agent_id,
2215
+ presence_generation,
2216
+ hub_instance_id,
2217
+ store_fingerprint,
2134
2218
  });
2135
2219
  if (result.ok) {
2136
2220
  finishWorkerTrace(agent_id, "deregister");
@@ -2274,102 +2358,107 @@ export async function startHub({
2274
2358
  httpServer.requestTimeout = 30000;
2275
2359
  httpServer.headersTimeout = 10000;
2276
2360
 
2277
- router.startSweeper();
2361
+ let hitlTimer = null;
2362
+ let sessionTimer = null;
2363
+ let synapsePruneTimer = null;
2364
+ let orphanCleanupTimer = null;
2365
+ let rateLimitTimer = null;
2278
2366
 
2279
- const hitlTimer = setInterval(() => {
2280
- try {
2281
- hitl.checkTimeouts();
2282
- } catch (err) {
2283
- hubLog.warn({ err }, "hitl.timeout_check_failed");
2284
- }
2285
- }, 10000);
2286
- hitlTimer.unref();
2287
-
2288
- // MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
2289
- // Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
2290
- const SESSION_TTL_MS =
2291
- parseInt(process.env.TFX_SESSION_TTL_MS || "", 10) || 30 * 60 * 1000;
2292
- const sessionTimer = setInterval(() => {
2293
- const now = Date.now();
2294
- for (const [sid, session] of transports) {
2295
- if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS)
2296
- continue;
2297
- void closeMcpTransportSession(sid, session, "session_ttl");
2298
- }
2299
- }, 60000);
2300
- 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();
2301
2390
 
2302
- const synapsePruneTimer = setInterval(() => {
2303
- try {
2304
- const { count } = synapseRegistry.pruneExpired();
2305
- if (count > 0) hubLog.info({ count }, "synapse.prune");
2306
- } catch (err) {
2307
- hubLog.warn({ err }, "synapse.prune_failed");
2308
- }
2309
- }, 60000);
2310
- synapsePruneTimer.unref();
2311
-
2312
- // 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
2313
- // TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
2314
- const orphanCleanupTimer = setInterval(
2315
- () => {
2316
- if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
2391
+ synapsePruneTimer = setInterval(() => {
2317
2392
  try {
2318
- const { killed, killedProcesses = [] } = cleanupOrphanNodeProcesses();
2319
- const {
2320
- killed: runtimeKilled,
2321
- killedProcesses: runtimeKilledProcesses = [],
2322
- } = cleanupOrphanRuntimeProcesses();
2323
- const totalKilled = killed + runtimeKilled;
2324
- if (totalKilled > 0) {
2325
- hubLog.info(
2326
- {
2327
- killed: totalKilled,
2328
- processes: [...killedProcesses, ...runtimeKilledProcesses],
2329
- caller: "timer",
2330
- },
2331
- "hub.orphan_cleanup",
2332
- );
2333
- }
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
+ }
2334
2423
 
2335
- const { killed: fsmonitorKilled, stale } = cleanupStaleFsmonitorDaemons(
2336
- {
2337
- minAgeMs: 24 * 60 * 60 * 1000,
2338
- },
2339
- );
2340
- if (fsmonitorKilled > 0) {
2341
- hubLog.info(
2342
- { killed: fsmonitorKilled, stale: stale.length },
2343
- "hub.fsmonitor_cleanup",
2344
- );
2345
- }
2346
- } 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 {}
2347
2435
 
2348
- // stale tfx-spawn-* psmux 세션 정리 (30분 이상 idle)
2349
- try {
2350
- const staleKilled = cleanupStaleSpawnSessions(hubLog);
2351
- if (staleKilled > 0) {
2352
- 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);
2353
2457
  }
2354
- } catch {}
2355
- },
2356
- 5 * 60 * 1000,
2357
- );
2358
- orphanCleanupTimer.unref();
2359
-
2360
- // Evict stale rate-limit buckets once per minute to bound memory usage.
2361
- const rateLimitTimer = setInterval(() => {
2362
- const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
2363
- for (const [ip, timestamps] of rateLimitMap) {
2364
- const fresh = timestamps.filter((t) => t >= cutoff);
2365
- if (fresh.length === 0) {
2366
- rateLimitMap.delete(ip);
2367
- } else {
2368
- rateLimitMap.set(ip, fresh);
2369
2458
  }
2370
- }
2371
- }, RATE_LIMIT_WINDOW_MS);
2372
- rateLimitTimer.unref();
2459
+ }, RATE_LIMIT_WINDOW_MS);
2460
+ rateLimitTimer.unref();
2461
+ };
2373
2462
 
2374
2463
  mkdirSync(PID_DIR, { recursive: true });
2375
2464
 
@@ -2377,6 +2466,19 @@ export async function startHub({
2377
2466
  try {
2378
2467
  router.stopSweeper();
2379
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();
2380
2482
  try {
2381
2483
  await pipe.stop();
2382
2484
  } catch {}
@@ -2386,6 +2488,9 @@ export async function startHub({
2386
2488
  try {
2387
2489
  await delegatorWorker.stop();
2388
2490
  } catch {}
2491
+ try {
2492
+ synapseRegistry.destroy();
2493
+ } catch {}
2389
2494
  try {
2390
2495
  store.close();
2391
2496
  } catch {}
@@ -2396,6 +2501,22 @@ export async function startHub({
2396
2501
  };
2397
2502
 
2398
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();
2399
2520
  await pipe.start();
2400
2521
  await assignCallbacks.start();
2401
2522
  } catch (error) {