triflux 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.
Files changed (62) hide show
  1. package/bin/tfx-live.mjs +3 -1
  2. package/config/routing-policy.json +1 -1
  3. package/hooks/pipeline-stop.mjs +1 -1
  4. package/hub/bridge.mjs +80 -1
  5. package/hub/cli-adapter-base.mjs +100 -19
  6. package/hub/codex-adapter.mjs +5 -1
  7. package/hub/dynamic-routing-engine.mjs +1 -1
  8. package/hub/gemini-adapter.mjs +3 -1
  9. package/hub/intent.mjs +13 -9
  10. package/hub/lib/cto-env.mjs +46 -0
  11. package/hub/lib/memory-store.mjs +25 -13
  12. package/hub/lib/timeout-defaults.mjs +45 -0
  13. package/hub/lib/worker-lifecycle.mjs +101 -0
  14. package/hub/middleware/request-logger.mjs +4 -1
  15. package/hub/pipe.mjs +9 -0
  16. package/hub/role-contract.mjs +314 -0
  17. package/hub/role-control-events.mjs +113 -0
  18. package/hub/router.mjs +111 -29
  19. package/hub/schema.sql +1 -0
  20. package/hub/server.mjs +88 -43
  21. package/hub/store.mjs +35 -17
  22. package/hub/team/claude-daemon-control.mjs +28 -15
  23. package/hub/team/cli/commands/start/parse-args.mjs +2 -2
  24. package/hub/team/headless.mjs +295 -24
  25. package/hub/team/intervention.mjs +530 -0
  26. package/hub/team/retry-state-machine.mjs +6 -2
  27. package/hub/team/swarm-hypervisor.mjs +20 -2
  28. package/hub/team/swarm-locks.mjs +17 -1
  29. package/hub/team/uds-orchestrator.mjs +62 -9
  30. package/hub/tools.mjs +8 -3
  31. package/hub/workers/codex-app-server-worker.mjs +54 -5
  32. package/hub/workers/codex-mcp.mjs +1 -1
  33. package/hub/workers/delegator-mcp.mjs +18 -18
  34. package/hud/constants.mjs +21 -0
  35. package/hud/context-monitor.mjs +18 -21
  36. package/hud/hud-qos-status.mjs +1 -1
  37. package/hud/providers/codex-probe.mjs +216 -0
  38. package/hud/providers/codex.mjs +206 -29
  39. package/hud/renderers.mjs +6 -9
  40. package/package.json +1 -1
  41. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
  42. package/scripts/__tests__/tfx-route-node-entry.test.mjs +6 -6
  43. package/scripts/headless-guard.mjs +70 -23
  44. package/scripts/lib/cli-agy.mjs +1 -0
  45. package/scripts/lib/cli-claude.mjs +1 -0
  46. package/scripts/lib/cli-codex.mjs +22 -21
  47. package/scripts/lib/codex-profile-config.mjs +2 -2
  48. package/scripts/setup.mjs +15 -11
  49. package/scripts/tfx-gate-activate.mjs +1 -1
  50. package/scripts/tfx-route-worker.mjs +5 -0
  51. package/scripts/tfx-route.sh +136 -84
  52. package/skills/tfx-analysis/SKILL.md +3 -1
  53. package/skills/tfx-hub/SKILL.md +1 -1
  54. package/skills/tfx-plan/SKILL.md +3 -1
  55. package/skills/tfx-profile/SKILL.md +9 -9
  56. package/skills/tfx-prune/SKILL.md +4 -2
  57. package/skills/tfx-qa/SKILL.md +6 -2
  58. package/skills/tfx-research/SKILL.md +3 -1
  59. package/skills/tfx-review/SKILL.md +5 -3
  60. package/skills/tfx-setup/SKILL.md +1 -1
  61. package/tui/codex-profile.mjs +3 -1
  62. package/tui/setup.mjs +4 -4
package/bin/tfx-live.mjs CHANGED
@@ -23,7 +23,9 @@ const DEFAULT_POLL_INTERVAL_MS = 1500;
23
23
  const DEFAULT_READY_TIMEOUT_MS = 30_000;
24
24
  const DEFAULT_ANSWER_TIMEOUT_MS = 60_000;
25
25
  const FALLBACK_QUIET_POLLS = 3;
26
- const CAPTURE_START = "-200";
26
+ // Capture the complete pane history so a long TUI response does not lose its
27
+ // leading lines. runTmux's MAX_BUFFER remains the memory/output ceiling.
28
+ const CAPTURE_START = "-";
27
29
  const MAX_BUFFER = 10 * 1024 * 1024;
