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.
- package/bin/tfx-live.mjs +3 -1
- package/config/routing-policy.json +1 -1
- package/hooks/pipeline-stop.mjs +1 -1
- package/hub/bridge.mjs +80 -1
- package/hub/cli-adapter-base.mjs +100 -19
- package/hub/codex-adapter.mjs +5 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +3 -1
- package/hub/intent.mjs +13 -9
- package/hub/lib/cto-env.mjs +46 -0
- package/hub/lib/memory-store.mjs +25 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/middleware/request-logger.mjs +4 -1
- package/hub/pipe.mjs +9 -0
- package/hub/role-contract.mjs +314 -0
- package/hub/role-control-events.mjs +113 -0
- package/hub/router.mjs +111 -29
- package/hub/schema.sql +1 -0
- package/hub/server.mjs +88 -43
- package/hub/store.mjs +35 -17
- package/hub/team/claude-daemon-control.mjs +28 -15
- package/hub/team/cli/commands/start/parse-args.mjs +2 -2
- package/hub/team/headless.mjs +295 -24
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/retry-state-machine.mjs +6 -2
- package/hub/team/swarm-hypervisor.mjs +20 -2
- package/hub/team/swarm-locks.mjs +17 -1
- package/hub/team/uds-orchestrator.mjs +62 -9
- package/hub/tools.mjs +8 -3
- package/hub/workers/codex-app-server-worker.mjs +54 -5
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hub/workers/delegator-mcp.mjs +18 -18
- package/hud/constants.mjs +21 -0
- package/hud/context-monitor.mjs +18 -21
- package/hud/hud-qos-status.mjs +1 -1
- package/hud/providers/codex-probe.mjs +216 -0
- package/hud/providers/codex.mjs +206 -29
- package/hud/renderers.mjs +6 -9
- package/package.json +1 -1
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +6 -6
- package/scripts/headless-guard.mjs +70 -23
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +22 -21
- package/scripts/lib/codex-profile-config.mjs +2 -2
- package/scripts/setup.mjs +15 -11
- package/scripts/tfx-gate-activate.mjs +1 -1
- package/scripts/tfx-route-worker.mjs +5 -0
- package/scripts/tfx-route.sh +136 -84
- package/skills/tfx-analysis/SKILL.md +3 -1
- package/skills/tfx-hub/SKILL.md +1 -1
- package/skills/tfx-plan/SKILL.md +3 -1
- package/skills/tfx-profile/SKILL.md +9 -9
- package/skills/tfx-prune/SKILL.md +4 -2
- package/skills/tfx-qa/SKILL.md +6 -2
- package/skills/tfx-research/SKILL.md +3 -1
- package/skills/tfx-review/SKILL.md +5 -3
- package/skills/tfx-setup/SKILL.md +1 -1
- package/tui/codex-profile.mjs +3 -1
- 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
|
-
|
|
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
|
package/hooks/pipeline-stop.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -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 —
|
|
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] —
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
524
|
+
await terminateProcess(child.pid);
|
|
470
525
|
};
|
|
471
526
|
|
|
472
|
-
const timeoutTimer =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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
|
|
491
|
-
stallTimer
|
|
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
|
-
|
|
500
|
-
|
|
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:
|
|
593
|
+
duration: now() - startedAt,
|
|
513
594
|
failureMode: ok ? null : failureMode || "crash",
|
|
514
595
|
});
|
|
515
596
|
}
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -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:
|
|
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
|
|
package/hub/gemini-adapter.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
// -
|
|
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
|
-
// -
|
|
69
|
-
implement: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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: "
|
|
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: "
|
|
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 },
|
package/hub/lib/cto-env.mjs
CHANGED
|
@@ -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
|
}
|
package/hub/lib/memory-store.mjs
CHANGED
|
@@ -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 =
|
|
468
|
-
timeout_ms =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
+
}
|