triflux 10.41.1 → 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 (73) hide show
  1. package/CLAUDE.md +233 -0
  2. package/bin/tfx-live.mjs +3 -1
  3. package/bin/triflux.mjs +48 -1
  4. package/config/routing-policy.json +1 -1
  5. package/cto/hygiene-actions.mjs +355 -0
  6. package/cto/hygiene.mjs +79 -9
  7. package/cto/index.mjs +14 -2
  8. package/cto/steward.mjs +362 -0
  9. package/hooks/keyword-rules.json +39 -5
  10. package/hooks/pipeline-stop.mjs +1 -1
  11. package/hub/bridge.mjs +80 -1
  12. package/hub/cli-adapter-base.mjs +100 -19
  13. package/hub/codex-adapter.mjs +5 -1
  14. package/hub/dynamic-routing-engine.mjs +1 -1
  15. package/hub/gemini-adapter.mjs +3 -1
  16. package/hub/intent.mjs +13 -9
  17. package/hub/lib/cto-env.mjs +94 -0
  18. package/hub/lib/memory-store.mjs +34 -13
  19. package/hub/lib/timeout-defaults.mjs +45 -0
  20. package/hub/lib/worker-lifecycle.mjs +101 -0
  21. package/hub/log-retention.mjs +726 -0
  22. package/hub/middleware/request-logger.mjs +4 -1
  23. package/hub/pipe.mjs +11 -1
  24. package/hub/public/tray.html +50 -53
  25. package/hub/role-contract.mjs +314 -0
  26. package/hub/role-control-events.mjs +113 -0
  27. package/hub/router.mjs +135 -30
  28. package/hub/schema.sql +1 -0
  29. package/hub/server.mjs +89 -44
  30. package/hub/store.mjs +50 -19
  31. package/hub/team/claude-daemon-control.mjs +28 -15
  32. package/hub/team/cli/commands/start/parse-args.mjs +2 -2
  33. package/hub/team/headless.mjs +295 -24
  34. package/hub/team/intervention.mjs +530 -0
  35. package/hub/team/retry-state-machine.mjs +6 -2
  36. package/hub/team/swarm-hypervisor.mjs +20 -2
  37. package/hub/team/swarm-locks.mjs +17 -1
  38. package/hub/team/uds-orchestrator.mjs +62 -9
  39. package/hub/tools.mjs +9 -7
  40. package/hub/workers/codex-app-server-worker.mjs +54 -5
  41. package/hub/workers/codex-mcp.mjs +1 -1
  42. package/hub/workers/delegator-mcp.mjs +18 -18
  43. package/hud/constants.mjs +21 -0
  44. package/hud/context-monitor.mjs +18 -21
  45. package/hud/hud-qos-status.mjs +1 -1
  46. package/hud/providers/codex-probe.mjs +216 -0
  47. package/hud/providers/codex.mjs +206 -29
  48. package/hud/renderers.mjs +6 -9
  49. package/package.json +1 -1
  50. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
  51. package/scripts/__tests__/tfx-route-node-entry.test.mjs +6 -6
  52. package/scripts/headless-guard.mjs +70 -23
  53. package/scripts/lib/cli-agy.mjs +1 -0
  54. package/scripts/lib/cli-claude.mjs +1 -0
  55. package/scripts/lib/cli-codex.mjs +22 -21
  56. package/scripts/lib/codex-profile-config.mjs +2 -2
  57. package/scripts/pack.mjs +1 -0
  58. package/scripts/setup.mjs +15 -11
  59. package/scripts/tfx-gate-activate.mjs +1 -1
  60. package/scripts/tfx-route-worker.mjs +5 -0
  61. package/scripts/tfx-route.sh +136 -84
  62. package/skills/tfx-analysis/SKILL.md +3 -1
  63. package/skills/tfx-harness/SKILL.md +89 -0
  64. package/skills/tfx-hub/SKILL.md +1 -1
  65. package/skills/tfx-plan/SKILL.md +3 -1
  66. package/skills/tfx-profile/SKILL.md +9 -9
  67. package/skills/tfx-prune/SKILL.md +4 -2
  68. package/skills/tfx-qa/SKILL.md +6 -2
  69. package/skills/tfx-research/SKILL.md +3 -1
  70. package/skills/tfx-review/SKILL.md +5 -3
  71. package/skills/tfx-setup/SKILL.md +1 -1
  72. package/tui/codex-profile.mjs +3 -1
  73. package/tui/setup.mjs +4 -4
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 },
@@ -0,0 +1,94 @@
1
+ const OFF_VALUES = new Set(["0", "false", "off", "no"]);
2
+ const ON_VALUES = new Set(["1", "true", "on", "yes"]);
3
+
4
+ function normalizeEnvValue(name) {
5
+ return String(process.env[name] ?? "")
6
+ .trim()
7
+ .toLowerCase();
8
+ }
9
+
10
+ function isExplicitlyOff(name) {
11
+ return OFF_VALUES.has(normalizeEnvValue(name));
12
+ }
13
+
14
+ function isExplicitlyOn(name) {
15
+ return ON_VALUES.has(normalizeEnvValue(name));
16
+ }
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
+
64
+ export function isCtoDisabled() {
65
+ return isExplicitlyOff("TFX_CTO");
66
+ }
67
+
68
+ export function isCtoManagerEnabled() {
69
+ if (isCtoDisabled()) return false;
70
+ return isExplicitlyOn("TFX_CTO_MANAGER");
71
+ }
72
+
73
+ export function getCtoHygieneApplyMode() {
74
+ if (isExplicitlyOff("TFX_CTO")) return "off";
75
+ return normalizeEnvValue("TFX_CTO_HYGIENE_APPLY") === "archive"
76
+ ? "archive"
77
+ : "off";
78
+ }
79
+
80
+ export function isCtoRetentionEnabled() {
81
+ if (isExplicitlyOff("TFX_CTO")) return false;
82
+ return isExplicitlyOn("TFX_CTO_RETENTION");
83
+ }
84
+
85
+ export function getCtoMode() {
86
+ return normalizeEnvValue("TFX_CTO_MODE") === "bridge" ? "bridge" : "bounded";
87
+ }
88
+
89
+ export function getCtoMaxTokens() {
90
+ const raw = normalizeEnvValue("TFX_CTO_MAX_TOKENS");
91
+ if (!raw) return 0;
92
+ const value = Number(raw);
93
+ return Number.isSafeInteger(value) && value > 0 ? value : 0;
94
+ }
@@ -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;
@@ -177,6 +175,15 @@ export function createMemoryStore() {
177
175
  lease_id: uuidv7(),
178
176
  lease_expires_ms: next.lease_expires_ms,
179
177
  server_time_ms: now,
178
+ effective: {
179
+ updated: true,
180
+ online: next.status === "online",
181
+ status: next.status,
182
+ last_seen_ms: next.last_seen_ms,
183
+ lease_expires_ms: next.lease_expires_ms,
184
+ store_type: "memory",
185
+ db_path: null,
186
+ },
180
187
  };
