@triflux/remote 10.42.0 → 10.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cto/hygiene.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { basename, dirname, join } from "node:path";
14
- import { getCtoHygieneApplyMode } from "@triflux/core/hub/lib/cto-env.mjs";
14
+ import { getCtoHygieneApplyMode } from "../hub/lib/cto-env.mjs";
15
15
  import { appendCtoEvent } from "./events.mjs";
16
16
  import { applyHygieneArchiveActions } from "./hygiene-actions.mjs";
17
17
  import { resolveLakeRootDir } from "./lake-root.mjs";
@@ -0,0 +1 @@
1
+ export * from "@triflux/core/hub/lib/cto-env.mjs";
package/hub/pipe.mjs CHANGED
@@ -269,6 +269,15 @@ export function createPipeServer({
269
269
  payload.heartbeat_ttl_ms || heartbeatTtlMs,
270
270
  );
271
271
  if (client) touchClient(client);
272
+ if (result?.effective?.updated === false) {
273
+ return {
274
+ ok: false,
275
+ error: {
276
+ code: "AGENT_NOT_FOUND",
277
+ message: `heartbeat target not registered: ${agentId}`,
278
+ },
279
+ };
280
+ }
272
281
  return { ok: true, data: result };
273
282
  }
274
283
 
package/hub/schema.sql CHANGED
@@ -11,7 +11,11 @@ CREATE TABLE IF NOT EXISTS agents (
11
11
  last_seen_ms INTEGER NOT NULL,
12
12
  lease_expires_ms INTEGER NOT NULL,
13
13
  status TEXT NOT NULL CHECK (status IN ('online','stale','offline')),
14
- metadata_json TEXT NOT NULL DEFAULT '{}'
14
+ metadata_json TEXT NOT NULL DEFAULT '{}',
15
+ project_id TEXT,
16
+ session_id TEXT,
17
+ host_id TEXT,
18
+ transport_locators_json TEXT NOT NULL DEFAULT '[]'
15
19
  );
16
20
 
17
21
  -- 메시지 테이블
