triflux 10.4.0 → 10.7.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 (391) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/bin/tfx-doctor-tui.mjs +1 -1
  4. package/bin/tfx-doctor.mjs +6 -1
  5. package/bin/tfx-profile.mjs +1 -1
  6. package/bin/tfx-setup-tui.mjs +1 -1
  7. package/bin/tfx-setup.mjs +6 -1
  8. package/bin/triflux.mjs +2396 -1140
  9. package/config/mcp-registry.json +29 -0
  10. package/hooks/agent-route-guard.mjs +12 -8
  11. package/hooks/cross-review-tracker.mjs +21 -8
  12. package/hooks/error-context.mjs +19 -7
  13. package/hooks/hook-adaptive-collector.mjs +18 -16
  14. package/hooks/hook-manager.mjs +93 -32
  15. package/hooks/hook-orchestrator.mjs +108 -24
  16. package/hooks/hook-registry.json +267 -256
  17. package/hooks/keyword-rules.json +6 -10
  18. package/hooks/lib/resolve-root.mjs +1 -1
  19. package/hooks/mcp-config-watcher.mjs +6 -2
  20. package/hooks/pipeline-stop.mjs +3 -6
  21. package/hooks/safety-guard.mjs +99 -28
  22. package/hooks/session-start-fast.mjs +143 -0
  23. package/hooks/subagent-verifier.mjs +5 -4
  24. package/hub/account-broker.mjs +256 -60
  25. package/hub/adaptive-diagnostic.mjs +75 -48
  26. package/hub/adaptive-inject.mjs +96 -58
  27. package/hub/adaptive-memory.mjs +156 -42
  28. package/hub/adaptive.mjs +60 -31
  29. package/hub/assign-callbacks.mjs +163 -126
  30. package/hub/bridge.mjs +0 -1
  31. package/hub/cli-adapter-base.mjs +200 -48
  32. package/hub/codex-adapter.mjs +76 -96
  33. package/hub/codex-compat.mjs +3 -3
  34. package/hub/codex-preflight.mjs +63 -37
  35. package/hub/delegator/contracts.mjs +19 -23
  36. package/hub/delegator/index.mjs +14 -14
  37. package/hub/delegator/service.mjs +88 -64
  38. package/hub/delegator/tool-definitions.mjs +35 -35
  39. package/hub/fullcycle.mjs +33 -17
  40. package/hub/gemini-adapter.mjs +69 -94
  41. package/hub/hitl.mjs +202 -143
  42. package/hub/intent.mjs +161 -38
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/mcp-response-cache.mjs +66 -32
  45. package/hub/lib/memory-store.mjs +285 -111
  46. package/hub/lib/path-utils.mjs +35 -37
  47. package/hub/lib/process-utils.mjs +106 -37
  48. package/hub/lib/spawn-trace.mjs +527 -0
  49. package/hub/lib/ssh-command.mjs +34 -4
  50. package/hub/lib/ssh-retry.mjs +5 -1
  51. package/hub/lib/uuidv7.mjs +4 -3
  52. package/hub/memory-doctor.mjs +266 -106
  53. package/hub/middleware/request-logger.mjs +61 -34
  54. package/hub/paths.mjs +9 -9
  55. package/hub/pipeline/gates/confidence.mjs +34 -15
  56. package/hub/pipeline/gates/consensus.mjs +27 -15
  57. package/hub/pipeline/gates/index.mjs +7 -3
  58. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  59. package/hub/pipeline/index.mjs +77 -42
  60. package/hub/pipeline/state.mjs +10 -10
  61. package/hub/pipeline/transitions.mjs +40 -23
  62. package/hub/platform.mjs +57 -48
  63. package/hub/promote-penalties.mjs +25 -7
  64. package/hub/quality/deslop.mjs +70 -49
  65. package/hub/research.mjs +32 -25
  66. package/hub/router.mjs +924 -791
  67. package/hub/routing/complexity.mjs +132 -29
  68. package/hub/routing/index.mjs +17 -12
  69. package/hub/routing/q-learning.mjs +76 -28
  70. package/hub/server.mjs +4 -4
  71. package/hub/session-fingerprint.mjs +127 -61
  72. package/hub/state.mjs +84 -43
  73. package/hub/store-adapter.mjs +59 -26
  74. package/hub/store.mjs +356 -153
  75. package/hub/team/agent-map.json +22 -7
  76. package/hub/team/ansi.mjs +186 -122
  77. package/hub/team/backend.mjs +28 -10
  78. package/hub/team/cli/commands/attach.mjs +57 -37
  79. package/hub/team/cli/commands/control.mjs +29 -8
  80. package/hub/team/cli/commands/debug.mjs +95 -74
  81. package/hub/team/cli/commands/focus.mjs +80 -53
  82. package/hub/team/cli/commands/interrupt.mjs +18 -6
  83. package/hub/team/cli/commands/kill.mjs +16 -5
  84. package/hub/team/cli/commands/list.mjs +31 -24
  85. package/hub/team/cli/commands/send.mjs +19 -6
  86. package/hub/team/cli/commands/start/index.mjs +154 -31
  87. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  88. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  89. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  90. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  91. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  92. package/hub/team/cli/commands/status.mjs +43 -14
  93. package/hub/team/cli/commands/stop.mjs +11 -4
  94. package/hub/team/cli/commands/task.mjs +8 -3
  95. package/hub/team/cli/commands/tasks.mjs +13 -13
  96. package/hub/team/cli/index.mjs +2 -2
  97. package/hub/team/cli/manifest.mjs +38 -8
  98. package/hub/team/cli/render.mjs +52 -30
  99. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  100. package/hub/team/cli/services/hub-client.mjs +42 -14
  101. package/hub/team/cli/services/member-selector.mjs +37 -30
  102. package/hub/team/cli/services/native-control.mjs +143 -116
  103. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  104. package/hub/team/cli/services/state-store.mjs +25 -8
  105. package/hub/team/cli/services/task-model.mjs +40 -30
  106. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  107. package/hub/team/conductor.mjs +8 -4
  108. package/hub/team/dashboard-anchor.mjs +4 -5
  109. package/hub/team/dashboard-layout.mjs +3 -1
  110. package/hub/team/dashboard-open.mjs +41 -21
  111. package/hub/team/dashboard.mjs +76 -28
  112. package/hub/team/event-log.mjs +18 -10
  113. package/hub/team/handoff.mjs +31 -15
  114. package/hub/team/headless.mjs +2 -1
  115. package/hub/team/health-probe.mjs +69 -54
  116. package/hub/team/launcher-template.mjs +16 -13
  117. package/hub/team/native-supervisor.mjs +65 -21
  118. package/hub/team/native.mjs +74 -35
  119. package/hub/team/nativeProxy.mjs +184 -113
  120. package/hub/team/notify.mjs +120 -77
  121. package/hub/team/orchestrator.mjs +166 -161
  122. package/hub/team/pane.mjs +12 -7
  123. package/hub/team/process-cleanup.mjs +25 -16
  124. package/hub/team/psmux.mjs +491 -201
  125. package/hub/team/remote-probe.mjs +68 -52
  126. package/hub/team/remote-session.mjs +117 -59
  127. package/hub/team/remote-watcher.mjs +61 -33
  128. package/hub/team/routing.mjs +51 -25
  129. package/hub/team/runtime-strategy.mjs +3 -1
  130. package/hub/team/session.mjs +675 -611
  131. package/hub/team/shared.mjs +13 -13
  132. package/hub/team/staleState.mjs +72 -30
  133. package/hub/team/swarm-locks.mjs +15 -13
  134. package/hub/team/swarm-planner.mjs +32 -21
  135. package/hub/team/swarm-reconciler.mjs +48 -23
  136. package/hub/team/tui-lite.mjs +266 -68
  137. package/hub/team/tui-remote-adapter.mjs +14 -10
  138. package/hub/team/tui-viewer.mjs +99 -43
  139. package/hub/team/tui.mjs +708 -271
  140. package/hub/team/worktree-lifecycle.mjs +152 -58
  141. package/hub/team/wt-manager.mjs +24 -14
  142. package/hub/token-mode.mjs +71 -71
  143. package/hub/tray.mjs +411 -368
  144. package/hub/workers/claude-worker.mjs +162 -118
  145. package/hub/workers/codex-mcp.mjs +558 -507
  146. package/hub/workers/delegator-mcp.mjs +507 -333
  147. package/hub/workers/factory.mjs +21 -21
  148. package/hub/workers/gemini-worker.mjs +115 -84
  149. package/hub/workers/interface.mjs +6 -1
  150. package/hub/workers/worker-utils.mjs +21 -14
  151. package/hud/colors.mjs +27 -9
  152. package/hud/constants.mjs +162 -26
  153. package/hud/context-monitor.mjs +82 -41
  154. package/hud/hud-qos-status.mjs +129 -49
  155. package/hud/mission-board.mjs +6 -3
  156. package/hud/providers/claude.mjs +226 -115
  157. package/hud/providers/codex.mjs +62 -22
  158. package/hud/providers/gemini.mjs +168 -56
  159. package/hud/renderers.mjs +384 -119
  160. package/hud/terminal.mjs +101 -31
  161. package/hud/utils.mjs +78 -38
  162. package/mesh/index.mjs +11 -5
  163. package/mesh/mesh-budget.mjs +18 -9
  164. package/mesh/mesh-heartbeat.mjs +1 -1
  165. package/mesh/mesh-queue.mjs +3 -5
  166. package/mesh/mesh-router.mjs +5 -4
  167. package/package.json +56 -21
  168. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  169. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  170. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  171. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  172. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  173. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  174. package/scripts/__tests__/skill-template.test.mjs +98 -50
  175. package/scripts/__tests__/smoke.test.mjs +1 -1
  176. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  177. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  178. package/scripts/cache-doctor.mjs +11 -4
  179. package/scripts/cache-warmup.mjs +96 -37
  180. package/scripts/claudemd-sync.mjs +27 -17
  181. package/scripts/codex-gateway-preflight.mjs +52 -37
  182. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +139 -120
  193. package/scripts/keyword-detector.mjs +293 -272
  194. package/scripts/keyword-rules-expander.mjs +538 -521
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +669 -554
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +61 -57
  229. package/scripts/tfx-batch-stats.mjs +117 -96
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/idle-notif-cooldown.json +1 -1
  237. package/skills/.omc/state/last-tool-error.json +1 -1
  238. package/skills/.omc/state/subagent-tracking.json +1 -1
  239. package/skills/_templates/base.md +1 -6
  240. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  241. package/skills/shared/telemetry-segment.md +6 -0
  242. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  243. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  244. package/skills/tfx-analysis/skill.json +1 -6
  245. package/skills/tfx-auto/SKILL.md +1 -0
  246. package/skills/{tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md → tfx-auto-codex/SKILL.md.tmpl} +33 -4
  247. package/skills/tfx-auto-codex/skill.json +1 -3
  248. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  249. package/skills/tfx-autopilot/skill.json +1 -5
  250. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  251. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  252. package/skills/tfx-autoroute/skill.json +1 -7
  253. package/skills/tfx-codex/SKILL.md +1 -0
  254. package/skills/tfx-codex/skill.json +1 -3
  255. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  256. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  257. package/skills/tfx-codex-swarm/skill.json +1 -4
  258. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  265. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  266. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  267. package/skills/tfx-debate/skill.json +1 -7
  268. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  269. package/skills/tfx-deep-analysis/skill.json +1 -5
  270. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  271. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  272. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  273. package/skills/tfx-deep-qa/skill.json +1 -6
  274. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  275. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  276. package/skills/tfx-doctor/SKILL.md +21 -0
  277. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  278. package/skills/tfx-doctor/skill.json +1 -3
  279. package/skills/tfx-find/SKILL.md +1 -0
  280. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  281. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  282. package/skills/tfx-fullcycle/skill.json +1 -6
  283. package/skills/{tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md → tfx-gemini/SKILL.md.tmpl} +16 -7
  284. package/skills/tfx-gemini/skill.json +1 -3
  285. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  286. package/skills/tfx-hooks/skill.json +1 -3
  287. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  288. package/skills/tfx-hub/skill.json +1 -3
  289. package/skills/tfx-index/SKILL.md +1 -0
  290. package/skills/tfx-index/skill.json +1 -6
  291. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  292. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  293. package/skills/tfx-multi/skill.json +1 -3
  294. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  295. package/skills/tfx-panel/skill.json +1 -7
  296. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  297. package/skills/tfx-persist/skill.json +1 -7
  298. package/skills/tfx-plan/SKILL.md +1 -0
  299. package/skills/tfx-plan/skill.json +1 -6
  300. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  301. package/skills/tfx-profile/skill.json +1 -3
  302. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  303. package/skills/tfx-prune/skill.json +1 -7
  304. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  305. package/skills/tfx-psmux-rules/skill.json +1 -4
  306. package/skills/tfx-qa/SKILL.md +1 -0
  307. package/skills/tfx-qa/skill.json +1 -6
  308. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  309. package/skills/tfx-ralph/skill.json +1 -4
  310. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  311. package/skills/tfx-remote-setup/skill.json +1 -3
  312. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  313. package/skills/tfx-remote-spawn/references/hosts.json +16 -16
  314. package/skills/tfx-remote-spawn/skill.json +1 -4
  315. package/skills/tfx-research/SKILL.md +1 -0
  316. package/skills/tfx-review/SKILL.md +1 -0
  317. package/skills/tfx-review/skill.json +1 -6
  318. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  319. package/skills/tfx-setup/skill.json +1 -3
  320. package/skills/tfx-swarm/SKILL.md +22 -0
  321. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  322. package/tui/codex-profile.mjs +457 -0
  323. package/tui/core.mjs +266 -0
  324. package/tui/doctor.mjs +375 -0
  325. package/tui/gemini-profile.mjs +299 -0
  326. package/tui/monitor-data.mjs +152 -0
  327. package/tui/monitor.mjs +339 -0
  328. package/tui/setup.mjs +533 -0
  329. package/CLAUDE.md +0 -193
  330. package/references/hosts.json +0 -33
  331. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  332. package/skills/tfx-workspace/evals/evals.json +0 -79
  333. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -162
  334. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  335. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -9
  336. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  337. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  338. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -9
  339. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  340. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  341. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  342. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -9
  343. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  344. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  345. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -9
  346. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  347. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  348. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  349. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -8
  350. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  351. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  352. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -8
  353. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  354. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  355. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  356. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -10
  357. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  358. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  359. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -10
  360. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  361. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  362. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  363. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  364. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -10
  365. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  366. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  367. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -10
  368. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  369. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  370. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  371. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -10
  372. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  373. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  374. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -10
  375. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  376. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  377. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -62
  378. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  379. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -11
  380. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  381. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  382. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -11
  383. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  384. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  385. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  386. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  387. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  388. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  389. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  390. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  391. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