181
188
  },
182
189
 
@@ -191,6 +198,7 @@ export function createMemoryStore() {
191
198
  agent_id: agentId,
192
199
  lease_expires_ms: Date.now() + ttlMs,
193
200
  server_time_ms: Date.now(),
201
+ effective: { updated: false, store_type: "memory", db_path: null },
194
202
  };
195
203
  const now = Date.now();
196
204
  current.last_seen_ms = now;
@@ -200,6 +208,7 @@ export function createMemoryStore() {
200
208
  agent_id: agentId,
201
209
  lease_expires_ms: current.lease_expires_ms,
202
210
  server_time_ms: now,
211
+ effective: { updated: true, store_type: "memory", db_path: null },
203
212
  };
204
213
  },
205
214
 
@@ -455,9 +464,10 @@ export function createMemoryStore() {
455
464
  retry_count = 0,
456
465
  max_retries = 0,
457
466
  priority = 5,
458
- ttl_ms = 600000,
459
- timeout_ms = 600000,
467
+ ttl_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
468
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
460
469
  deadline_ms,
470
+ dispatched_at_ms = null,
461
471
  trace_id,
462
472
  correlation_id,
463
473
  last_message_id = null,
@@ -465,7 +475,10 @@ export function createMemoryStore() {
465
475
  error = null,
466
476
  }) {
467
477
  const now = Date.now();
468
- const normalizedTimeout = clampDuration(timeout_ms, 600000);
478
+ const normalizedTimeout = clampDurationMs(
479
+ timeout_ms,
480
+ DEFAULT_ASSIGN_TIMEOUT_MS,
481
+ );
469
482
  const row = {
470
483
  job_id: job_id || uuidv7(),
471
484
  supervisor_agent,
@@ -478,7 +491,7 @@ export function createMemoryStore() {
478
491
  retry_count: Math.max(0, Number(retry_count) || 0),
479
492
  max_retries: Math.max(0, Number(max_retries) || 0),
480
493
  priority: clampPriority(priority, 5),
481
- ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
494
+ ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
482
495
  timeout_ms: normalizedTimeout,
483
496
  deadline_ms: Number.isFinite(Number(deadline_ms))
484
497
  ? Math.trunc(Number(deadline_ms))
@@ -491,6 +504,10 @@ export function createMemoryStore() {
491
504
  created_at_ms: now,
492
505
  updated_at_ms: now,
493
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)),
494
511
  completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
495
512
  ? now
496
513
  : null,
@@ -515,7 +532,7 @@ export function createMemoryStore() {
515
532
  const isTerminal = ["succeeded", "failed", "timed_out"].includes(
516
533
  nextStatus,
517
534
  );
518
- const nextTimeout = clampDuration(
535
+ const nextTimeout = clampDurationMs(
519
536
  patch.timeout_ms ?? current.timeout_ms,
520
537
  current.timeout_ms,
521
538
  );
@@ -543,7 +560,7 @@ export function createMemoryStore() {
543
560
  patch.priority ?? current.priority,
544
561
  current.priority || 5,
545
562
  ),
546
- ttl_ms: clampDuration(
563
+ ttl_ms: clampDurationMs(
547
564
  patch.ttl_ms ?? current.ttl_ms,
548
565
  current.ttl_ms || nextTimeout,
549
566
  ),
@@ -580,6 +597,9 @@ export function createMemoryStore() {
580
597
  : nextStatus === "running"
581
598
  ? current.started_at_ms || now
582
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,
583
603
  completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
584
604
  ? patch.completed_at_ms
585
605
  : isTerminal
@@ -645,7 +665,7 @@ export function createMemoryStore() {
645
665
  current.attempt + 1,
646
666
  Number(patch.attempt ?? current.attempt + 1) || 1,
647
667
  );
648
- const nextTimeout = clampDuration(
668
+ const nextTimeout = clampDurationMs(
649
669
  patch.timeout_ms ?? current.timeout_ms,
650
670
  current.timeout_ms,
651
671
  );
@@ -657,6 +677,7 @@ export function createMemoryStore() {
657
677
  deadline_ms: Date.now() + nextTimeout,
658
678
  completed_at_ms: null,
659
679
  started_at_ms: null,
680
+ dispatched_at_ms: null,
660
681
  last_retry_at_ms: Date.now(),
661
682
  result: patch.result ?? null,
662
683
  error: Object.hasOwn(patch, "error") ? patch.error : current.error,