@@ -28,7 +32,61 @@ CREATE TABLE IF NOT EXISTS messages (
28
32
  correlation_id TEXT NOT NULL,
29
33
  trace_id TEXT NOT NULL,
30
34
  payload_json TEXT NOT NULL DEFAULT '{}',
31
- status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter'))
35
+ status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter')),
36
+ role_key_wire TEXT
37
+ );
38
+
39
+ -- 동적 역할 레지스트리 (schema v6)
40
+ CREATE TABLE IF NOT EXISTS role_registry (
41
+ role_key_wire TEXT PRIMARY KEY,
42
+ project_id TEXT NOT NULL,
43
+ role_kind TEXT NOT NULL CHECK (role_kind IN ('cto','lead')),
44
+ scope_id TEXT NOT NULL DEFAULT '',
45
+ state TEXT NOT NULL CHECK (
46
+ state IN (
47
+ 'electable',
48
+ 'elected-probing',
49
+ 'active',
50
+ 'unreachable',
51
+ 'quarantined'
52
+ )
53
+ ),
54
+ holder_agent_id TEXT,
55
+ previous_holder_agent_id TEXT,
56
+ epoch INTEGER NOT NULL DEFAULT 0 CHECK (epoch >= 0),
57
+ activation_seq INTEGER NOT NULL DEFAULT 0 CHECK (activation_seq >= 0),
58
+ activation_id TEXT,
59
+ activation_deadline_ms INTEGER,
60
+ holder_lease_expires_ms INTEGER,
61
+ charter_version TEXT,
62
+ charter_acked_epoch INTEGER,
63
+ retry_count INTEGER NOT NULL DEFAULT 0,
64
+ next_probe_ms INTEGER,
65
+ blocked_reason TEXT,
66
+ last_transition_ms INTEGER NOT NULL,
67
+ last_reason TEXT NOT NULL,
68
+ version INTEGER NOT NULL DEFAULT 0,
69
+ CHECK (
70
+ (role_kind = 'cto' AND scope_id = '') OR
71
+ (role_kind = 'lead' AND scope_id <> '')
72
+ ),
73
+ UNIQUE(project_id, role_kind, scope_id)
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS role_candidates (
77
+ role_key_wire TEXT NOT NULL,
78
+ agent_id TEXT NOT NULL,
79
+ source TEXT NOT NULL CHECK (source IN ('grant','standup','operator')),
80
+ priority INTEGER NOT NULL DEFAULT 0,
81
+ transport_locators_json TEXT NOT NULL DEFAULT '[]',
82
+ consecutive_probe_failures INTEGER NOT NULL DEFAULT 0,
83
+ excluded_until_ms INTEGER,
84
+ last_probe_ms INTEGER,
85
+ last_probe_code TEXT,
86
+ updated_at_ms INTEGER NOT NULL,
87
+ PRIMARY KEY(role_key_wire, agent_id),
88
+ FOREIGN KEY(role_key_wire)
89
+ REFERENCES role_registry(role_key_wire) ON DELETE CASCADE
32
90
  );
33
91
 
34
92
  -- 메시지 수신함 (배달 추적)
@@ -73,6 +131,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_correlation ON messages(correlation_id);
73
131
  CREATE INDEX IF NOT EXISTS idx_messages_trace ON messages(trace_id);
74
132
  CREATE INDEX IF NOT EXISTS idx_messages_expires ON messages(expires_at_ms);
75
133
  CREATE INDEX IF NOT EXISTS idx_messages_priority ON messages(priority DESC, created_at_ms ASC);
134
+ CREATE INDEX IF NOT EXISTS idx_messages_role_key ON messages(role_key_wire, status, expires_at_ms);
76
135
  CREATE INDEX IF NOT EXISTS idx_inbox_agent ON message_inbox(agent_id, delivered_at_ms);
77
136
  CREATE INDEX IF NOT EXISTS idx_inbox_message ON message_inbox(message_id);
78
137
  CREATE INDEX IF NOT EXISTS idx_human_requests_state ON human_requests(state);
@@ -103,6 +162,7 @@ CREATE TABLE IF NOT EXISTS assign_jobs (
103
162
  created_at_ms INTEGER NOT NULL,
104
163
  updated_at_ms INTEGER NOT NULL,
105
164
  started_at_ms INTEGER,
165
+ dispatched_at_ms INTEGER,
106
166
  completed_at_ms INTEGER,
107
167
  last_retry_at_ms INTEGER
108
168
  );
package/hub/server.mjs CHANGED
@@ -44,6 +44,13 @@ import {
44
44
  cleanupStaleFsmonitorDaemons,
45
45
  } from "@triflux/core/hub/lib/process-utils.mjs";
46
46
  import * as spawnTrace from "@triflux/core/hub/lib/spawn-trace.mjs";
47
+ import {
48
+ clampDurationMs,
49
+ DEFAULT_ASSIGN_TIMEOUT_MS,
50
+ DEFAULT_ASSIGN_TTL_MS,
51
+ DEFAULT_REGISTER_TIMEOUT_SEC,
52
+ resolveWorkerLeaseTtlMs,
53
+ } from "@triflux/core/hub/lib/timeout-defaults.mjs";
47
54
  import {
48
55
  recordRequest,
49
56
  recordWorker,
@@ -143,7 +150,7 @@ function buildHubUrl(host, port) {
143
150
 
144
151
  function parseHubPort(value) {
145
152
  const parsed = Number.parseInt(String(value ?? ""), 10);
146
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
153
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
147
154
  }
148
155
 
149
156
  function isPidAlive(pid, killFn = process.kill) {
@@ -963,16 +970,23 @@ export async function startHub({
963
970
  createDelegatorWorker = createDelegatorMcpWorker,
964
971
  } = {}) {
965
972
  const resolvedPort = parseHubPort(portOpt);
966
- const portSpecified = Number.isFinite(resolvedPort) && resolvedPort > 0;
973
+ const portSpecified = Number.isFinite(resolvedPort);
974
+ const ephemeralPort = resolvedPort === 0;
967
975
  const livePeer = detectLivePeer();
968
976
  const livePidPort = parseHubPort(livePeer?.port);
969
- const port = portSpecified
977
+ let port = portSpecified
970
978
  ? resolvedPort
971
979
  : resolveHubPort(process.env, {
972
980
  detectPeer: () => livePeer,
973
981
  });
974
982
 
975
- if (portSpecified && livePeer.alive && livePidPort && port !== livePidPort) {
983
+ if (
984
+ portSpecified &&
985
+ !ephemeralPort &&
986
+ livePeer.alive &&
987
+ livePidPort &&
988
+ port !== livePidPort
989
+ ) {
976
990
  hubLog.warn(
977
991
  {
978
992
  requestedPort: port,
@@ -983,12 +997,14 @@ export async function startHub({
983
997
  );
984
998
  }
985
999
 
986
- const existingHub = await tryReuseExistingHub({
987
- port,
988
- portSpecified,
989
- host,
990
- detectPeer: () => livePeer,
991
- });
1000
+ const existingHub = ephemeralPort
1001
+ ? null
1002
+ : await tryReuseExistingHub({
1003
+ port,
1004
+ portSpecified,
1005
+ host,
1006
+ detectPeer: () => livePeer,
1007
+ });
992
1008
  if (existingHub) return existingHub;
993
1009
 
994
1010
  attachBrokerDiagnostics(brokerInstance);
@@ -1026,21 +1042,23 @@ export async function startHub({
1026
1042
  lockHeld = false;
1027
1043
  };
1028
1044
 
1029
- const lockedExistingHub = await tryReuseExistingHub({
1030
- port,
1031
- portSpecified,
1032
- host,
1033
- detectPeer: () => detectLivePeer(),
1034
- });
1045
+ const lockedExistingHub = ephemeralPort
1046
+ ? null
1047
+ : await tryReuseExistingHub({
1048
+ port,
1049
+ portSpecified,
1050
+ host,
1051
+ detectPeer: () => detectLivePeer(),
1052
+ });
1035
1053
  if (lockedExistingHub) {
1036
1054
  releaseStartupLock();
1037
1055
  return lockedExistingHub;
1038
1056
  }
1039
1057
 
1040
1058
  const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
1041
- if (HUB_TOKEN) {
1059
+ if (!ephemeralPort && HUB_TOKEN) {
1042
1060
  writeOwnedTokenFile({ token: HUB_TOKEN, port });
1043
- } else {
1061
+ } else if (!ephemeralPort) {
1044
1062
  cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
1045
1063
  }
1046
1064
 
@@ -1540,6 +1558,38 @@ export async function startHub({
1540
1558
  try {
1541
1559
  const body = await parseBody(req);
1542
1560
  const { sessionId } = body || {};
1561
+ if (body?.agent_id && body?.cli) {
1562
+ const registered = router.registerAgent({
1563
+ agent_id: body.agent_id,
1564
+ cli: body.cli,
1565
+ capabilities: Array.isArray(body.capabilities)
1566
+ ? body.capabilities
1567
+ : [],
1568
+ topics: Array.isArray(body.topics) ? body.topics : [],
1569
+ heartbeat_ttl_ms: body.heartbeat_ttl_ms ?? 300000,
1570
+ metadata:
1571
+ body.metadata && typeof body.metadata === "object"
1572
+ ? body.metadata
1573
+ : { source: "session_start" },
1574
+ ...(Object.hasOwn(body, "project_id")
1575
+ ? { project_id: body.project_id }
1576
+ : {}),
1577
+ ...(Object.hasOwn(body, "session_id")
1578
+ ? { session_id: body.session_id }
1579
+ : {}),
1580
+ ...(Object.hasOwn(body, "host_id")
1581
+ ? { host_id: body.host_id }
1582
+ : {}),
1583
+ ...(Object.hasOwn(body, "transport_locators_json")
1584
+ ? { transport_locators_json: body.transport_locators_json }
1585
+ : {}),
1586
+ });
1587
+ if (!registered?.ok) {
1588
+ throw new Error(
1589
+ registered?.error?.message || "hub agent register failed",
1590
+ );
1591
+ }
1592
+ }
1543
1593
  const result = synapseRegistry.register(sessionId, body);
1544
1594
  if (!result?.ok) {
1545
1595
  throw new Error(result?.reason || "register failed");
@@ -1690,10 +1740,14 @@ export async function startHub({
1690
1740
  const {
1691
1741
  agent_id,
1692
1742
  cli,
1693
- timeout_sec = 600,
1743
+ timeout_sec = DEFAULT_REGISTER_TIMEOUT_SEC,
1694
1744
  topics = [],
1695
1745
  capabilities = [],
1696
1746
  metadata = {},
1747
+ project_id,
1748
+ session_id,
1749
+ host_id,
1750
+ transport_locators_json,
1697
1751
  } = body;
1698
1752
  if (!agent_id || !cli) {
1699
1753
  return writeJson(res, 400, {
@@ -1709,7 +1763,12 @@ export async function startHub({
1709
1763
  metadata?.task ||
1710
1764
  cli;
1711
1765
  const workerStartedAt = performance.now();
1712
- const heartbeat_ttl_ms = (timeout_sec + 120) * 1000;
1766
+ const hasExplicitTimeout = Number.isFinite(
1767
+ Number(body.timeout_sec),
1768
+ );
1769
+ const heartbeat_ttl_ms = hasExplicitTimeout
1770
+ ? (timeout_sec + 120) * 1000
1771
+ : Math.max((timeout_sec + 120) * 1000, resolveWorkerLeaseTtlMs());
1713
1772
  const result = await pipe.executeCommand("register", {
1714
1773
  agent_id,
1715
1774
  cli,
@@ -1717,6 +1776,12 @@ export async function startHub({
1717
1776
  topics,
1718
1777
  heartbeat_ttl_ms,
1719
1778
  metadata,
1779
+ ...(project_id === undefined ? {} : { project_id }),
1780
+ ...(session_id === undefined ? {} : { session_id }),
1781
+ ...(host_id === undefined ? {} : { host_id }),
1782
+ ...(transport_locators_json === undefined
1783
+ ? {}
1784
+ : { transport_locators_json }),
1720
1785
  });
1721
1786
  if (result.ok) {
1722
1787
  workerSpans.set(agent_id, {
@@ -1725,7 +1790,7 @@ export async function startHub({
1725
1790
  });
1726
1791
  recordWorker(agent_id, "spawned", command, 0);
1727
1792
  }
1728
- return writeJson(res, 200, result);
1793
+ return writeJson(res, result.ok ? 200 : 409, result);
1729
1794
  }
1730
1795
 
1731
1796
  if (path === "/bridge/result" && req.method === "POST") {
@@ -1750,6 +1815,20 @@ export async function startHub({
1750
1815
  return writeJson(res, 200, result);
1751
1816
  }
1752
1817
 
1818
+ if (path === "/bridge/heartbeat" && req.method === "POST") {
1819
+ const { agent_id, ttl_ms } = body;
1820
+ if (!agent_id)
1821
+ return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
1822
+ const result = await pipe.executeCommand("heartbeat", {
1823
+ agent_id,
1824
+ heartbeat_ttl_ms: clampDurationMs(
1825
+ ttl_ms,
1826
+ resolveWorkerLeaseTtlMs(),
1827
+ ),
1828
+ });
1829
+ return writeJson(res, result.ok ? 200 : 404, result);
1830
+ }
1831
+
1753
1832
  if (path === "/bridge/control" && req.method === "POST") {
1754
1833
  const {
1755
1834
  from_agent = "lead",
@@ -1881,8 +1960,8 @@ export async function startHub({
1881
1960
  topic = "assign.job",
1882
1961
  payload = {},
1883
1962
  priority = 5,
1884
- ttl_ms = 600000,
1885
- timeout_ms = 600000,
1963
+ ttl_ms = DEFAULT_ASSIGN_TTL_MS,
1964
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
1886
1965
  max_retries = 0,
1887
1966
  trace_id,
1888
1967
  correlation_id,
@@ -2352,7 +2431,7 @@ export async function startHub({
2352
2431
  try {
2353
2432
  store.close();
2354
2433
  } catch {}
2355
- if (!preserveTokenFile) {
2434
+ if (!ephemeralPort && !preserveTokenFile) {
2356
2435
  cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2357
2436
  }
2358
2437
  releaseStartupLock();
@@ -2426,6 +2505,9 @@ export async function startHub({
2426
2505
  httpServer.listen(port, host, () => {
2427
2506
  httpServer.off("error", onError);
2428
2507
  try {
2508
+ const boundAddress = httpServer.address();
2509
+ if (boundAddress && typeof boundAddress === "object")
2510
+ port = boundAddress.port;
2429
2511
  let idleTimer = null;
2430
2512
  let stopPromise = null;
2431
2513
 
@@ -2446,25 +2528,28 @@ export async function startHub({
2446
2528
  idleTimeoutMs: hubIdleTimeoutMs,
2447
2529
  };
2448
2530
 
2449
- writeState({
2450
- pid: process.pid,
2451
- port,
2452
- host,
2453
- auth_mode: HUB_TOKEN ? "token-required" : "localhost-only",
2454
- url: info.url,
2455
- pipe_path: pipe.path,
2456
- pipePath: pipe.path,
2457
- assign_callback_pipe_path: assignCallbacks.path,
2458
- assignCallbackPipePath: assignCallbacks.path,
2459
- authMode: HUB_TOKEN ? "token-required" : "localhost-only",
2460
- startedAt,
2461
- started: startedAtMs,
2462
- version,
2463
- sessionId,
2464
- session_id: sessionId,
2465
- });
2531
+ if (!ephemeralPort) {
2532
+ writeState({
2533
+ pid: process.pid,
2534
+ port,
2535
+ host,
2536
+ auth_mode: HUB_TOKEN ? "token-required" : "localhost-only",
2537
+ url: info.url,
2538
+ pipe_path: pipe.path,
2539
+ pipePath: pipe.path,
2540
+ assign_callback_pipe_path: assignCallbacks.path,
2541
+ assignCallbackPipePath: assignCallbacks.path,
2542
+ authMode: HUB_TOKEN ? "token-required" : "localhost-only",
2543
+ startedAt,
2544
+ started: startedAtMs,
2545
+ version,
2546
+ sessionId,
2547
+ session_id: sessionId,
2548
+ });
2549
+ }
2466
2550
  releaseStartupLock();
2467
- void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2551
+ if (!ephemeralPort)
2552
+ void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2468
2553
 
2469
2554
  hubLog.info(
2470
2555
  {
@@ -2551,8 +2636,10 @@ export async function startHub({
2551
2636
  synapseRegistry.destroy();
2552
2637
  } catch {}
2553
2638
  store.close();
2554
- cleanupOwnedPidFile();
2555
- cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2639
+ if (!ephemeralPort) {
2640
+ cleanupOwnedPidFile();
2641
+ cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2642
+ }
2556
2643
  httpServer.closeAllConnections();
2557
2644
  await new Promise((resolveClose) =>
2558
2645
  httpServer.close(resolveClose),