package/hub/router.mjs CHANGED
@@ -1,791 +1,924 @@
1
- // hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
2
- // SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
3
- import { EventEmitter, once } from 'node:events';
4
- import { uuidv7 } from './lib/uuidv7.mjs';
5
-
6
- const ASSIGN_PENDING_STATUSES = new Set(['queued', 'running']);
7
-
8
- function uniqueStrings(values = []) {
9
- return Array.from(new Set((values || []).map((value) => String(value || '').trim()).filter(Boolean)));
10
- }
11
-
12
- function clampAssignDuration(value, fallback = 600000, min = 1000, max = 86400000) {
13
- const num = Number(value);
14
- if (!Number.isFinite(num)) return fallback;
15
- return Math.max(min, Math.min(Math.trunc(num), max));
16
- }
17
-
18
- function normalizeAssignTerminalStatus(input, metadata = {}) {
19
- const status = String(input || '').trim().toLowerCase();
20
- const resultTag = String(
21
- metadata?.result
22
- ?? metadata?.status
23
- ?? metadata?.outcome
24
- ?? '',
25
- ).trim().toLowerCase();
26
-
27
- if (status === 'queued') return 'queued';
28
- if (status === 'running' || status === 'in_progress') return 'running';
29
- if (status === 'timed_out' || status === 'timeout') return 'timed_out';
30
- if (status === 'failed' || status === 'error') return 'failed';
31
- if (status === 'succeeded' || status === 'success') return 'succeeded';
32
-
33
- if (status === 'completed') {
34
- if (resultTag === 'failed' || resultTag === 'error') return 'failed';
35
- if (resultTag === 'timed_out' || resultTag === 'timeout') return 'timed_out';
36
- return 'succeeded';
37
- }
38
-
39
- if (resultTag === 'failed' || resultTag === 'error') return 'failed';
40
- if (resultTag === 'timed_out' || resultTag === 'timeout') return 'timed_out';
41
- if (resultTag === 'succeeded' || resultTag === 'success') return 'succeeded';
42
- return 'succeeded';
43
- }
44
-
45
- function normalizeAgentTopics(store, agentId, runtimeTopics) {
46
- const topics = new Set(runtimeTopics || []);
47
- const persisted = store.getAgent(agentId)?.topics || [];
48
- for (const topic of persisted) topics.add(topic);
49
- return Array.from(topics);
50
- }
51
-
52
- /**
53
- * 라우터 생성
54
- * @param {object} store
55
- */
56
- export function createRouter(store) {
57
- let sweepTimer = null;
58
- let staleTimer = null;
59
- const responseEmitter = new EventEmitter();
60
- const deliveryEmitter = new EventEmitter();
61
- responseEmitter.setMaxListeners(200);
62
- deliveryEmitter.setMaxListeners(200);
63
-
64
- const runtimeTopics = new Map();
65
- const queuesByAgent = new Map();
66
- const liveMessages = new Map();
67
- const MAX_LATENCY_SAMPLES = 100;
68
- let latencyIdx = 0;
69
- const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
70
-
71
- function ensureAgentQueue(agentId) {
72
- let queue = queuesByAgent.get(agentId);
73
- if (!queue) {
74
- queue = new Map();
75
- queuesByAgent.set(agentId, queue);
76
- }
77
- return queue;
78
- }
79
-
80
- function recordLatency(ms) {
81
- deliveryLatencies[latencyIdx % MAX_LATENCY_SAMPLES] = ms;
82
- latencyIdx++;
83
- }
84
-
85
- function upsertRuntimeTopics(agentId, topics, { replace = true } = {}) {
86
- const normalized = uniqueStrings(topics);
87
- const current = replace ? new Set() : new Set(runtimeTopics.get(agentId) || []);
88
- for (const topic of normalized) current.add(topic);
89
- runtimeTopics.set(agentId, current);
90
- store.updateAgentTopics(agentId, Array.from(current));
91
- return Array.from(current);
92
- }
93
-
94
- function listRuntimeTopics(agentId) {
95
- return normalizeAgentTopics(store, agentId, runtimeTopics.get(agentId));
96
- }
97
-
98
- function trackMessage(message, recipients) {
99
- liveMessages.set(message.id, {
100
- message,
101
- recipients: new Set(recipients),
102
- ackedBy: new Set(),
103
- });
104
- }
105
-
106
- function getMessageRecord(messageId) {
107
- return liveMessages.get(messageId) || null;
108
- }
109
-
110
- function removeMessage(messageId) {
111
- const record = liveMessages.get(messageId);
112
- if (!record) return;
113
- for (const agentId of record.recipients) {
114
- queuesByAgent.get(agentId)?.delete(messageId);
115
- }
116
- liveMessages.delete(messageId);
117
- }
118
-
119
- function queueMessage(agentId, message) {
120
- const queue = ensureAgentQueue(agentId);
121
- queue.set(message.id, {
122
- message,
123
- attempts: 0,
124
- delivered_at_ms: null,
125
- acked_at_ms: null,
126
- });
127
- deliveryEmitter.emit('message', agentId, message);
128
- }
129
-
130
- function resolveRecipients(msg) {
131
- const to = msg.to_agent ?? msg.to;
132
- if (!to?.startsWith('topic:')) {
133
- return [to];
134
- }
135
-
136
- const topic = to.slice(6);
137
- const recipients = new Set();
138
- for (const [agentId, topics] of runtimeTopics) {
139
- if (topics.has(topic)) recipients.add(agentId);
140
- }
141
- for (const agent of store.getAgentsByTopic(topic)) {
142
- recipients.add(agent.agent_id);
143
- }
144
- return Array.from(recipients);
145
- }
146
-
147
- function sortedPending(agentId, { max_messages = 20, include_topics = null } = {}) {
148
- const queue = ensureAgentQueue(agentId);
149
- const topicFilter = include_topics?.length ? new Set(include_topics) : null;
150
- const now = Date.now();
151
- const pending = [];
152
-
153
- for (const delivery of queue.values()) {
154
- const { message } = delivery;
155
- if (delivery.acked_at_ms) continue;
156
- if (message.expires_at_ms <= now) continue;
157
- if (topicFilter && !topicFilter.has(message.topic)) continue;
158
- pending.push(message);
159
- }
160
-
161
- pending.sort((a, b) => {
162
- if (b.priority !== a.priority) return b.priority - a.priority;
163
- return a.created_at_ms - b.created_at_ms;
164
- });
165
- return pending.slice(0, max_messages);
166
- }
167
-
168
- function markDelivered(agentId, messageId) {
169
- const delivery = queuesByAgent.get(agentId)?.get(messageId);
170
- const record = getMessageRecord(messageId);
171
- if (!delivery || !record) return false;
172
-
173
- delivery.attempts += 1;
174
- if (!delivery.delivered_at_ms) {
175
- delivery.delivered_at_ms = Date.now();
176
- record.message.status = 'delivered';
177
- store.updateMessageStatus(messageId, 'delivered');
178
- recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
179
- return true;
180
- }
181
- return false;
182
- }
183
-
184
- function ackMessages(ids, agentId) {
185
- const now = Date.now();
186
- let count = 0;
187
-
188
- for (const id of ids || []) {
189
- const delivery = queuesByAgent.get(agentId)?.get(id);
190
- const record = getMessageRecord(id);
191
- if (!delivery || !record || delivery.acked_at_ms) continue;
192
-
193
- delivery.acked_at_ms = now;
194
- record.ackedBy.add(agentId);
195
- count += 1;
196
-
197
- if (record.ackedBy.size >= record.recipients.size) {
198
- record.message.status = 'acked';
199
- store.updateMessageStatus(id, 'acked');
200
- removeMessage(id);
201
- }
202
- }
203
-
204
- return count;
205
- }
206
-
207
- function dispatchMessage({ type, from, to, topic, priority = 5, ttl_ms = 300000, payload = {}, trace_id, correlation_id }) {
208
- const msg = store.auditLog({
209
- type,
210
- from,
211
- to,
212
- topic,
213
- priority,
214
- ttl_ms,
215
- payload,
216
- trace_id,
217
- correlation_id,
218
- });
219
- const recipients = uniqueStrings(resolveRecipients(msg));
220
- if (recipients.length) {
221
- trackMessage(msg, recipients);
222
- for (const agentId of recipients) {
223
- queueMessage(agentId, msg);
224
- }
225
- msg.status = 'delivered';
226
- store.updateMessageStatus(msg.id, 'delivered');
227
- }
228
- if (msg.type === 'response') {
229
- responseEmitter.emit(msg.correlation_id, msg.payload);
230
- }
231
- return { msg, recipients };
232
- }
233
-
234
- function buildAssignSnapshot(job, extra = {}) {
235
- if (!job) return null;
236
- return {
237
- job_id: job.job_id,
238
- supervisor_agent: job.supervisor_agent,
239
- worker_agent: job.worker_agent,
240
- topic: job.topic,
241
- task: job.task,
242
- status: job.status,
243
- attempt: job.attempt,
244
- retry_count: job.retry_count,
245
- max_retries: job.max_retries,
246
- timeout_ms: job.timeout_ms,
247
- deadline_ms: job.deadline_ms,
248
- trace_id: job.trace_id,
249
- correlation_id: job.correlation_id,
250
- last_message_id: job.last_message_id,
251
- result: job.result,
252
- error: job.error,
253
- updated_at_ms: job.updated_at_ms,
254
- completed_at_ms: job.completed_at_ms,
255
- ...extra,
256
- };
257
- }
258
-
259
- function notifyAssignSupervisor(job, event, extra = {}) {
260
- if (!job?.supervisor_agent) return null;
261
- const { msg } = dispatchMessage({
262
- type: 'event',
263
- from: job.worker_agent || 'assign-router',
264
- to: job.supervisor_agent,
265
- topic: 'assign.result',
266
- priority: Math.max(5, job.priority || 5),
267
- ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
268
- payload: {
269
- event,
270
- ...buildAssignSnapshot(job),
271
- ...extra,
272
- },
273
- trace_id: job.trace_id,
274
- correlation_id: job.correlation_id,
275
- });
276
- return msg;
277
- }
278
-
279
- function dispatchAssignJob(job, reason = 'dispatch') {
280
- const { msg, recipients } = dispatchMessage({
281
- type: 'handoff',
282
- from: job.supervisor_agent,
283
- to: job.worker_agent,
284
- topic: job.topic || 'assign.job',
285
- priority: job.priority || 5,
286
- ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
287
- payload: {
288
- kind: 'assign.job',
289
- reason,
290
- assign_job_id: job.job_id,
291
- attempt: job.attempt,
292
- retry_count: job.retry_count,
293
- max_retries: job.max_retries,
294
- timeout_ms: job.timeout_ms,
295
- supervisor_agent: job.supervisor_agent,
296
- worker_agent: job.worker_agent,
297
- task: job.task,
298
- payload: job.payload || {},
299
- },
300
- trace_id: job.trace_id,
301
- correlation_id: job.correlation_id,
302
- });
303
-
304
- const updated = store.updateAssignStatus(job.job_id, job.status, {
305
- last_message_id: msg.id,
306
- });
307
- return { job: updated || job, recipients, message_id: msg.id };
308
- }
309
-
310
- function scheduleAssignRetry(job, reason, error = null, requested_by = 'system') {
311
- if (!job) {
312
- return { ok: false, error: { code: 'ASSIGN_NOT_FOUND', message: 'assign job not found' } };
313
- }
314
- if (job.retry_count >= job.max_retries) {
315
- return {
316
- ok: false,
317
- error: {
318
- code: 'ASSIGN_RETRY_EXHAUSTED',
319
- message: `retry exhausted for ${job.job_id}`,
320
- },
321
- };
322
- }
323
-
324
- const queued = store.retryAssign(job.job_id, {
325
- error,
326
- timeout_ms: job.timeout_ms,
327
- ttl_ms: job.ttl_ms,
328
- });
329
- const dispatched = dispatchAssignJob(queued, 'retry');
330
- notifyAssignSupervisor(dispatched.job, 'retry_scheduled', {
331
- retry_reason: reason,
332
- requested_by,
333
- });
334
- return {
335
- ok: true,
336
- data: {
337
- retried: true,
338
- ...buildAssignSnapshot(dispatched.job, {
339
- retry_reason: reason,
340
- requested_by,
341
- }),
342
- },
343
- };
344
- }
345
-
346
- function handleAssignTimeout(job) {
347
- const timedOut = store.updateAssignStatus(job.job_id, 'timed_out', {
348
- error: job.error ?? { message: 'assign job timed out' },
349
- });
350
-
351
- if (timedOut.retry_count < timedOut.max_retries) {
352
- return scheduleAssignRetry(timedOut, 'timed_out', timedOut.error, 'sweeper');
353
- }
354
-
355
- notifyAssignSupervisor(timedOut, 'completed', {
356
- completion_reason: 'timed_out',
357
- });
358
- return { ok: true, data: buildAssignSnapshot(timedOut, { completion_reason: 'timed_out' }) };
359
- }
360
-
361
- const router = {
362
- responseEmitter,
363
- deliveryEmitter,
364
-
365
- registerAgent(args) {
366
- const result = store.registerAgent(args);
367
- upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
368
- return result;
369
- },
370
-
371
- refreshAgentLease(agentId, ttlMs = 30000) {
372
- return store.refreshLease(agentId, ttlMs);
373
- },
374
-
375
- subscribeAgent(agentId, topics, { replace = false } = {}) {
376
- const nextTopics = upsertRuntimeTopics(agentId, topics, { replace });
377
- return { agent_id: agentId, topics: nextTopics };
378
- },
379
-
380
- getSubscribedTopics(agentId) {
381
- return listRuntimeTopics(agentId);
382
- },
383
-
384
- updateAgentStatus(agentId, status) {
385
- if (status === 'offline') {
386
- runtimeTopics.delete(agentId);
387
- }
388
- return store.updateAgentStatus(agentId, status);
389
- },
390
-
391
- route(msg) {
392
- const recipients = uniqueStrings(resolveRecipients(msg));
393
- if (!recipients.length) return 0;
394
- if (!getMessageRecord(msg.id)) {
395
- trackMessage(msg, recipients);
396
- }
397
- for (const agentId of recipients) {
398
- queueMessage(agentId, msg);
399
- }
400
- store.updateMessageStatus(msg.id, 'delivered');
401
- return recipients.length;
402
- },
403
-
404
- getPendingMessages(agentId, options = {}) {
405
- return sortedPending(agentId, options);
406
- },
407
-
408
- countPendingMessages(agentId) {
409
- const queue = ensureAgentQueue(agentId);
410
- const now = Date.now();
411
- let count = 0;
412
- for (const delivery of queue.values()) {
413
- if (delivery.acked_at_ms) continue;
414
- if (delivery.message.expires_at_ms <= now) continue;
415
- count++;
416
- }
417
- return count;
418
- },
419
-
420
- markMessagePushed(agentId, messageId) {
421
- return markDelivered(agentId, messageId);
422
- },
423
-
424
- drainAgent(agentId, { max_messages = 20, include_topics = null, auto_ack = false } = {}) {
425
- const messages = sortedPending(agentId, { max_messages, include_topics });
426
- for (const message of messages) {
427
- markDelivered(agentId, message.id);
428
- }
429
- if (auto_ack && messages.length) {
430
- ackMessages(messages.map((message) => message.id), agentId);
431
- }
432
- return messages;
433
- },
434
-
435
- ackMessages(ids, agentId) {
436
- return ackMessages(ids, agentId);
437
- },
438
-
439
- async handleAsk({
440
- from, to, topic, question, context_refs,
441
- payload = {}, priority = 5, ttl_ms = 300000,
442
- await_response_ms = 0, trace_id, correlation_id,
443
- }) {
444
- const cid = correlation_id || uuidv7();
445
- const tid = trace_id || uuidv7();
446
-
447
- const { msg } = dispatchMessage({
448
- type: 'request',
449
- from,
450
- to,
451
- topic,
452
- priority,
453
- ttl_ms,
454
- payload: { question, context_refs, ...payload },
455
- correlation_id: cid,
456
- trace_id: tid,
457
- });
458
-
459
- if (await_response_ms <= 0) {
460
- return {
461
- ok: true,
462
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'queued' },
463
- };
464
- }
465
-
466
- try {
467
- const [response] = await once(responseEmitter, cid, {
468
- signal: AbortSignal.timeout(Math.min(await_response_ms, 30000)),
469
- });
470
- return {
471
- ok: true,
472
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response },
473
- };
474
- } catch {
475
- const resp = store.getResponseByCorrelation(cid);
476
- if (resp) {
477
- return {
478
- ok: true,
479
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response: resp.payload },
480
- };
481
- }
482
- return {
483
- ok: true,
484
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'delivered' },
485
- };
486
- }
487
- },
488
-
489
- handlePublish({
490
- from, to, topic, priority = 5, ttl_ms = 300000,
491
- payload = {}, trace_id, correlation_id, message_type,
492
- }) {
493
- const type = message_type || (correlation_id ? 'response' : 'event');
494
- const { msg, recipients } = dispatchMessage({
495
- type,
496
- from,
497
- to,
498
- topic,
499
- priority,
500
- ttl_ms,
501
- payload,
502
- trace_id: trace_id || uuidv7(),
503
- correlation_id: correlation_id || uuidv7(),
504
- });
505
- return {
506
- ok: true,
507
- data: {
508
- message_id: msg.id,
509
- fanout_count: recipients.length,
510
- expires_at_ms: msg.expires_at_ms,
511
- },
512
- };
513
- },
514
-
515
- handleHandoff({
516
- from, to, topic, task, acceptance_criteria, context_refs,
517
- priority = 5, ttl_ms = 600000, trace_id, correlation_id,
518
- }) {
519
- const { msg } = dispatchMessage({
520
- type: 'handoff',
521
- from,
522
- to,
523
- topic,
524
- priority,
525
- ttl_ms,
526
- payload: { task, acceptance_criteria, context_refs },
527
- trace_id: trace_id || uuidv7(),
528
- correlation_id: correlation_id || uuidv7(),
529
- });
530
- return {
531
- ok: true,
532
- data: { handoff_message_id: msg.id, state: 'queued', assigned_to: to },
533
- };
534
- },
535
-
536
- assignAsync({
537
- supervisor_agent,
538
- worker_agent,
539
- topic = 'assign.job',
540
- task = '',
541
- payload = {},
542
- priority = 5,
543
- ttl_ms = 600000,
544
- timeout_ms = 600000,
545
- max_retries = 0,
546
- trace_id,
547
- correlation_id,
548
- }) {
549
- const job = store.createAssign({
550
- supervisor_agent,
551
- worker_agent,
552
- topic,
553
- task,
554
- payload,
555
- priority,
556
- ttl_ms,
557
- timeout_ms,
558
- max_retries,
559
- trace_id,
560
- correlation_id,
561
- });
562
- const dispatched = dispatchAssignJob(job, 'create');
563
- return {
564
- ok: true,
565
- data: {
566
- assigned_to: worker_agent,
567
- ...buildAssignSnapshot(dispatched.job),
568
- },
569
- };
570
- },
571
-
572
- reportAssignResult({
573
- job_id,
574
- worker_agent,
575
- status,
576
- attempt,
577
- result,
578
- error,
579
- payload = {},
580
- metadata = {},
581
- }) {
582
- const job = store.getAssign(job_id);
583
- if (!job) {
584
- return {
585
- ok: false,
586
- error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` },
587
- };
588
- }
589
- if (worker_agent && worker_agent !== job.worker_agent) {
590
- return {
591
- ok: false,
592
- error: { code: 'ASSIGN_WORKER_MISMATCH', message: `worker mismatch: ${worker_agent}` },
593
- };
594
- }
595
- if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
596
- return {
597
- ok: false,
598
- error: {
599
- code: 'ASSIGN_ATTEMPT_MISMATCH',
600
- message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
601
- },
602
- };
603
- }
604
-
605
- const mergedMetadata = {
606
- ...(payload?.metadata || {}),
607
- ...(metadata || {}),
608
- };
609
- const normalizedStatus = normalizeAssignTerminalStatus(
610
- status || payload?.status,
611
- mergedMetadata,
612
- );
613
- const nextResult = result ?? (Object.hasOwn(payload || {}, 'result') ? payload.result : payload);
614
- const nextError = error ?? payload?.error ?? null;
615
-
616
- if (normalizedStatus === 'running') {
617
- const running = store.updateAssignStatus(job.job_id, 'running', {
618
- started_at_ms: job.started_at_ms || Date.now(),
619
- deadline_ms: Date.now() + clampAssignDuration(job.timeout_ms, job.timeout_ms),
620
- result: nextResult,
621
- error: nextError,
622
- });
623
- notifyAssignSupervisor(running, 'progress');
624
- return { ok: true, data: buildAssignSnapshot(running) };
625
- }
626
-
627
- const finalized = store.updateAssignStatus(job.job_id, normalizedStatus, {
628
- result: nextResult,
629
- error: nextError,
630
- });
631
-
632
- if ((normalizedStatus === 'failed' || normalizedStatus === 'timed_out')
633
- && finalized.retry_count < finalized.max_retries) {
634
- return scheduleAssignRetry(finalized, normalizedStatus, nextError, worker_agent || finalized.worker_agent);
635
- }
636
-
637
- notifyAssignSupervisor(finalized, 'completed');
638
- return { ok: true, data: buildAssignSnapshot(finalized) };
639
- },
640
-
641
- getAssignStatus({ job_id, ...filters } = {}) {
642
- if (job_id) {
643
- const job = store.getAssign(job_id);
644
- return job
645
- ? { ok: true, data: buildAssignSnapshot(job) }
646
- : { ok: false, error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` } };
647
- }
648
- return {
649
- ok: true,
650
- data: {
651
- assigns: store.listAssigns(filters).map((job) => buildAssignSnapshot(job)),
652
- },
653
- };
654
- },
655
-
656
- retryAssign(job_id, { reason = 'manual', requested_by = 'manual' } = {}) {
657
- const job = store.getAssign(job_id);
658
- if (!job) {
659
- return {
660
- ok: false,
661
- error: { code: 'ASSIGN_NOT_FOUND', message: `assign job not found: ${job_id}` },
662
- };
663
- }
664
- return scheduleAssignRetry(job, reason, job.error, requested_by);
665
- },
666
-
667
- sweepExpired() {
668
- const now = Date.now();
669
- let expired = 0;
670
- for (const [messageId, record] of Array.from(liveMessages.entries())) {
671
- if (record.message.expires_at_ms > now) continue;
672
- store.moveToDeadLetter(messageId, 'ttl_expired', null);
673
- removeMessage(messageId);
674
- expired += 1;
675
- }
676
- return { messages: expired };
677
- },
678
-
679
- sweepTimedOutAssigns() {
680
- const expiredAssigns = store.listAssigns({
681
- statuses: Array.from(ASSIGN_PENDING_STATUSES),
682
- active_before_ms: Date.now(),
683
- limit: 100,
684
- });
685
- let timed_out = 0;
686
- let retried = 0;
687
-
688
- for (const job of expiredAssigns) {
689
- const result = handleAssignTimeout(job);
690
- timed_out += 1;
691
- if (result?.data?.retried) retried += 1;
692
- }
693
-
694
- return { timed_out, retried };
695
- },
696
-
697
- startSweeper() {
698
- if (sweepTimer) return;
699
- sweepTimer = setInterval(() => {
700
- try {
701
- router.sweepExpired();
702
- router.sweepTimedOutAssigns();
703
- } catch {}
704
- }, 10000);
705
- staleTimer = setInterval(() => {
706
- try { store.sweepStaleAgents(); } catch {}
707
- }, 120000);
708
- sweepTimer.unref();
709
- staleTimer.unref();
710
- },
711
-
712
- stopSweeper() {
713
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
714
- if (staleTimer) { clearInterval(staleTimer); staleTimer = null; }
715
- },
716
-
717
- getQueueDepths() {
718
- const counts = { urgent: 0, normal: 0, dlq: store.getAuditStats().dlq };
719
- for (const record of liveMessages.values()) {
720
- const pending = record.recipients.size > record.ackedBy.size;
721
- if (!pending) continue;
722
- if (record.message.priority >= 7) counts.urgent += 1;
723
- else counts.normal += 1;
724
- }
725
- return counts;
726
- },
727
-
728
- getDeliveryStats() {
729
- if (latencyIdx === 0) {
730
- return { total_deliveries: 0, avg_delivery_ms: 0 };
731
- }
732
- const filled = Math.min(latencyIdx, MAX_LATENCY_SAMPLES);
733
- const total = deliveryLatencies.slice(0, filled).reduce((sum, ms) => sum + ms, 0);
734
- return {
735
- total_deliveries: latencyIdx,
736
- avg_delivery_ms: Math.round(total / filled),
737
- };
738
- },
739
-
740
- getStatus(scope = 'hub', { agent_id, trace_id, include_metrics = true } = {}) {
741
- const data = {};
742
-
743
- if (scope === 'hub' || scope === 'queue') {
744
- data.hub = {
745
- state: 'healthy',
746
- uptime_ms: process.uptime() * 1000 | 0,
747
- realtime_transport: 'named-pipe',
748
- audit_store: store.type || 'sqlite',
749
- };
750
- if (include_metrics) {
751
- const depths = router.getQueueDepths();
752
- const stats = router.getDeliveryStats();
753
- const auditStats = store.getAuditStats();
754
- data.queues = {
755
- urgent_depth: depths.urgent,
756
- normal_depth: depths.normal,
757
- dlq_depth: depths.dlq,
758
- avg_delivery_ms: stats.avg_delivery_ms,
759
- };
760
- data.assigns = {
761
- queued: auditStats.assign_queued,
762
- running: auditStats.assign_running,
763
- failed: auditStats.assign_failed,
764
- timed_out: auditStats.assign_timed_out,
765
- };
766
- }
767
- }
768
-
769
- if (scope === 'agent' && agent_id) {
770
- const agent = store.getAgent(agent_id);
771
- if (agent) {
772
- data.agent = {
773
- agent_id: agent.agent_id,
774
- status: agent.status,
775
- pending: sortedPending(agent_id, { max_messages: 1000 }).length,
776
- last_seen_ms: agent.last_seen_ms,
777
- topics: listRuntimeTopics(agent_id),
778
- };
779
- }
780
- }
781
-
782
- if (scope === 'trace' && trace_id) {
783
- data.trace = store.getMessagesByTrace(trace_id);
784
- }
785
-
786
- return { ok: true, data };
787
- },
788
- };
789
-
790
- return router;
791
- }
1
+ // hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
2
+ // SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
3
+ import { EventEmitter, once } from "node:events";
4
+ import { uuidv7 } from "./lib/uuidv7.mjs";
5
+
6
+ const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
7
+
8
+ function uniqueStrings(values = []) {
9
+ return Array.from(
10
+ new Set(
11
+ (values || []).map((value) => String(value || "").trim()).filter(Boolean),
12
+ ),
13
+ );
14
+ }
15
+
16
+ function clampAssignDuration(
17
+ value,
18
+ fallback = 600000,
19
+ min = 1000,
20
+ max = 86400000,
21
+ ) {
22
+ const num = Number(value);
23
+ if (!Number.isFinite(num)) return fallback;
24
+ return Math.max(min, Math.min(Math.trunc(num), max));
25
+ }
26
+
27
+ function normalizeAssignTerminalStatus(input, metadata = {}) {
28
+ const status = String(input || "")
29
+ .trim()
30
+ .toLowerCase();
31
+ const resultTag = String(
32
+ metadata?.result ?? metadata?.status ?? metadata?.outcome ?? "",
33
+ )
34
+ .trim()
35
+ .toLowerCase();
36
+
37
+ if (status === "queued") return "queued";
38
+ if (status === "running" || status === "in_progress") return "running";
39
+ if (status === "timed_out" || status === "timeout") return "timed_out";
40
+ if (status === "failed" || status === "error") return "failed";
41
+ if (status === "succeeded" || status === "success") return "succeeded";
42
+
43
+ if (status === "completed") {
44
+ if (resultTag === "failed" || resultTag === "error") return "failed";
45
+ if (resultTag === "timed_out" || resultTag === "timeout")
46
+ return "timed_out";
47
+ return "succeeded";
48
+ }
49
+
50
+ if (resultTag === "failed" || resultTag === "error") return "failed";
51
+ if (resultTag === "timed_out" || resultTag === "timeout") return "timed_out";
52
+ if (resultTag === "succeeded" || resultTag === "success") return "succeeded";
53
+ return "succeeded";
54
+ }
55
+
56
+ function normalizeAgentTopics(store, agentId, runtimeTopics) {
57
+ const topics = new Set(runtimeTopics || []);
58
+ const persisted = store.getAgent(agentId)?.topics || [];
59
+ for (const topic of persisted) topics.add(topic);
60
+ return Array.from(topics);
61
+ }
62
+
63
+ /**
64
+ * 라우터 생성
65
+ * @param {object} store
66
+ */
67
+ export function createRouter(store) {
68
+ let sweepTimer = null;
69
+ let staleTimer = null;
70
+ const responseEmitter = new EventEmitter();
71
+ const deliveryEmitter = new EventEmitter();
72
+ responseEmitter.setMaxListeners(200);
73
+ deliveryEmitter.setMaxListeners(200);
74
+
75
+ const runtimeTopics = new Map();
76
+ const queuesByAgent = new Map();
77
+ const liveMessages = new Map();
78
+ const MAX_LATENCY_SAMPLES = 100;
79
+ let latencyIdx = 0;
80
+ const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
81
+
82
+ function ensureAgentQueue(agentId) {
83
+ let queue = queuesByAgent.get(agentId);
84
+ if (!queue) {
85
+ queue = new Map();
86
+ queuesByAgent.set(agentId, queue);
87
+ }
88
+ return queue;
89
+ }
90
+
91
+ function recordLatency(ms) {
92
+ deliveryLatencies[latencyIdx % MAX_LATENCY_SAMPLES] = ms;
93
+ latencyIdx++;
94
+ }
95
+
96
+ function upsertRuntimeTopics(agentId, topics, { replace = true } = {}) {
97
+ const normalized = uniqueStrings(topics);
98
+ const current = replace
99
+ ? new Set()
100
+ : new Set(runtimeTopics.get(agentId) || []);
101
+ for (const topic of normalized) current.add(topic);
102
+ runtimeTopics.set(agentId, current);
103
+ store.updateAgentTopics(agentId, Array.from(current));
104
+ return Array.from(current);
105
+ }
106
+
107
+ function listRuntimeTopics(agentId) {
108
+ return normalizeAgentTopics(store, agentId, runtimeTopics.get(agentId));
109
+ }
110
+
111
+ function trackMessage(message, recipients) {
112
+ liveMessages.set(message.id, {
113
+ message,
114
+ recipients: new Set(recipients),
115
+ ackedBy: new Set(),
116
+ });
117
+ }
118
+
119
+ function getMessageRecord(messageId) {
120
+ return liveMessages.get(messageId) || null;
121
+ }
122
+
123
+ function removeMessage(messageId) {
124
+ const record = liveMessages.get(messageId);
125
+ if (!record) return;
126
+ for (const agentId of record.recipients) {
127
+ queuesByAgent.get(agentId)?.delete(messageId);
128
+ }
129
+ liveMessages.delete(messageId);
130
+ }
131
+
132
+ function queueMessage(agentId, message) {
133
+ const queue = ensureAgentQueue(agentId);
134
+ queue.set(message.id, {
135
+ message,
136
+ attempts: 0,
137
+ delivered_at_ms: null,
138
+ acked_at_ms: null,
139
+ });
140
+ deliveryEmitter.emit("message", agentId, message);
141
+ }
142
+
143
+ function resolveRecipients(msg) {
144
+ const to = msg.to_agent ?? msg.to;
145
+ if (!to?.startsWith("topic:")) {
146
+ return [to];
147
+ }
148
+
149
+ const topic = to.slice(6);
150
+ const recipients = new Set();
151
+ for (const [agentId, topics] of runtimeTopics) {
152
+ if (topics.has(topic)) recipients.add(agentId);
153
+ }
154
+ for (const agent of store.getAgentsByTopic(topic)) {
155
+ recipients.add(agent.agent_id);
156
+ }
157
+ return Array.from(recipients);
158
+ }
159
+
160
+ function sortedPending(
161
+ agentId,
162
+ { max_messages = 20, include_topics = null } = {},
163
+ ) {
164
+ const queue = ensureAgentQueue(agentId);
165
+ const topicFilter = include_topics?.length ? new Set(include_topics) : null;
166
+ const now = Date.now();
167
+ const pending = [];
168
+
169
+ for (const delivery of queue.values()) {
170
+ const { message } = delivery;
171
+ if (delivery.acked_at_ms) continue;
172
+ if (message.expires_at_ms <= now) continue;
173
+ if (topicFilter && !topicFilter.has(message.topic)) continue;
174
+ pending.push(message);
175
+ }
176
+
177
+ pending.sort((a, b) => {
178
+ if (b.priority !== a.priority) return b.priority - a.priority;
179
+ return a.created_at_ms - b.created_at_ms;
180
+ });
181
+ return pending.slice(0, max_messages);
182
+ }
183
+
184
+ function markDelivered(agentId, messageId) {
185
+ const delivery = queuesByAgent.get(agentId)?.get(messageId);
186
+ const record = getMessageRecord(messageId);
187
+ if (!delivery || !record) return false;
188
+
189
+ delivery.attempts += 1;
190
+ if (!delivery.delivered_at_ms) {
191
+ delivery.delivered_at_ms = Date.now();
192
+ record.message.status = "delivered";
193
+ store.updateMessageStatus(messageId, "delivered");
194
+ recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
195
+ return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ function ackMessages(ids, agentId) {
201
+ const now = Date.now();
202
+ let count = 0;
203
+
204
+ for (const id of ids || []) {
205
+ const delivery = queuesByAgent.get(agentId)?.get(id);
206
+ const record = getMessageRecord(id);
207
+ if (!delivery || !record || delivery.acked_at_ms) continue;
208
+
209
+ delivery.acked_at_ms = now;
210
+ record.ackedBy.add(agentId);
211
+ count += 1;
212
+
213
+ if (record.ackedBy.size >= record.recipients.size) {
214
+ record.message.status = "acked";
215
+ store.updateMessageStatus(id, "acked");
216
+ removeMessage(id);
217
+ }
218
+ }
219
+
220
+ return count;
221
+ }
222
+
223
+ function dispatchMessage({
224
+ type,
225
+ from,
226
+ to,
227
+ topic,
228
+ priority = 5,
229
+ ttl_ms = 300000,
230
+ payload = {},
231
+ trace_id,
232
+ correlation_id,
233
+ }) {
234
+ const msg = store.auditLog({
235
+ type,
236
+ from,
237
+ to,
238
+ topic,
239
+ priority,
240
+ ttl_ms,
241
+ payload,
242
+ trace_id,
243
+ correlation_id,
244
+ });
245
+ const recipients = uniqueStrings(resolveRecipients(msg));
246
+ if (recipients.length) {
247
+ trackMessage(msg, recipients);
248
+ for (const agentId of recipients) {
249
+ queueMessage(agentId, msg);
250
+ }
251
+ msg.status = "delivered";
252
+ store.updateMessageStatus(msg.id, "delivered");
253
+ }
254
+ if (msg.type === "response") {
255
+ responseEmitter.emit(msg.correlation_id, msg.payload);
256
+ }
257
+ return { msg, recipients };
258
+ }
259
+
260
+ function buildAssignSnapshot(job, extra = {}) {
261
+ if (!job) return null;
262
+ return {
263
+ job_id: job.job_id,
264
+ supervisor_agent: job.supervisor_agent,
265
+ worker_agent: job.worker_agent,
266
+ topic: job.topic,
267
+ task: job.task,
268
+ status: job.status,
269
+ attempt: job.attempt,
270
+ retry_count: job.retry_count,
271
+ max_retries: job.max_retries,
272
+ timeout_ms: job.timeout_ms,
273
+ deadline_ms: job.deadline_ms,
274
+ trace_id: job.trace_id,
275
+ correlation_id: job.correlation_id,
276
+ last_message_id: job.last_message_id,
277
+ result: job.result,
278
+ error: job.error,
279
+ updated_at_ms: job.updated_at_ms,
280
+ completed_at_ms: job.completed_at_ms,
281
+ ...extra,
282
+ };
283
+ }
284
+
285
+ function notifyAssignSupervisor(job, event, extra = {}) {
286
+ if (!job?.supervisor_agent) return null;
287
+ const { msg } = dispatchMessage({
288
+ type: "event",
289
+ from: job.worker_agent || "assign-router",
290
+ to: job.supervisor_agent,
291
+ topic: "assign.result",
292
+ priority: Math.max(5, job.priority || 5),
293
+ ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
294
+ payload: {
295
+ event,
296
+ ...buildAssignSnapshot(job),
297
+ ...extra,
298
+ },
299
+ trace_id: job.trace_id,
300
+ correlation_id: job.correlation_id,
301
+ });
302
+ return msg;
303
+ }
304
+
305
+ function dispatchAssignJob(job, reason = "dispatch") {
306
+ const { msg, recipients } = dispatchMessage({
307
+ type: "handoff",
308
+ from: job.supervisor_agent,
309
+ to: job.worker_agent,
310
+ topic: job.topic || "assign.job",
311
+ priority: job.priority || 5,
312
+ ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
313
+ payload: {
314
+ kind: "assign.job",
315
+ reason,
316
+ assign_job_id: job.job_id,
317
+ attempt: job.attempt,
318
+ retry_count: job.retry_count,
319
+ max_retries: job.max_retries,
320
+ timeout_ms: job.timeout_ms,
321
+ supervisor_agent: job.supervisor_agent,
322
+ worker_agent: job.worker_agent,
323
+ task: job.task,
324
+ payload: job.payload || {},
325
+ },
326
+ trace_id: job.trace_id,
327
+ correlation_id: job.correlation_id,
328
+ });
329
+
330
+ const updated = store.updateAssignStatus(job.job_id, job.status, {
331
+ last_message_id: msg.id,
332
+ });
333
+ return { job: updated || job, recipients, message_id: msg.id };
334
+ }
335
+
336
+ function scheduleAssignRetry(
337
+ job,
338
+ reason,
339
+ error = null,
340
+ requested_by = "system",
341
+ ) {
342
+ if (!job) {
343
+ return {
344
+ ok: false,
345
+ error: { code: "ASSIGN_NOT_FOUND", message: "assign job not found" },
346
+ };
347
+ }
348
+ if (job.retry_count >= job.max_retries) {
349
+ return {
350
+ ok: false,
351
+ error: {
352
+ code: "ASSIGN_RETRY_EXHAUSTED",
353
+ message: `retry exhausted for ${job.job_id}`,
354
+ },
355
+ };
356
+ }
357
+
358
+ const queued = store.retryAssign(job.job_id, {
359
+ error,
360
+ timeout_ms: job.timeout_ms,
361
+ ttl_ms: job.ttl_ms,
362
+ });
363
+ const dispatched = dispatchAssignJob(queued, "retry");
364
+ notifyAssignSupervisor(dispatched.job, "retry_scheduled", {
365
+ retry_reason: reason,
366
+ requested_by,
367
+ });
368
+ return {
369
+ ok: true,
370
+ data: {
371
+ retried: true,
372
+ ...buildAssignSnapshot(dispatched.job, {
373
+ retry_reason: reason,
374
+ requested_by,
375
+ }),
376
+ },
377
+ };
378
+ }
379
+
380
+ function handleAssignTimeout(job) {
381
+ const timedOut = store.updateAssignStatus(job.job_id, "timed_out", {
382
+ error: job.error ?? { message: "assign job timed out" },
383
+ });
384
+
385
+ if (timedOut.retry_count < timedOut.max_retries) {
386
+ return scheduleAssignRetry(
387
+ timedOut,
388
+ "timed_out",
389
+ timedOut.error,
390
+ "sweeper",
391
+ );
392
+ }
393
+
394
+ notifyAssignSupervisor(timedOut, "completed", {
395
+ completion_reason: "timed_out",
396
+ });
397
+ return {
398
+ ok: true,
399
+ data: buildAssignSnapshot(timedOut, { completion_reason: "timed_out" }),
400
+ };
401
+ }
402
+
403
+ const router = {
404
+ responseEmitter,
405
+ deliveryEmitter,
406
+
407
+ registerAgent(args) {
408
+ const result = store.registerAgent(args);
409
+ upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
410
+ return result;
411
+ },
412
+
413
+ refreshAgentLease(agentId, ttlMs = 30000) {
414
+ return store.refreshLease(agentId, ttlMs);
415
+ },
416
+
417
+ subscribeAgent(agentId, topics, { replace = false } = {}) {
418
+ const nextTopics = upsertRuntimeTopics(agentId, topics, { replace });
419
+ return { agent_id: agentId, topics: nextTopics };
420
+ },
421
+
422
+ getSubscribedTopics(agentId) {
423
+ return listRuntimeTopics(agentId);
424
+ },
425
+
426
+ updateAgentStatus(agentId, status) {
427
+ if (status === "offline") {
428
+ runtimeTopics.delete(agentId);
429
+ }
430
+ return store.updateAgentStatus(agentId, status);
431
+ },
432
+
433
+ route(msg) {
434
+ const recipients = uniqueStrings(resolveRecipients(msg));
435
+ if (!recipients.length) return 0;
436
+ if (!getMessageRecord(msg.id)) {
437
+ trackMessage(msg, recipients);
438
+ }
439
+ for (const agentId of recipients) {
440
+ queueMessage(agentId, msg);
441
+ }
442
+ store.updateMessageStatus(msg.id, "delivered");
443
+ return recipients.length;
444
+ },
445
+
446
+ getPendingMessages(agentId, options = {}) {
447
+ return sortedPending(agentId, options);
448
+ },
449
+
450
+ countPendingMessages(agentId) {
451
+ const queue = ensureAgentQueue(agentId);
452
+ const now = Date.now();
453
+ let count = 0;
454
+ for (const delivery of queue.values()) {
455
+ if (delivery.acked_at_ms) continue;
456
+ if (delivery.message.expires_at_ms <= now) continue;
457
+ count++;
458
+ }
459
+ return count;
460
+ },
461
+
462
+ markMessagePushed(agentId, messageId) {
463
+ return markDelivered(agentId, messageId);
464
+ },
465
+
466
+ drainAgent(
467
+ agentId,
468
+ { max_messages = 20, include_topics = null, auto_ack = false } = {},
469
+ ) {
470
+ const messages = sortedPending(agentId, { max_messages, include_topics });
471
+ for (const message of messages) {
472
+ markDelivered(agentId, message.id);
473
+ }
474
+ if (auto_ack && messages.length) {
475
+ ackMessages(
476
+ messages.map((message) => message.id),
477
+ agentId,
478
+ );
479
+ }
480
+ return messages;
481
+ },
482
+
483
+ ackMessages(ids, agentId) {
484
+ return ackMessages(ids, agentId);
485
+ },
486
+
487
+ async handleAsk({
488
+ from,
489
+ to,
490
+ topic,
491
+ question,
492
+ context_refs,
493
+ payload = {},
494
+ priority = 5,
495
+ ttl_ms = 300000,
496
+ await_response_ms = 0,
497
+ trace_id,
498
+ correlation_id,
499
+ }) {
500
+ const cid = correlation_id || uuidv7();
501
+ const tid = trace_id || uuidv7();
502
+
503
+ const { msg } = dispatchMessage({
504
+ type: "request",
505
+ from,
506
+ to,
507
+ topic,
508
+ priority,
509
+ ttl_ms,
510
+ payload: { question, context_refs, ...payload },
511
+ correlation_id: cid,
512
+ trace_id: tid,
513
+ });
514
+
515
+ if (await_response_ms <= 0) {
516
+ return {
517
+ ok: true,
518
+ data: {
519
+ request_message_id: msg.id,
520
+ correlation_id: cid,
521
+ trace_id: tid,
522
+ state: "queued",
523
+ },
524
+ };
525
+ }
526
+
527
+ try {
528
+ const [response] = await once(responseEmitter, cid, {
529
+ signal: AbortSignal.timeout(Math.min(await_response_ms, 30000)),
530
+ });
531
+ return {
532
+ ok: true,
533
+ data: {
534
+ request_message_id: msg.id,
535
+ correlation_id: cid,
536
+ trace_id: tid,
537
+ state: "answered",
538
+ response,
539
+ },
540
+ };
541
+ } catch {
542
+ const resp = store.getResponseByCorrelation(cid);
543
+ if (resp) {
544
+ return {
545
+ ok: true,
546
+ data: {
547
+ request_message_id: msg.id,
548
+ correlation_id: cid,
549
+ trace_id: tid,
550
+ state: "answered",
551
+ response: resp.payload,
552
+ },
553
+ };
554
+ }
555
+ return {
556
+ ok: true,
557
+ data: {
558
+ request_message_id: msg.id,
559
+ correlation_id: cid,
560
+ trace_id: tid,
561
+ state: "delivered",
562
+ },
563
+ };
564
+ }
565
+ },
566
+
567
+ handlePublish({
568
+ from,
569
+ to,
570
+ topic,
571
+ priority = 5,
572
+ ttl_ms = 300000,
573
+ payload = {},
574
+ trace_id,
575
+ correlation_id,
576
+ message_type,
577
+ }) {
578
+ const type = message_type || (correlation_id ? "response" : "event");
579
+ const { msg, recipients } = dispatchMessage({
580
+ type,
581
+ from,
582
+ to,
583
+ topic,
584
+ priority,
585
+ ttl_ms,
586
+ payload,
587
+ trace_id: trace_id || uuidv7(),
588
+ correlation_id: correlation_id || uuidv7(),
589
+ });
590
+ return {
591
+ ok: true,
592
+ data: {
593
+ message_id: msg.id,
594
+ fanout_count: recipients.length,
595
+ expires_at_ms: msg.expires_at_ms,
596
+ },
597
+ };
598
+ },
599
+
600
+ handleHandoff({
601
+ from,
602
+ to,
603
+ topic,
604
+ task,
605
+ acceptance_criteria,
606
+ context_refs,
607
+ priority = 5,
608
+ ttl_ms = 600000,
609
+ trace_id,
610
+ correlation_id,
611
+ }) {
612
+ const { msg } = dispatchMessage({
613
+ type: "handoff",
614
+ from,
615
+ to,
616
+ topic,
617
+ priority,
618
+ ttl_ms,
619
+ payload: { task, acceptance_criteria, context_refs },
620
+ trace_id: trace_id || uuidv7(),
621
+ correlation_id: correlation_id || uuidv7(),
622
+ });
623
+ return {
624
+ ok: true,
625
+ data: { handoff_message_id: msg.id, state: "queued", assigned_to: to },
626
+ };
627
+ },
628
+
629
+ assignAsync({
630
+ supervisor_agent,
631
+ worker_agent,
632
+ topic = "assign.job",
633
+ task = "",
634
+ payload = {},
635
+ priority = 5,
636
+ ttl_ms = 600000,
637
+ timeout_ms = 600000,
638
+ max_retries = 0,
639
+ trace_id,
640
+ correlation_id,
641
+ }) {
642
+ const job = store.createAssign({
643
+ supervisor_agent,
644
+ worker_agent,
645
+ topic,
646
+ task,
647
+ payload,
648
+ priority,
649
+ ttl_ms,
650
+ timeout_ms,
651
+ max_retries,
652
+ trace_id,
653
+ correlation_id,
654
+ });
655
+ const dispatched = dispatchAssignJob(job, "create");
656
+ return {
657
+ ok: true,
658
+ data: {
659
+ assigned_to: worker_agent,
660
+ ...buildAssignSnapshot(dispatched.job),
661
+ },
662
+ };
663
+ },
664
+
665
+ reportAssignResult({
666
+ job_id,
667
+ worker_agent,
668
+ status,
669
+ attempt,
670
+ result,
671
+ error,
672
+ payload = {},
673
+ metadata = {},
674
+ }) {
675
+ const job = store.getAssign(job_id);
676
+ if (!job) {
677
+ return {
678
+ ok: false,
679
+ error: {
680
+ code: "ASSIGN_NOT_FOUND",
681
+ message: `assign job not found: ${job_id}`,
682
+ },
683
+ };
684
+ }
685
+ if (worker_agent && worker_agent !== job.worker_agent) {
686
+ return {
687
+ ok: false,
688
+ error: {
689
+ code: "ASSIGN_WORKER_MISMATCH",
690
+ message: `worker mismatch: ${worker_agent}`,
691
+ },
692
+ };
693
+ }
694
+ if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
695
+ return {
696
+ ok: false,
697
+ error: {
698
+ code: "ASSIGN_ATTEMPT_MISMATCH",
699
+ message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
700
+ },
701
+ };
702
+ }
703
+
704
+ const mergedMetadata = {
705
+ ...(payload?.metadata || {}),
706
+ ...(metadata || {}),
707
+ };
708
+ const normalizedStatus = normalizeAssignTerminalStatus(
709
+ status || payload?.status,
710
+ mergedMetadata,
711
+ );
712
+ const nextResult =
713
+ result ??
714
+ (Object.hasOwn(payload || {}, "result") ? payload.result : payload);
715
+ const nextError = error ?? payload?.error ?? null;
716
+
717
+ if (normalizedStatus === "running") {
718
+ const running = store.updateAssignStatus(job.job_id, "running", {
719
+ started_at_ms: job.started_at_ms || Date.now(),
720
+ deadline_ms:
721
+ Date.now() + clampAssignDuration(job.timeout_ms, job.timeout_ms),
722
+ result: nextResult,
723
+ error: nextError,
724
+ });
725
+ notifyAssignSupervisor(running, "progress");
726
+ return { ok: true, data: buildAssignSnapshot(running) };
727
+ }
728
+
729
+ const finalized = store.updateAssignStatus(job.job_id, normalizedStatus, {
730
+ result: nextResult,
731
+ error: nextError,
732
+ });
733
+
734
+ if (
735
+ (normalizedStatus === "failed" || normalizedStatus === "timed_out") &&
736
+ finalized.retry_count < finalized.max_retries
737
+ ) {
738
+ return scheduleAssignRetry(
739
+ finalized,
740
+ normalizedStatus,
741
+ nextError,
742
+ worker_agent || finalized.worker_agent,
743
+ );
744
+ }
745
+
746
+ notifyAssignSupervisor(finalized, "completed");
747
+ return { ok: true, data: buildAssignSnapshot(finalized) };
748
+ },
749
+
750
+ getAssignStatus({ job_id, ...filters } = {}) {
751
+ if (job_id) {
752
+ const job = store.getAssign(job_id);
753
+ return job
754
+ ? { ok: true, data: buildAssignSnapshot(job) }
755
+ : {
756
+ ok: false,
757
+ error: {
758
+ code: "ASSIGN_NOT_FOUND",
759
+ message: `assign job not found: ${job_id}`,
760
+ },
761
+ };
762
+ }
763
+ return {
764
+ ok: true,
765
+ data: {
766
+ assigns: store
767
+ .listAssigns(filters)
768
+ .map((job) => buildAssignSnapshot(job)),
769
+ },
770
+ };
771
+ },
772
+
773
+ retryAssign(job_id, { reason = "manual", requested_by = "manual" } = {}) {
774
+ const job = store.getAssign(job_id);
775
+ if (!job) {
776
+ return {
777
+ ok: false,
778
+ error: {
779
+ code: "ASSIGN_NOT_FOUND",
780
+ message: `assign job not found: ${job_id}`,
781
+ },
782
+ };
783
+ }
784
+ return scheduleAssignRetry(job, reason, job.error, requested_by);
785
+ },
786
+
787
+ sweepExpired() {
788
+ const now = Date.now();
789
+ let expired = 0;
790
+ for (const [messageId, record] of Array.from(liveMessages.entries())) {
791
+ if (record.message.expires_at_ms > now) continue;
792
+ store.moveToDeadLetter(messageId, "ttl_expired", null);
793
+ removeMessage(messageId);
794
+ expired += 1;
795
+ }
796
+ return { messages: expired };
797
+ },
798
+
799
+ sweepTimedOutAssigns() {
800
+ const expiredAssigns = store.listAssigns({
801
+ statuses: Array.from(ASSIGN_PENDING_STATUSES),
802
+ active_before_ms: Date.now(),
803
+ limit: 100,
804
+ });
805
+ let timed_out = 0;
806
+ let retried = 0;
807
+
808
+ for (const job of expiredAssigns) {
809
+ const result = handleAssignTimeout(job);
810
+ timed_out += 1;
811
+ if (result?.data?.retried) retried += 1;
812
+ }
813
+
814
+ return { timed_out, retried };
815
+ },
816
+
817
+ startSweeper() {
818
+ if (sweepTimer) return;
819
+ sweepTimer = setInterval(() => {
820
+ try {
821
+ router.sweepExpired();
822
+ router.sweepTimedOutAssigns();
823
+ } catch {}
824
+ }, 10000);
825
+ staleTimer = setInterval(() => {
826
+ try {
827
+ store.sweepStaleAgents();
828
+ } catch {}
829
+ }, 120000);
830
+ sweepTimer.unref();
831
+ staleTimer.unref();
832
+ },
833
+
834
+ stopSweeper() {
835
+ if (sweepTimer) {
836
+ clearInterval(sweepTimer);
837
+ sweepTimer = null;
838
+ }
839
+ if (staleTimer) {
840
+ clearInterval(staleTimer);
841
+ staleTimer = null;
842
+ }
843
+ },
844
+
845
+ getQueueDepths() {
846
+ const counts = { urgent: 0, normal: 0, dlq: store.getAuditStats().dlq };
847
+ for (const record of liveMessages.values()) {
848
+ const pending = record.recipients.size > record.ackedBy.size;
849
+ if (!pending) continue;
850
+ if (record.message.priority >= 7) counts.urgent += 1;
851
+ else counts.normal += 1;
852
+ }
853
+ return counts;
854
+ },
855
+
856
+ getDeliveryStats() {
857
+ if (latencyIdx === 0) {
858
+ return { total_deliveries: 0, avg_delivery_ms: 0 };
859
+ }
860
+ const filled = Math.min(latencyIdx, MAX_LATENCY_SAMPLES);
861
+ const total = deliveryLatencies
862
+ .slice(0, filled)
863
+ .reduce((sum, ms) => sum + ms, 0);
864
+ return {
865
+ total_deliveries: latencyIdx,
866
+ avg_delivery_ms: Math.round(total / filled),
867
+ };
868
+ },
869
+
870
+ getStatus(
871
+ scope = "hub",
872
+ { agent_id, trace_id, include_metrics = true } = {},
873
+ ) {
874
+ const data = {};
875
+
876
+ if (scope === "hub" || scope === "queue") {
877
+ data.hub = {
878
+ state: "healthy",
879
+ uptime_ms: (process.uptime() * 1000) | 0,
880
+ realtime_transport: "named-pipe",
881
+ audit_store: store.type || "sqlite",
882
+ };
883
+ if (include_metrics) {
884
+ const depths = router.getQueueDepths();
885
+ const stats = router.getDeliveryStats();
886
+ const auditStats = store.getAuditStats();
887
+ data.queues = {
888
+ urgent_depth: depths.urgent,
889
+ normal_depth: depths.normal,
890
+ dlq_depth: depths.dlq,
891
+ avg_delivery_ms: stats.avg_delivery_ms,
892
+ };
893
+ data.assigns = {
894
+ queued: auditStats.assign_queued,
895
+ running: auditStats.assign_running,
896
+ failed: auditStats.assign_failed,
897
+ timed_out: auditStats.assign_timed_out,
898
+ };
899
+ }
900
+ }
901
+
902
+ if (scope === "agent" && agent_id) {
903
+ const agent = store.getAgent(agent_id);
904
+ if (agent) {
905
+ data.agent = {
906
+ agent_id: agent.agent_id,
907
+ status: agent.status,
908
+ pending: sortedPending(agent_id, { max_messages: 1000 }).length,
909
+ last_seen_ms: agent.last_seen_ms,
910
+ topics: listRuntimeTopics(agent_id),
911
+ };
912
+ }
913
+ }
914
+
915
+ if (scope === "trace" && trace_id) {
916
+ data.trace = store.getMessagesByTrace(trace_id);
917
+ }
918
+
919
+ return { ok: true, data };
920
+ },
921
+ };
922
+
923
+ return router;
924
+ }