@triflux/remote 10.42.0 → 10.43.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/hub/pipe.mjs CHANGED
@@ -269,6 +269,9 @@ 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 { ok: false, error: { code: "AGENT_NOT_FOUND", message: `heartbeat target not registered: ${agentId}` } };
274
+ }
272
275
  return { ok: true, data: result };
273
276
  }
274
277
 
package/hub/schema.sql CHANGED
@@ -103,6 +103,7 @@ CREATE TABLE IF NOT EXISTS assign_jobs (
103
103
  created_at_ms INTEGER NOT NULL,
104
104
  updated_at_ms INTEGER NOT NULL,
105
105
  started_at_ms INTEGER,
106
+ dispatched_at_ms INTEGER,
106
107
  completed_at_ms INTEGER,
107
108
  last_retry_at_ms INTEGER
108
109
  );
package/hub/server.mjs CHANGED
@@ -32,6 +32,13 @@ import {
32
32
  import { broker as brokerInstance, reloadBroker } from "@triflux/core/hub/account-broker.mjs";
33
33
  import { createAdaptiveEngine } from "@triflux/core/hub/adaptive.mjs";
34
34
  import { createAssignCallbackServer } from "@triflux/core/hub/assign-callbacks.mjs";
35
+ import {
36
+ clampDurationMs,
37
+ DEFAULT_ASSIGN_TIMEOUT_MS,
38
+ DEFAULT_ASSIGN_TTL_MS,
39
+ DEFAULT_REGISTER_TIMEOUT_SEC,
40
+ resolveWorkerLeaseTtlMs,
41
+ } from "@triflux/core/hub/lib/timeout-defaults.mjs";
35
42
  import { DelegatorService } from "@triflux/core/hub/delegator/index.mjs";
36
43
  import { createHitlManager } from "@triflux/core/hub/hitl.mjs";
37
44
  import {
@@ -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,17 @@ 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 (portSpecified && !ephemeralPort && livePeer.alive && livePidPort && port !== livePidPort) {
976
984
  hubLog.warn(
977
985
  {
978
986
  requestedPort: port,
@@ -983,11 +991,8 @@ export async function startHub({
983
991
  );
984
992
  }
985
993
 
986
- const existingHub = await tryReuseExistingHub({
987
- port,
988
- portSpecified,
989
- host,
990
- detectPeer: () => livePeer,
994
+ const existingHub = ephemeralPort ? null : await tryReuseExistingHub({
995
+ port, portSpecified, host, detectPeer: () => livePeer,
991
996
  });
992
997
  if (existingHub) return existingHub;
993
998
 
@@ -1026,7 +1031,7 @@ export async function startHub({
1026
1031
  lockHeld = false;
1027
1032
  };
1028
1033
 
1029
- const lockedExistingHub = await tryReuseExistingHub({
1034
+ const lockedExistingHub = ephemeralPort ? null : await tryReuseExistingHub({
1030
1035
  port,
1031
1036
  portSpecified,
1032
1037
  host,
@@ -1038,9 +1043,9 @@ export async function startHub({
1038
1043
  }
1039
1044
 
1040
1045
  const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
1041
- if (HUB_TOKEN) {
1046
+ if (!ephemeralPort && HUB_TOKEN) {
1042
1047
  writeOwnedTokenFile({ token: HUB_TOKEN, port });
1043
- } else {
1048
+ } else if (!ephemeralPort) {
1044
1049
  cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
1045
1050
  }
1046
1051
 
@@ -1690,7 +1695,7 @@ export async function startHub({
1690
1695
  const {
1691
1696
  agent_id,
1692
1697
  cli,
1693
- timeout_sec = 600,
1698
+ timeout_sec = DEFAULT_REGISTER_TIMEOUT_SEC,
1694
1699
  topics = [],
1695
1700
  capabilities = [],
1696
1701
  metadata = {},
@@ -1709,7 +1714,10 @@ export async function startHub({
1709
1714
  metadata?.task ||
1710
1715
  cli;
1711
1716
  const workerStartedAt = performance.now();
1712
- const heartbeat_ttl_ms = (timeout_sec + 120) * 1000;
1717
+ const hasExplicitTimeout = Number.isFinite(Number(body.timeout_sec));
1718
+ const heartbeat_ttl_ms = hasExplicitTimeout
1719
+ ? (timeout_sec + 120) * 1000
1720
+ : Math.max((timeout_sec + 120) * 1000, resolveWorkerLeaseTtlMs());
1713
1721
  const result = await pipe.executeCommand("register", {
1714
1722
  agent_id,
1715
1723
  cli,
@@ -1725,7 +1733,7 @@ export async function startHub({
1725
1733
  });
1726
1734
  recordWorker(agent_id, "spawned", command, 0);
1727
1735
  }
1728
- return writeJson(res, 200, result);
1736
+ return writeJson(res, result.ok ? 200 : 409, result);
1729
1737
  }
1730
1738
 
1731
1739
  if (path === "/bridge/result" && req.method === "POST") {
@@ -1750,6 +1758,16 @@ export async function startHub({
1750
1758
  return writeJson(res, 200, result);
1751
1759
  }
1752
1760
 
1761
+ if (path === "/bridge/heartbeat" && req.method === "POST") {
1762
+ const { agent_id, ttl_ms } = body;
1763
+ if (!agent_id) return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
1764
+ const result = await pipe.executeCommand("heartbeat", {
1765
+ agent_id,
1766
+ heartbeat_ttl_ms: clampDurationMs(ttl_ms, resolveWorkerLeaseTtlMs()),
1767
+ });
1768
+ return writeJson(res, result.ok ? 200 : 404, result);
1769
+ }
1770
+
1753
1771
  if (path === "/bridge/control" && req.method === "POST") {
1754
1772
  const {
1755
1773
  from_agent = "lead",
@@ -1881,8 +1899,8 @@ export async function startHub({
1881
1899
  topic = "assign.job",
1882
1900
  payload = {},
1883
1901
  priority = 5,
1884
- ttl_ms = 600000,
1885
- timeout_ms = 600000,
1902
+ ttl_ms = DEFAULT_ASSIGN_TTL_MS,
1903
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
1886
1904
  max_retries = 0,
1887
1905
  trace_id,
1888
1906
  correlation_id,
@@ -2352,7 +2370,7 @@ export async function startHub({
2352
2370
  try {
2353
2371
  store.close();
2354
2372
  } catch {}
2355
- if (!preserveTokenFile) {
2373
+ if (!ephemeralPort && !preserveTokenFile) {
2356
2374
  cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2357
2375
  }
2358
2376
  releaseStartupLock();
@@ -2426,6 +2444,8 @@ export async function startHub({
2426
2444
  httpServer.listen(port, host, () => {
2427
2445
  httpServer.off("error", onError);
2428
2446
  try {
2447
+ const boundAddress = httpServer.address();
2448
+ if (boundAddress && typeof boundAddress === "object") port = boundAddress.port;
2429
2449
  let idleTimer = null;
2430
2450
  let stopPromise = null;
2431
2451
 
@@ -2446,7 +2466,7 @@ export async function startHub({
2446
2466
  idleTimeoutMs: hubIdleTimeoutMs,
2447
2467
  };
2448
2468
 
2449
- writeState({
2469
+ if (!ephemeralPort) writeState({
2450
2470
  pid: process.pid,
2451
2471
  port,
2452
2472
  host,
@@ -2464,7 +2484,7 @@ export async function startHub({
2464
2484
  session_id: sessionId,
2465
2485
  });
2466
2486
  releaseStartupLock();
2467
- void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2487
+ if (!ephemeralPort) void syncHubMcpSettingsIfAvailable({ hubUrl: info.url });
2468
2488
 
2469
2489
  hubLog.info(
2470
2490
  {
@@ -2551,8 +2571,10 @@ export async function startHub({
2551
2571
  synapseRegistry.destroy();
2552
2572
  } catch {}
2553
2573
  store.close();
2554
- cleanupOwnedPidFile();
2555
- cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2574
+ if (!ephemeralPort) {
2575
+ cleanupOwnedPidFile();
2576
+ cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
2577
+ }
2556
2578
  httpServer.closeAllConnections();
2557
2579
  await new Promise((resolveClose) =>
2558
2580
  httpServer.close(resolveClose),
package/hub/store.mjs CHANGED
@@ -5,6 +5,7 @@ import { mkdirSync, readFileSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { clampDurationMs, DEFAULT_ASSIGN_TIMEOUT_MS, DEFAULT_ASSIGN_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
8
9
  import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
9
10
  import { recalcConfidence } from "@triflux/core/hub/reflexion.mjs";
10
11
 
@@ -111,7 +112,7 @@ export function createStore(dbPath, options = {}) {
111
112
  db.exec(
112
113
  "CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
113
114
  );
114
- const SCHEMA_VERSION = "4";
115
+ const SCHEMA_VERSION = "5";
115
116
  const curVer = (() => {
116
117
  try {
117
118
  return db
@@ -143,6 +144,7 @@ export function createStore(dbPath, options = {}) {
143
144
  "type",
144
145
  "TEXT NOT NULL DEFAULT 'reflexion'",
145
146
  );
147
+ ensureColumn(db, "assign_jobs", "dispatched_at_ms", "INTEGER");
146
148
  ensureColumn(
147
149
  db,
148
150
  "reflexion_entries",
@@ -233,12 +235,12 @@ export function createStore(dbPath, options = {}) {
233
235
  job_id, supervisor_agent, worker_agent, topic, task, payload_json,
234
236
  status, attempt, retry_count, max_retries, priority, ttl_ms, timeout_ms, deadline_ms,
235
237
  trace_id, correlation_id, last_message_id, result_json, error_json,
236
- created_at_ms, updated_at_ms, started_at_ms, completed_at_ms, last_retry_at_ms
238
+ created_at_ms, updated_at_ms, started_at_ms, dispatched_at_ms, completed_at_ms, last_retry_at_ms
237
239
  ) VALUES (
238
240
  @job_id, @supervisor_agent, @worker_agent, @topic, @task, @payload_json,
239
241
  @status, @attempt, @retry_count, @max_retries, @priority, @ttl_ms, @timeout_ms, @deadline_ms,
240
242
  @trace_id, @correlation_id, @last_message_id, @result_json, @error_json,
241
- @created_at_ms, @updated_at_ms, @started_at_ms, @completed_at_ms, @last_retry_at_ms
243
+ @created_at_ms, @updated_at_ms, @started_at_ms, @dispatched_at_ms, @completed_at_ms, @last_retry_at_ms
242
244
  )`),
243
245
  getAssign: db.prepare("SELECT * FROM assign_jobs WHERE job_id = ?"),
244
246
  updateAssign: db.prepare(`
@@ -263,6 +265,7 @@ export function createStore(dbPath, options = {}) {
263
265
  error_json=@error_json,
264
266
  updated_at_ms=@updated_at_ms,
265
267
  started_at_ms=@started_at_ms,
268
+ dispatched_at_ms=@dispatched_at_ms,
266
269
  completed_at_ms=@completed_at_ms,
267
270
  last_retry_at_ms=@last_retry_at_ms
268
271
  WHERE job_id=@job_id`),
@@ -356,12 +359,6 @@ export function createStore(dbPath, options = {}) {
356
359
  return Math.max(1, Math.min(Math.trunc(num), 9));
357
360
  }
358
361
 
359
- function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
360
- const num = Number(value);
361
- if (!Number.isFinite(num)) return fallback;
362
- return Math.max(min, Math.min(Math.trunc(num), max));
363
- }
364
-
365
362
  const store = {
366
363
  db,
367
364
  uuidv7,
@@ -419,11 +416,12 @@ export function createStore(dbPath, options = {}) {
419
416
 
420
417
  refreshLease(agentId, ttlMs = 30000) {
421
418
  const now = Date.now();
422
- S.heartbeat.run(now, now + ttlMs, agentId);
419
+ const write = S.heartbeat.run(now, now + ttlMs, agentId);
423
420
  return {
424
421
  agent_id: agentId,
425
422
  lease_expires_ms: now + ttlMs,
426
423
  server_time_ms: now,
424
+ effective: { updated: write.changes > 0, store_type: "sqlite", db_path: dbPath ?? null },
427
425
  };
428
426
  },
429
427
 
@@ -629,9 +627,10 @@ export function createStore(dbPath, options = {}) {
629
627
  retry_count = 0,
630
628
  max_retries = 0,
631
629
  priority = 5,
632
- ttl_ms = 600000,
633
- timeout_ms = 600000,
630
+ ttl_ms = DEFAULT_ASSIGN_TTL_MS,
631
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
634
632
  deadline_ms,
633
+ dispatched_at_ms = null,
635
634
  trace_id,
636
635
  correlation_id,
637
636
  last_message_id = null,
@@ -639,7 +638,7 @@ export function createStore(dbPath, options = {}) {
639
638
  error = null,
640
639
  }) {
641
640
  const now = Date.now();
642
- const normalizedTimeout = clampDuration(timeout_ms, 600000);
641
+ const normalizedTimeout = clampDurationMs(timeout_ms, DEFAULT_ASSIGN_TIMEOUT_MS);
643
642
  const row = {
644
643
  job_id: job_id || uuidv7(),
645
644
  supervisor_agent,
@@ -652,7 +651,7 @@ export function createStore(dbPath, options = {}) {
652
651
  retry_count: Math.max(0, Number(retry_count) || 0),
653
652
  max_retries: Math.max(0, Number(max_retries) || 0),
654
653
  priority: clampPriority(priority, 5),
655
- ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
654
+ ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
656
655
  timeout_ms: normalizedTimeout,
657
656
  deadline_ms: Number.isFinite(Number(deadline_ms))
658
657
  ? Math.trunc(Number(deadline_ms))
@@ -665,6 +664,10 @@ export function createStore(dbPath, options = {}) {
665
664
  created_at_ms: now,
666
665
  updated_at_ms: now,
667
666
  started_at_ms: status === "running" ? now : null,
667
+ dispatched_at_ms:
668
+ dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
669
+ ? null
670
+ : Math.trunc(Number(dispatched_at_ms)),
668
671
  completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
669
672
  ? now
670
673
  : null,
@@ -689,7 +692,7 @@ export function createStore(dbPath, options = {}) {
689
692
  const isTerminal = ["succeeded", "failed", "timed_out"].includes(
690
693
  nextStatus,
691
694
  );
692
- const nextTimeout = clampDuration(
695
+ const nextTimeout = clampDurationMs(
693
696
  patch.timeout_ms ?? current.timeout_ms,
694
697
  current.timeout_ms,
695
698
  );
@@ -717,7 +720,7 @@ export function createStore(dbPath, options = {}) {
717
720
  patch.priority ?? current.priority,
718
721
  current.priority || 5,
719
722
  ),
720
- ttl_ms: clampDuration(
723
+ ttl_ms: clampDurationMs(
721
724
  patch.ttl_ms ?? current.ttl_ms,
722
725
  current.ttl_ms || nextTimeout,
723
726
  ),
@@ -758,6 +761,7 @@ export function createStore(dbPath, options = {}) {
758
761
  : nextStatus === "running"
759
762
  ? current.started_at_ms || now
760
763
  : current.started_at_ms,
764
+ dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms") ? patch.dispatched_at_ms : current.dispatched_at_ms,
761
765
  completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
762
766
  ? patch.completed_at_ms
763
767
  : isTerminal
@@ -849,7 +853,7 @@ export function createStore(dbPath, options = {}) {
849
853
  current.attempt + 1,
850
854
  Number(patch.attempt ?? current.attempt + 1) || 1,
851
855
  );
852
- const nextTimeout = clampDuration(
856
+ const nextTimeout = clampDurationMs(
853
857
  patch.timeout_ms ?? current.timeout_ms,
854
858
  current.timeout_ms,
855
859
  );
@@ -861,6 +865,7 @@ export function createStore(dbPath, options = {}) {
861
865
  deadline_ms: Date.now() + nextTimeout,
862
866
  completed_at_ms: null,
863
867
  started_at_ms: null,
868
+ dispatched_at_ms: null,
864
869
  last_retry_at_ms: Date.now(),
865
870
  result: patch.result ?? null,
866
871
  error: Object.hasOwn(patch, "error") ? patch.error : current.error,
@@ -464,25 +464,46 @@ export function sendClaudeControlRequest(
464
464
 
465
465
  // claude daemon 은 control.sock 의 mutating op(dispatch 등)에 control-key
466
466
  // 인증을 강제한다 (미제시 시 EAUTH "didn't present the daemon control key").
467
- // key 는 <configDir>/daemon/control.key (configDir 스코프별). 파일이 없으면
468
- // 인증 미강제 daemon 으로 보고 auth 필드를 생략한다 (fail-open — 구버전 호환).
467
+ // OMC runtime config 는 .omc-launch-profile.json 의 sourceConfigDir 에서 key 를
468
+ // 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
469
+ // 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
470
+ // 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
471
+ // (fail-open — 구버전 호환).
469
472
  export async function readDaemonControlKey(
470
473
  configDir = resolveClaudeConfigDir(),
474
+ { diagnostics } = {},
471
475
  ) {
472
- try {
473
- const key = await fs.readFile(
474
- path.join(configDir, "daemon", "control.key"),
475
- "utf8",
476
- );
477
- return key.trim() || undefined;
478
- } catch {
479
- return undefined;
476
+ if (!configDir) return undefined;
477
+
478
+ const runtimeConfigDir = path.resolve(configDir);
479
+ const omcProfile = await readOmcLaunchProfile(runtimeConfigDir);
480
+ const keyConfigDirs = [omcProfile?.sourceConfigDir, runtimeConfigDir].filter(
481
+ (candidate, index, entries) =>
482
+ candidate && entries.indexOf(candidate) === index,
483
+ );
484
+
485
+ for (const keyConfigDir of keyConfigDirs) {
486
+ const keyPath = path.join(keyConfigDir, "daemon", "control.key");
487
+ try {
488
+ const key = await fs.readFile(keyPath, "utf8");
489
+ if (key.trim()) return key.trim();
490
+ } catch (error) {
491
+ if (error?.code === "ENOENT") continue;
492
+ if (Array.isArray(diagnostics)) {
493
+ diagnostics.push({
494
+ code: error?.code || "UNKNOWN",
495
+ path: keyPath,
496
+ message: error?.message || String(error),
497
+ });
498
+ }
499
+ }
480
500
  }
501
+ return undefined;
481
502
  }
482
503
 
483
- export async function buildDaemonControlAuth(configDir) {
504
+ export async function buildDaemonControlAuth(configDir, opts = {}) {
484
505
  if (!configDir) return {};
485
- const auth = await readDaemonControlKey(configDir);
506
+ const auth = await readDaemonControlKey(configDir, opts);
486
507
  return auth ? { auth } : {};
487
508
  }
488
509
 
@@ -74,7 +74,7 @@ export function parseTeamArgs(args = []) {
74
74
  const assigns = []; // --assign "codex:프롬프트:역할" 형식
75
75
  let autoAttach = true;
76
76
  let progressive = true;
77
- let timeoutSec = 300;
77
+ let timeoutSec = 0;
78
78
  let verbose = false;
79
79
  let dashboard = true;
80
80
  let dashboardLayout = "lite";
@@ -130,7 +130,7 @@ export function parseTeamArgs(args = []) {
130
130
  } else if (current === "--no-progressive") {
131
131
  progressive = false;
132
132
  } else if (current === "--timeout" && args[index + 1]) {
133
- timeoutSec = Number(args[++index]) || 300;
133
+ timeoutSec = Number(args[++index]) || 0;
134
134
  } else if (current === "--mcp-profile" && args[index + 1]) {
135
135
  mcpProfile = args[++index].trim();
136
136
  } else if ((current === "--model" || current === "-m") && args[index + 1]) {