28
30
  // execFile timeout for a bridge verb = attach timeout + this buffer, so the
29
31
  // helper's own timeoutMs fires first and returns {timedOut:true} rather than
@@ -109,7 +109,7 @@
109
109
  "fallback": {
110
110
  "default_mode": "codex-default",
111
111
  "default_cli": "codex",
112
- "default_model": "gpt-5.5",
112
+ "default_model": "gpt-5.6-terra",
113
113
  "triggers": [
114
114
  "hud_null",
115
115
  "hud_stale_gte_15min",
@@ -152,7 +152,7 @@ function contextPercentsFromObject(value) {
152
152
  value.total_tokens ??
153
153
  value.totalTokens;
154
154
  // Fall back to a 1M context window when the payload omits one — Opus 4.x
155
- // [1M] and Codex gpt-5.5 both run on a 1M window, and assuming 200K there
155
+ // [1M] and Codex GPT-5.6 family run on a 1M window, and assuming 200K there
156
156
  // false-positives at ~15% real usage. Override via
157
157
  // TFX_CONTEXT_DEFAULT_MAX_TOKENS for narrower setups.
158
158
  const envFallback = Number(process.env.TFX_CONTEXT_DEFAULT_MAX_TOKENS);
package/hub/bridge.mjs CHANGED
@@ -99,6 +99,11 @@ const HUB_OPERATIONS = Object.freeze({
99
99
  action: "register",
100
100
  httpPath: "/bridge/register",
101
101
  },
102
+ heartbeat: {
103
+ transport: "command",
104
+ action: "heartbeat",
105
+ httpPath: "/bridge/heartbeat",
106
+ },
102
107
  result: {
103
108
  transport: "command",
104
109
  action: "result",
@@ -637,6 +642,14 @@ async function cmdRegister(args) {
637
642
  return emitJson(result || unavailableResult());
638
643
  }
639
644
 
645
+ async function cmdHeartbeat(args) {
646
+ const outcome = await requestHub(HUB_OPERATIONS.heartbeat, {
647
+ agent_id: args.agent,
648
+ ttl_ms: args["ttl-ms"] != null ? Number(args["ttl-ms"]) : undefined,
649
+ });
650
+ return emitJson(outcome?.result || unavailableResult());
651
+ }
652
+
640
653
  async function cmdResult(args) {
641
654
  let output = "";
642
655
  if (args.file && existsSync(args.file)) {
@@ -1559,6 +1572,68 @@ async function cmdRetryStatus(args) {
1559
1572
  return true;
1560
1573
  }
1561
1574
 
1575
+ // intervene-run은 hub transport를 거치지 않는다. heartbeat가 hub 장애 중에도
1576
+ // process/daemon/pane 사다리를 실행할 수 있도록 순수 로컬 모듈만 호출한다.
1577
+ async function cmdInterveneRun(args) {
1578
+ try {
1579
+ const payload = readBridgePayload(args);
1580
+ const intervention = await import("./team/intervention.mjs");
1581
+ let rolloutFile = payload.rolloutFile || null;
1582
+ if (!rolloutFile && payload.cli === "codex" && payload.pid) {
1583
+ rolloutFile = await intervention.resolveCodexRolloutFile({
1584
+ pid: numericOption(payload.pid, undefined),
1585
+ codexHome: payload.codexHome,
1586
+ });
1587
+ }
1588
+ const seconds = (value) => {
1589
+ const parsed = numericOption(value, undefined);
1590
+ return parsed > 0 ? parsed * 1_000 : undefined;
1591
+ };
1592
+ const readActivitySignature = intervention.createFileActivitySource({
1593
+ files: [payload.stdoutLog, payload.stderrLog, payload.resultFile].filter(
1594
+ Boolean,
1595
+ ),
1596
+ rolloutFile,
1597
+ });
1598
+ const ladder = intervention.createInterventionLadder({
1599
+ target: {
1600
+ channel: payload.channel,
1601
+ pid: numericOption(payload.pid, undefined),
1602
+ paneId: payload.paneId,
1603
+ interactive: payload.interactive === true,
1604
+ cli: payload.cli,
1605
+ sessionId: payload.sessionId,
1606
+ codexHome: payload.codexHome,
1607
+ rolloutFile,
1608
+ daemon: payload.daemon,
1609
+ },
1610
+ reinstructPrompt: payload.reinstructPrompt || undefined,
1611
+ readActivitySignature,
1612
+ config: {
1613
+ ...(seconds(payload.reinstructWaitSec)
1614
+ ? { reinstructWaitMs: seconds(payload.reinstructWaitSec) }
1615
+ : {}),
1616
+ ...(seconds(payload.resumeWaitSec)
1617
+ ? { resumeWaitMs: seconds(payload.resumeWaitSec) }
1618
+ : {}),
1619
+ ...(seconds(payload.sigtermGraceSec)
1620
+ ? { sigtermGraceMs: seconds(payload.sigtermGraceSec) }
1621
+ : {}),
1622
+ },
1623
+ log: (event) =>
1624
+ process.stderr.write(`[tfx-intervene] ${JSON.stringify(event)}\n`),
1625
+ });
1626
+ return emitJson(await ladder.intervene());
1627
+ } catch (error) {
1628
+ return emitJson({
1629
+ ok: false,
1630
+ outcome: "failed",
1631
+ step: null,
1632
+ error: error?.message || String(error),
1633
+ });
1634
+ }
1635
+ }
1636
+
1562
1637
  export async function main(argv = process.argv.slice(2)) {
1563
1638
  const cmd = argv[0];
1564
1639
  const args = parseArgs(argv.slice(1));
@@ -1566,6 +1641,8 @@ export async function main(argv = process.argv.slice(2)) {
1566
1641
  switch (cmd) {
1567
1642
  case "register":
1568
1643
  return await cmdRegister(args);
1644
+ case "heartbeat":
1645
+ return await cmdHeartbeat(args);
1569
1646
  case "result":
1570
1647
  return await cmdResult(args);
1571
1648
  case "control":
@@ -1630,9 +1707,11 @@ export async function main(argv = process.argv.slice(2)) {
1630
1707
  return await cmdRetryRun(args);
1631
1708
  case "retry-status":
1632
1709
  return await cmdRetryStatus(args);
1710
+ case "intervene-run":
1711
+ return await cmdInterveneRun(args);
1633
1712
  default:
1634
1713
  console.error(
1635
- "사용법: bridge.mjs <register|result|control|handoff|publish|takeover-role|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
1714
+ "사용법: bridge.mjs <register|heartbeat|result|control|handoff|publish|takeover-role|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status|intervene-run> [--옵션]",
1636
1715
  );
1637
1716
  process.exit(1);
1638
1717
  }
@@ -6,7 +6,14 @@ import { existsSync, readFileSync, statSync } from "node:fs";
6
6
 
7
7
  import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
8
8
  import { writePromptToTmpFile } from "./lib/prompt-tmp.mjs";
9
+ import {
10
+ createActivityLifecycle,
11
+ isActivityLifecycleEnabled,
12
+ resolveHardCeilingMs,
13
+ resolveStallInterventionMs,
14
+ } from "./lib/worker-lifecycle.mjs";
9
15
  import { IS_WINDOWS, killProcess } from "./platform.mjs";
16
+ import { createInterventionLadder } from "./team/intervention.mjs";
10
17
 
11
18
  // ── Quota retry-after 파싱 ──────────────────────────────────────
12
19
 
@@ -397,19 +404,36 @@ export async function terminateChild(pid, opts = {}) {
397
404
  *
398
405
  * @param {string} command — shell command to run
399
406
  * @param {string} workdir — cwd for the child process
400
- * @param {number} timeout — max duration in ms
407
+ * @param {number} timeout — legacy wall-clock duration (TFX_ACTIVITY_LIFECYCLE=0 only)
401
408
  * @param {object} [opts]
402
409
  * @param {string} [opts.resultFile] — file to read output from (if CLI writes there)
403
410
  * @param {function} [opts.inferStallMode] — (stdout, stderr) => string. Default: () => 'timeout'
404
411
  * @param {number} [opts.stallCheckIntervalMs] — stall check interval (default 10_000)
405
- * @param {number} [opts.stallThresholdMs] — stall threshold (default 30_000)
412
+ * @param {number} [opts.stallThresholdMs] — inactivity threshold
413
+ * @param {number} [opts.hardCeilingMs] — activity-independent maximum duration
414
+ * @param {(context: object) => Promise<boolean|string>|boolean|string} [opts.onStallIntervene]
415
+ * T5 intervention seam; true or "resolved" keeps the process alive
416
+ * @param {object} [opts.deps] — clock/process/timer overrides for deterministic tests
406
417
  * @returns {Promise<object>} createResult-shaped object
407
418
  */
408
419
  export async function runProcess(command, workdir, timeout, opts = {}) {
409
- const startedAt = Date.now();
420
+ const deps = opts.deps || {};
421
+ const now = deps.now || Date.now;
422
+ const spawnProcess = deps.spawn || spawn;
423
+ const scheduleTimeout = deps.setTimeout || setTimeout;
424
+ const cancelTimeout = deps.clearTimeout || clearTimeout;
425
+ const scheduleInterval = deps.setInterval || setInterval;
426
+ const cancelInterval = deps.clearInterval || clearInterval;
427
+ const terminateProcess = deps.terminateChild || terminateChild;
428
+ const startedAt = now();
410
429
  const inferStallMode = opts.inferStallMode || (() => "timeout");
411
430
  const stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 10_000;
412
- const stallThresholdMs = opts.stallThresholdMs ?? 30_000;
431
+ const legacyLifecycle = !isActivityLifecycleEnabled();
432
+ const stallThresholdMs =
433
+ opts.stallThresholdMs ??
434
+ (legacyLifecycle ? 30_000 : resolveStallInterventionMs());
435
+ const hardCeilingMs = opts.hardCeilingMs ?? resolveHardCeilingMs();
436
+ const earlyClassifyMs = opts.earlyClassifyMs ?? 30_000;
413
437
  const resultFile = opts.resultFile || null;
414
438
 
415
439
  let stdout = "";
@@ -421,7 +445,7 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
421
445
  try {
422
446
  // PRD A1: opts.spawnEnv 가 있으면 그 env 로 spawn (lease 의 authFile/env 적용).
423
447
  // undefined 면 spawn 의 default 동작 (부모 process env inherit) 유지.
424
- child = spawn(command, {
448
+ child = spawnProcess(command, {
425
449
  cwd: workdir,
426
450
  shell: true,
427
451
  windowsHide: true,
@@ -430,7 +454,7 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
430
454
  } catch (error) {
431
455
  return createResult(false, {
432
456
  stderr: String(error?.message || error),
433
- duration: Date.now() - startedAt,
457
+ duration: now() - startedAt,
434
458
  });
435
459
  }
436
460
 
@@ -446,9 +470,40 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
446
470
 
447
471
  let lastBytes = 0;
448
472
  let lastResultFileSignature = resultFileSignature();
449
- let lastChange = Date.now();
473
+ let lastChange = now();
474
+ const activitySignature = () =>
475
+ `${Buffer.byteLength(stdout) + Buffer.byteLength(stderr)}:${resultFileSignature()}`;
476
+ let ladder = null;
477
+ const lifecycle = createActivityLifecycle({
478
+ enabled: !legacyLifecycle,
479
+ interventionMs: stallThresholdMs,
480
+ hardCeilingMs,
481
+ now,
482
+ onIntervene: async (context) => {
483
+ if (typeof opts.onStallIntervene === "function") {
484
+ const result = await opts.onStallIntervene(context);
485
+ return result === true || result === "resolved";
486
+ }
487
+ ladder ??= createInterventionLadder({
488
+ target: {
489
+ pid: child.pid,
490
+ cli: opts.cli,
491
+ codexHome: opts.codexHome,
492
+ rolloutFile: opts.rolloutFile,
493
+ },
494
+ readActivitySignature: activitySignature,
495
+ deps:
496
+ typeof opts.resumeHandler === "function"
497
+ ? { resumeHandler: opts.resumeHandler }
498
+ : {},
499
+ });
500
+ const result = await ladder.intervene();
501
+ return result?.outcome === "reactivated" || result?.outcome === "resumed";
502
+ },
503
+ });
450
504
  const touch = () => {
451
- lastChange = Date.now();
505
+ lastChange = now();
506
+ lifecycle.observe(activitySignature());
452
507
  };
453
508
  child.stdout?.on("data", (chunk) => {
454
509
  stdout += String(chunk);
@@ -466,13 +521,15 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
466
521
  const stopFor = async (mode) => {
467
522
  if (failureMode) return;
468
523
  failureMode = mode;
469
- await terminateChild(child.pid);
524
+ await terminateProcess(child.pid);
470
525
  };
471
526
 
472
- const timeoutTimer = setTimeout(() => {
473
- void stopFor("timeout");
474
- }, timeout);
475
- const stallTimer = setInterval(() => {
527
+ const timeoutTimer = legacyLifecycle
528
+ ? scheduleTimeout(() => {
529
+ void stopFor("timeout");
530
+ }, timeout)
531
+ : null;
532
+ const stallTimer = scheduleInterval(() => {
476
533
  const size = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
477
534
  const currentResultFileSignature = resultFileSignature();
478
535
  if (
@@ -482,13 +539,37 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
482
539
  lastBytes = size;
483
540
  lastResultFileSignature = currentResultFileSignature;
484
541
  touch();
542
+ }
543
+ if (legacyLifecycle) {
544
+ if (now() - lastChange >= stallThresholdMs)
545
+ void stopFor(inferStallMode(stdout, stderr));
485
546
  return;
486
547
  }
487
- if (Date.now() - lastChange >= stallThresholdMs)
548
+ const quietMs = now() - lastChange;
549
+ const mode = inferStallMode(stdout, stderr);
550
+ if (
551
+ quietMs >= earlyClassifyMs &&
552
+ (mode === "rate_limited" || mode === "auth_stall")
553
+ ) {
488
554
  void stopFor(inferStallMode(stdout, stderr));
555
+ return;
556
+ }
557
+ void lifecycle
558
+ .check({
559
+ pid: child.pid,
560
+ command,
561
+ workdir,
562
+ mode,
563
+ stdoutTail: stdout.slice(-2000),
564
+ stderrTail: stderr.slice(-2000),
565
+ })
566
+ .then((reason) => {
567
+ if (!reason) return;
568
+ void stopFor(reason === "hard_ceiling" ? "timeout" : mode);
569
+ });
489
570
  }, stallCheckIntervalMs);
490
- timeoutTimer.unref?.();
491
- stallTimer.unref?.();
571
+ timeoutTimer?.unref?.();
572
+ stallTimer?.unref?.();
492
573
 
493
574
  await new Promise((resolve) =>
494
575
  child.on("close", (code) => {
@@ -496,8 +577,8 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
496
577
  resolve();
497
578
  }),
498
579
  );
499
- clearTimeout(timeoutTimer);
500
- clearInterval(stallTimer);
580
+ if (timeoutTimer) cancelTimeout(timeoutTimer);
581
+ cancelInterval(stallTimer);
501
582
 
502
583
  const fileOutput =
503
584
  resultFile && existsSync(resultFile)
@@ -509,7 +590,7 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
509
590
  output,
510
591
  stderr,
511
592
  exitCode,
512
- duration: Date.now() - startedAt,
593
+ duration: now() - startedAt,
513
594
  failureMode: ok ? null : failureMode || "crash",
514
595
  });
515
596
  }
@@ -11,6 +11,7 @@ import {
11
11
  shellQuote,
12
12
  } from "./cli-adapter-base.mjs";
13
13
  import { runPreflight } from "./codex-preflight.mjs";
14
+ import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
14
15
 
15
16
  // ── Codex-specific stall inference ──────────────────────────────
16
17
 
@@ -64,9 +65,10 @@ function buildAttempts(opts, preflight) {
64
65
  forceBypass: preflight.needsBypass,
65
66
  };
66
67
  if (opts.retryOnFail === false) return [base];
68
+ const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
67
69
  return [
68
70
  base,
69
- { ...base, timeout: timeout * 2, excluded: requested, forceBypass: true },
71
+ { ...base, timeout: retryTimeout, excluded: requested, forceBypass: true },
70
72
  ];
71
73
  }
72
74
 
@@ -183,6 +185,8 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
183
185
  resultFile,
184
186
  inferStallMode,
185
187
  spawnEnv,
188
+ cli: "codex",
189
+ codexHome: spawnEnv?.CODEX_HOME,
186
190
  });
187
191
  }
188
192
 
@@ -45,7 +45,7 @@ function makeFallback(policy, trigger, inputsHash, now) {
45
45
  {
46
46
  cli: fb.default_cli ?? "codex",
47
47
  accountId: null,
48
- model: fb.default_model ?? "gpt-5.5",
48
+ model: fb.default_model ?? "gpt-5.6-terra",
49
49
  riskTier: "safe",
50
50
  reason: trigger,
51
51
  },
@@ -9,6 +9,7 @@ import {
9
9
  normalizePathForShell,
10
10
  runProcess,
11
11
  } from "./cli-adapter-base.mjs";
12
+ import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
12
13
  import { whichCommandAsync } from "./platform.mjs";
13
14
 
14
15
  function shellSingleQuote(value) {
@@ -88,7 +89,8 @@ function buildAttempts(opts, preflight) {
88
89
  excludeMcpServers: [...(preflight.excludeMcpServers || [])],
89
90
  };
90
91
  if (opts.retryOnFail === false) return [base];
91
- return [base, { ...base, timeout: timeout * 2, allowedMcpServers: [] }];
92
+ const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
93
+ return [base, { ...base, timeout: retryTimeout, allowedMcpServers: [] }];
92
94
  }
93
95
 
94
96
  // ── Public: buildExecArgs ───────────────────────────────────────
package/hub/intent.mjs CHANGED
@@ -62,21 +62,25 @@ function _tryCodexClassify(prompt) {
62
62
  */
63
63
  export const INTENT_CATEGORIES = {
64
64
  // 모델 정책 (2026-04-25):
65
- // - gpt-5.5 = 메인 (코드 포함 모든 메인 직무, fast tier 가능)
65
+ // - GPT-5.6 family = 기존 semantic tier를 Sol/Terra/Luna lane으로 매핑
66
66
  // - gpt-5.4-mini = 자잘/부가/가성비 (fast tier 가능)
67
67
  // - gpt-5.3-codex = escalation 가성비 중간 (fast 미지원)
68
- // - gpt-5.4 폐기, gpt-5.5격상
69
- implement: { agent: "executor", mcp: "implement", effort: "gpt55_high" },
70
- debug: { agent: "debugger", mcp: "implement", effort: "gpt55_xhigh" },
71
- analyze: { agent: "analyst", mcp: "analyze", effort: "gpt55_xhigh" },
72
- design: { agent: "architect", mcp: "analyze", effort: "gpt55_xhigh" },
73
- review: { agent: "code-reviewer", mcp: "review", effort: "gpt55_high" },
68
+ // - 기존 effort는 유지하고 모델만 GPT-5.6 family교체
69
+ implement: {
70
+ agent: "executor",
71
+ mcp: "implement",
72
+ effort: "gpt56_terra_high",
73
+ },
74
+ debug: { agent: "debugger", mcp: "implement", effort: "gpt56_sol_xhigh" },
75
+ analyze: { agent: "analyst", mcp: "analyze", effort: "gpt56_sol_xhigh" },
76
+ design: { agent: "architect", mcp: "analyze", effort: "gpt56_sol_xhigh" },
77
+ review: { agent: "code-reviewer", mcp: "review", effort: "gpt56_terra_high" },
74
78
  document: { agent: "writer", mcp: "docs", effort: "pro" },
75
- research: { agent: "scientist", mcp: "analyze", effort: "gpt55_high" },
79
+ research: { agent: "scientist", mcp: "analyze", effort: "gpt56_terra_high" },
76
80
  "quick-fix": {
77
81
  agent: "build-fixer",
78
82
  mcp: "implement",
79
- effort: "gpt55_low",
83
+ effort: "gpt56_luna_low",
80
84
  },
81
85
  explain: { agent: "writer", mcp: "docs", effort: "flash" },
82
86
  test: { agent: "test-engineer", mcp: null, effort: null },
@@ -15,6 +15,52 @@ function isExplicitlyOn(name) {
15
15
  return ON_VALUES.has(normalizeEnvValue(name));
16
16
  }
17
17
 
18
+ function normalizeEnv(env, name) {
19
+ return String(env?.[name] ?? "")
20
+ .trim()
21
+ .toLowerCase();
22
+ }
23
+
24
+ function isEnvOff(env, name) {
25
+ return OFF_VALUES.has(normalizeEnv(env, name));
26
+ }
27
+
28
+ function isEnvOn(env, name) {
29
+ return ON_VALUES.has(normalizeEnv(env, name));
30
+ }
31
+
32
+ /**
33
+ * Resolve the C6a control plane without mutating environment or runtime
34
+ * state. Callers own storing and incrementing the returned generation.
35
+ */
36
+ export function resolveRoleControlSnapshot(env = process.env, options = {}) {
37
+ const masterEnabled = !isEnvOff(env, "TFX_CTO");
38
+ const ctoManagerEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_MANAGER");
39
+ const leadManagerEnabled =
40
+ ctoManagerEnabled && isEnvOn(env, "TFX_LEAD_MANAGER");
41
+ const northStarEnabled =
42
+ masterEnabled && !isEnvOff(env, "TFX_CTO_NORTH_STAR");
43
+ const autoCollectEnabled =
44
+ masterEnabled && !isEnvOff(env, "TFX_CTO_AUTO_COLLECT");
45
+ const values = {
46
+ master_enabled: masterEnabled,
47
+ cto_manager_enabled: ctoManagerEnabled,
48
+ lead_manager_enabled: leadManagerEnabled,
49
+ north_star_enabled: northStarEnabled,
50
+ auto_collect_enabled: autoCollectEnabled,
51
+ };
52
+ const previous = options.previous;
53
+ const changed = Object.keys(values).some(
54
+ (field) => previous?.[field] !== values[field],
55
+ );
56
+ const generation = previous
57
+ ? Number(previous.generation || 0) + (changed ? 1 : 0)
58
+ : Number.isSafeInteger(options.generation) && options.generation >= 0
59
+ ? options.generation
60
+ : 0;
61
+ return { generation, ...values };
62
+ }
63
+
18
64
  export function isCtoDisabled() {
19
65
  return isExplicitlyOff("TFX_CTO");
20
66
  }
@@ -1,6 +1,10 @@
1
1
  // hub/lib/memory-store.mjs — In-memory store (SQLite-free fallback for @triflux/core)
2
2
 
3
3
  import { recalcConfidence } from "../reflexion.mjs";
4
+ import {
5
+ clampDurationMs,
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ } from "./timeout-defaults.mjs";
4
8
  import { uuidv7 } from "./uuidv7.mjs";
5
9
 
6
10
  export function clone(value) {
@@ -19,12 +23,6 @@ function clampPriority(value, fallback = 5) {
19
23
  return Math.max(1, Math.min(Math.trunc(num), 9));
20
24
  }
21
25
 
22
- function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
23
- const num = Number(value);
24
- if (!Number.isFinite(num)) return fallback;
25
- return Math.max(min, Math.min(Math.trunc(num), max));
26
- }
27
-
28
26
  export function clampConfidence(value, fallback = 0.5) {
29
27
  const num = Number(value);
30
28
  if (!Number.isFinite(num)) return fallback;
@@ -200,6 +198,7 @@ export function createMemoryStore() {
200
198
  agent_id: agentId,
201
199
  lease_expires_ms: Date.now() + ttlMs,
202
200
  server_time_ms: Date.now(),
201
+ effective: { updated: false, store_type: "memory", db_path: null },
203
202
  };
204
203
  const now = Date.now();
205
204
  current.last_seen_ms = now;
@@ -209,6 +208,7 @@ export function createMemoryStore() {
209
208
  agent_id: agentId,
210
209
  lease_expires_ms: current.lease_expires_ms,
211
210
  server_time_ms: now,
211
+ effective: { updated: true, store_type: "memory", db_path: null },
212
212
  };
213
213
  },
214
214
 
@@ -464,9 +464,10 @@ export function createMemoryStore() {
464
464
  retry_count = 0,
465
465
  max_retries = 0,
466
466
  priority = 5,
467
- ttl_ms = 600000,
468
- timeout_ms = 600000,
467
+ ttl_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
468
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
469
469
  deadline_ms,
470
+ dispatched_at_ms = null,
470
471
  trace_id,
471
472
  correlation_id,
472
473
  last_message_id = null,
@@ -474,7 +475,10 @@ export function createMemoryStore() {
474
475
  error = null,
475
476
  }) {
476
477
  const now = Date.now();
477
- const normalizedTimeout = clampDuration(timeout_ms, 600000);
478
+ const normalizedTimeout = clampDurationMs(
479
+ timeout_ms,
480
+ DEFAULT_ASSIGN_TIMEOUT_MS,
481
+ );
478
482
  const row = {
479
483
  job_id: job_id || uuidv7(),
480
484
  supervisor_agent,
@@ -487,7 +491,7 @@ export function createMemoryStore() {
487
491
  retry_count: Math.max(0, Number(retry_count) || 0),
488
492
  max_retries: Math.max(0, Number(max_retries) || 0),
489
493
  priority: clampPriority(priority, 5),
490
- ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
494
+ ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
491
495
  timeout_ms: normalizedTimeout,
492
496
  deadline_ms: Number.isFinite(Number(deadline_ms))
493
497
  ? Math.trunc(Number(deadline_ms))
@@ -500,6 +504,10 @@ export function createMemoryStore() {
500
504
  created_at_ms: now,
501
505
  updated_at_ms: now,
502
506
  started_at_ms: status === "running" ? now : null,
507
+ dispatched_at_ms:
508
+ dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
509
+ ? null
510
+ : Math.trunc(Number(dispatched_at_ms)),
503
511
  completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
504
512
  ? now
505
513
  : null,
@@ -524,7 +532,7 @@ export function createMemoryStore() {
524
532
  const isTerminal = ["succeeded", "failed", "timed_out"].includes(
525
533
  nextStatus,
526
534
  );
527
- const nextTimeout = clampDuration(
535
+ const nextTimeout = clampDurationMs(
528
536
  patch.timeout_ms ?? current.timeout_ms,
529
537
  current.timeout_ms,
530
538
  );
@@ -552,7 +560,7 @@ export function createMemoryStore() {
552
560
  patch.priority ?? current.priority,
553
561
  current.priority || 5,
554
562
  ),
555
- ttl_ms: clampDuration(
563
+ ttl_ms: clampDurationMs(
556
564
  patch.ttl_ms ?? current.ttl_ms,
557
565
  current.ttl_ms || nextTimeout,
558
566
  ),
@@ -589,6 +597,9 @@ export function createMemoryStore() {
589
597
  : nextStatus === "running"
590
598
  ? current.started_at_ms || now
591
599
  : current.started_at_ms,
600
+ dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
601
+ ? patch.dispatched_at_ms
602
+ : current.dispatched_at_ms,
592
603
  completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
593
604
  ? patch.completed_at_ms
594
605
  : isTerminal
@@ -654,7 +665,7 @@ export function createMemoryStore() {
654
665
  current.attempt + 1,
655
666
  Number(patch.attempt ?? current.attempt + 1) || 1,
656
667
  );
657
- const nextTimeout = clampDuration(
668
+ const nextTimeout = clampDurationMs(
658
669
  patch.timeout_ms ?? current.timeout_ms,
659
670
  current.timeout_ms,
660
671
  );
@@ -666,6 +677,7 @@ export function createMemoryStore() {
666
677
  deadline_ms: Date.now() + nextTimeout,
667
678
  completed_at_ms: null,
668
679
  started_at_ms: null,
680
+ dispatched_at_ms: null,
669
681
  last_retry_at_ms: Date.now(),
670
682
  result: patch.result ?? null,
671
683
  error: Object.hasOwn(patch, "error") ? patch.error : current.error,
@@ -0,0 +1,45 @@
1
+ // hub 조율 평면 타임아웃/TTL의 단일 기준값. 환경은 호출 시점에 읽는다.
2
+
3
+ export const MIN_DURATION_MS = 1_000;
4
+ export const MAX_DURATION_MS = 86_400_000;
5
+ export const DEFAULT_ASSIGN_TIMEOUT_MS = 600_000;
6
+ export const DEFAULT_ASSIGN_TTL_MS = 600_000;
7
+ export const DEFAULT_HANDOFF_TTL_MS = 600_000;
8
+ export const DEFAULT_SWARM_LOCK_TTL_MS = 600_000;
9
+ export const DEFAULT_REGISTER_TIMEOUT_SEC = 600;
10
+ export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
11
+ export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
12
+ export const DEFAULT_HARD_CEILING_SEC = 21_600;
13
+
14
+ function readEnvSeconds(env, name, fallbackSec) {
15
+ const value = Number(env?.[name]);
16
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallbackSec;
17
+ }
18
+
19
+ export function resolveStallInterventionMs(env = process.env) {
20
+ return (
21
+ readEnvSeconds(env, "TFX_STALL_THRESHOLD", DEFAULT_STALL_INTERVENTION_SEC) *
22
+ 1000
23
+ );
24
+ }
25
+
26
+ export function resolveHardCeilingMs(env = process.env) {
27
+ return (
28
+ readEnvSeconds(env, "TFX_HARD_CEILING_SEC", DEFAULT_HARD_CEILING_SEC) * 1000
29
+ );
30
+ }
31
+
32
+ export function resolveWorkerLeaseTtlMs(env = process.env) {
33
+ return resolveStallInterventionMs(env) + REGISTER_HEARTBEAT_GRACE_MS;
34
+ }
35
+
36
+ export function clampDurationMs(
37
+ value,
38
+ fallback = DEFAULT_ASSIGN_TIMEOUT_MS,
39
+ min = MIN_DURATION_MS,
40
+ max = MAX_DURATION_MS,
41
+ ) {
42
+ const numeric = Number(value);
43
+ if (!Number.isFinite(numeric)) return fallback;
44
+ return Math.max(min, Math.min(Math.trunc(numeric), max));
45
+ }