triflux 10.13.0 → 10.13.1
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/bridge.mjs
CHANGED
|
@@ -23,7 +23,6 @@ import { parseArgs as nodeParseArgs } from "node:util";
|
|
|
23
23
|
import { getPipelineStateDbPath } from "./pipeline/state.mjs";
|
|
24
24
|
import {
|
|
25
25
|
createRetryStateMachine,
|
|
26
|
-
DEFAULT_ESCALATION_CHAIN,
|
|
27
26
|
loadSnapshot,
|
|
28
27
|
saveSnapshot,
|
|
29
28
|
} from "./team/retry-state-machine.mjs";
|
|
@@ -45,7 +45,7 @@ export async function startHeadlessTeam({
|
|
|
45
45
|
ok(`headless ${assignments.length}워커 시작`);
|
|
46
46
|
|
|
47
47
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
48
|
-
timeoutSec: timeoutSec ||
|
|
48
|
+
timeoutSec: timeoutSec || 900,
|
|
49
49
|
layout,
|
|
50
50
|
autoAttach: !!autoAttach,
|
|
51
51
|
dashboard: !!dashboard,
|
package/hub/team/headless.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
mkdirSync,
|
|
12
12
|
readFileSync,
|
|
13
13
|
renameSync,
|
|
14
|
+
rmSync,
|
|
14
15
|
statSync,
|
|
15
16
|
writeFileSync,
|
|
16
17
|
} from "node:fs";
|
|
@@ -240,16 +241,53 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
240
241
|
return `cd '${safeCwd.replace(/'/g, "'\\''")}' && ${backendCommand}`;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/**
|
|
245
|
+
* 이전 run 의 stale .txt / .partial / .err 를 제거한다.
|
|
246
|
+
* Issue #118 Codex review R1 HIGH: resultFile 경로 재사용 시 (워커 restart 또는
|
|
247
|
+
* 동일 세션명 재실행) 이전 run 의 HANDOFF 가 새 run 결과로 오인될 수 있다.
|
|
248
|
+
*
|
|
249
|
+
* Issue #118 R2 MEDIUM: Windows 에서 locked/open 파일로 인한 삭제 실패를
|
|
250
|
+
* silent swallow 하면 stale artifact 가 살아남아 bug 가 non-deterministic 하게
|
|
251
|
+
* 재발한다. ENOENT 외 에러는 경고 로그 + 재시도(1회) 후 계속.
|
|
252
|
+
* @param {string} resultFile
|
|
253
|
+
*/
|
|
254
|
+
export function cleanStaleResultArtifacts(resultFile) {
|
|
255
|
+
for (const suffix of ["", ".partial", ".err"]) {
|
|
256
|
+
const path = `${resultFile}${suffix}`;
|
|
257
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
rmSync(path);
|
|
260
|
+
break;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err?.code === "ENOENT") break; // 이미 없음 — 정상
|
|
263
|
+
if (attempt === 0) continue; // 1회 재시도
|
|
264
|
+
// 재시도도 실패: locked 파일일 수 있음. 경고 로그 후 계속 진행
|
|
265
|
+
// (dispatch 중단 시 워커 자체가 시작 못하므로 best-effort 유지)
|
|
266
|
+
console.warn(
|
|
267
|
+
`[headless] cleanStaleResultArtifacts: ${path} 삭제 실패 (${err?.code || "unknown"}). stale leak 가능.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
243
274
|
/**
|
|
244
275
|
* 결과 파일 읽기 (없으면 capture-pane fallback)
|
|
276
|
+
* Issue #118 fallback chain: .txt → .partial ([partial] prefix) → .err → capture-pane
|
|
245
277
|
* @param {string} resultFile
|
|
246
278
|
* @param {string} paneId
|
|
247
279
|
* @returns {string}
|
|
248
280
|
*/
|
|
249
|
-
function readResult(resultFile, paneId) {
|
|
281
|
+
export function readResult(resultFile, paneId) {
|
|
250
282
|
if (existsSync(resultFile)) {
|
|
251
283
|
return readFileSync(resultFile, "utf8").trim();
|
|
252
284
|
}
|
|
285
|
+
// Issue #118 fallback 0: timeout 직전 persist 된 부분 출력 (.partial)
|
|
286
|
+
const partialFile = `${resultFile}.partial`;
|
|
287
|
+
if (existsSync(partialFile)) {
|
|
288
|
+
const partial = readFileSync(partialFile, "utf8").trim();
|
|
289
|
+
if (partial) return `[partial] ${partial}`;
|
|
290
|
+
}
|
|
253
291
|
// fallback 1: stderr 파일 (codex 실패 시 원인 추적)
|
|
254
292
|
const errFile = `${resultFile}.err`;
|
|
255
293
|
if (existsSync(errFile)) {
|
|
@@ -417,6 +455,17 @@ export async function waitForCompletionWithStallDetect(
|
|
|
417
455
|
|
|
418
456
|
// 전체 타임아웃
|
|
419
457
|
if (now - startedAt > completionTimeout) {
|
|
458
|
+
// Issue #118: timeout kill 전에 capture-pane 출력을 .partial 파일로 persist.
|
|
459
|
+
// resultFile(`.txt`)이 아직 생성되지 않았을 수 있으므로 readResult 가 fallback 으로 읽는다.
|
|
460
|
+
try {
|
|
461
|
+
const partialSnapshot = _capture(currentPaneId, 200);
|
|
462
|
+
if (partialSnapshot && partialSnapshot.trim().length > 0) {
|
|
463
|
+
const writer = deps.writeFileSync || writeFileSync;
|
|
464
|
+
writer(`${resultFile}.partial`, partialSnapshot, "utf8");
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
/* best-effort, timeout 은 이미 확정 */
|
|
468
|
+
}
|
|
420
469
|
return {
|
|
421
470
|
matched: false,
|
|
422
471
|
exitCode: null,
|
|
@@ -610,6 +659,8 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
610
659
|
RESULT_DIR,
|
|
611
660
|
`${sessionName}-${paneName}.txt`,
|
|
612
661
|
).replace(/\\/g, "/");
|
|
662
|
+
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
663
|
+
cleanStaleResultArtifacts(resultFile);
|
|
613
664
|
const cmd = buildHeadlessCommand(
|
|
614
665
|
assignment.cli,
|
|
615
666
|
assignment.prompt,
|
|
@@ -686,6 +737,8 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
686
737
|
RESULT_DIR,
|
|
687
738
|
`${sessionName}-${paneName}.txt`,
|
|
688
739
|
).replace(/\\/g, "/");
|
|
740
|
+
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
741
|
+
cleanStaleResultArtifacts(resultFile);
|
|
689
742
|
const cmd = buildHeadlessCommand(
|
|
690
743
|
assignment.cli,
|
|
691
744
|
assignment.prompt,
|
|
@@ -832,9 +885,20 @@ async function awaitAll(
|
|
|
832
885
|
);
|
|
833
886
|
}
|
|
834
887
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
888
|
+
// Issue #118: matched=false(timeout kill) 에도 capture-pane 스냅샷을
|
|
889
|
+
// .partial 로 persist 하여 readResult fallback chain 이 복구할 수 있도록 한다.
|
|
890
|
+
if (!completion.matched) {
|
|
891
|
+
try {
|
|
892
|
+
const snap = capturePsmuxPane(d.paneId || d.paneName, 200);
|
|
893
|
+
if (snap && snap.trim().length > 0) {
|
|
894
|
+
writeFileSync(`${d.resultFile}.partial`, snap, "utf8");
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
/* best-effort */
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const output = readResult(d.resultFile, d.paneId);
|
|
838
902
|
unregisterHeadlessSynapseWorker(d.workerId);
|
|
839
903
|
|
|
840
904
|
if (safeProgress) {
|
|
@@ -945,7 +1009,7 @@ function collectGitDiffFiles(cwd) {
|
|
|
945
1009
|
*/
|
|
946
1010
|
export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
947
1011
|
const {
|
|
948
|
-
timeoutSec =
|
|
1012
|
+
timeoutSec = 900,
|
|
949
1013
|
layout = "2x2",
|
|
950
1014
|
onProgress,
|
|
951
1015
|
progressIntervalSec = 0,
|
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -56,10 +56,45 @@ const REQUIRED_CODEX_PROFILES = [
|
|
|
56
56
|
name: "codex53_xhigh",
|
|
57
57
|
lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "xhigh"'],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: "codex53_med",
|
|
61
|
+
lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "medium"'],
|
|
62
|
+
},
|
|
59
63
|
{
|
|
60
64
|
name: "spark53_low",
|
|
61
65
|
lines: ['model = "gpt-5.3-codex-spark"', 'model_reasoning_effort = "low"'],
|
|
62
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: "spark53_med",
|
|
69
|
+
lines: [
|
|
70
|
+
'model = "gpt-5.3-codex-spark"',
|
|
71
|
+
'model_reasoning_effort = "medium"',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "gpt54_xhigh",
|
|
76
|
+
lines: ['model = "gpt-5.4"', 'model_reasoning_effort = "xhigh"'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "gpt54_high",
|
|
80
|
+
lines: ['model = "gpt-5.4"', 'model_reasoning_effort = "high"'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "gpt54_low",
|
|
84
|
+
lines: ['model = "gpt-5.4"', 'model_reasoning_effort = "low"'],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "mini54_low",
|
|
88
|
+
lines: ['model = "gpt-5.4-mini"', 'model_reasoning_effort = "low"'],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "mini54_med",
|
|
92
|
+
lines: ['model = "gpt-5.4-mini"', 'model_reasoning_effort = "medium"'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "mini54_high",
|
|
96
|
+
lines: ['model = "gpt-5.4-mini"', 'model_reasoning_effort = "high"'],
|
|
97
|
+
},
|
|
63
98
|
];
|
|
64
99
|
|
|
65
100
|
const HUD_SYNC_EXCLUDES = new Set(["omc-hud.mjs", "omc-hud.mjs.bak"]);
|
|
@@ -556,6 +591,15 @@ function ensureCodexHubServerConfig({
|
|
|
556
591
|
}
|
|
557
592
|
}
|
|
558
593
|
|
|
594
|
+
// Top-level config.toml keys that must exist with these defaults.
|
|
595
|
+
// Only injected when the key is completely absent — existing user values are
|
|
596
|
+
// never overwritten, regardless of what value was set.
|
|
597
|
+
const REQUIRED_TOP_LEVEL_SETTINGS = [
|
|
598
|
+
{ key: "model", value: '"gpt-5.4"' },
|
|
599
|
+
{ key: "model_reasoning_effort", value: '"high"' },
|
|
600
|
+
{ key: "service_tier", value: '"fast"' },
|
|
601
|
+
];
|
|
602
|
+
|
|
559
603
|
function ensureCodexProfiles() {
|
|
560
604
|
try {
|
|
561
605
|
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
@@ -564,9 +608,45 @@ function ensureCodexProfiles() {
|
|
|
564
608
|
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
565
609
|
: "";
|
|
566
610
|
|
|
611
|
+
// Safety guard: if the file exists but is suspiciously small (< 100 bytes)
|
|
612
|
+
// skip all writes to avoid perpetuating a corrupted state.
|
|
613
|
+
if (original.length > 0 && original.length < 100) {
|
|
614
|
+
process.stderr.write(
|
|
615
|
+
`[tfx-setup] config.toml 크기 이상 (${original.length} bytes) — 쓰기 스킵. 수동 확인 필요: ${CODEX_CONFIG_PATH}\n`,
|
|
616
|
+
);
|
|
617
|
+
return { ok: false, changed: 0, message: "config too small, skipped" };
|
|
618
|
+
}
|
|
619
|
+
|
|
567
620
|
let updated = original;
|
|
568
621
|
let changed = 0;
|
|
569
622
|
|
|
623
|
+
// ── 1. top-level 필수 설정 주입 (없을 때만, 기존 값 보존) ──
|
|
624
|
+
// 파일 상단 [profiles.*] / [mcp_servers.*] 이전 영역만 검사한다.
|
|
625
|
+
// 프로필 섹션 내부의 동명 키(예: model = "gpt-5.3-codex")는 무시한다.
|
|
626
|
+
// 이미 존재하는 키는 절대 덮어쓰지 않는다.
|
|
627
|
+
for (const { key, value } of REQUIRED_TOP_LEVEL_SETTINGS) {
|
|
628
|
+
// top-level 영역 = 첫 번째 [profiles.*] / [mcp_servers.*] 헤더 이전
|
|
629
|
+
const firstSectionIdx = updated.search(/^\[(?:profiles|mcp_servers)\./m);
|
|
630
|
+
const topLevelRegion =
|
|
631
|
+
firstSectionIdx === -1 ? updated : updated.slice(0, firstSectionIdx);
|
|
632
|
+
const topLevelKeyRe = new RegExp(`^${key}\\s*=`, "m");
|
|
633
|
+
if (!topLevelKeyRe.test(topLevelRegion)) {
|
|
634
|
+
// firstSectionIdx already computed above for this iteration
|
|
635
|
+
const line = `${key} = ${value}\n`;
|
|
636
|
+
if (firstSectionIdx === -1) {
|
|
637
|
+
// 섹션이 없으면 파일 맨 앞에 추가
|
|
638
|
+
updated = line + updated;
|
|
639
|
+
} else {
|
|
640
|
+
updated =
|
|
641
|
+
updated.slice(0, firstSectionIdx) +
|
|
642
|
+
line +
|
|
643
|
+
updated.slice(firstSectionIdx);
|
|
644
|
+
}
|
|
645
|
+
changed++;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── 2. 필수 프로필 보장 ──
|
|
570
650
|
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
571
651
|
const desired = `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
572
652
|
|
|
@@ -898,6 +978,7 @@ export {
|
|
|
898
978
|
LEGACY_CODEX_MODELS,
|
|
899
979
|
PLUGIN_ROOT,
|
|
900
980
|
REQUIRED_CODEX_PROFILES,
|
|
981
|
+
REQUIRED_TOP_LEVEL_SETTINGS,
|
|
901
982
|
readMarker,
|
|
902
983
|
replaceProfileSection,
|
|
903
984
|
SETUP_MARKER_PATH,
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -1445,6 +1445,14 @@ _codex_config_swap() {
|
|
|
1445
1445
|
return 0
|
|
1446
1446
|
fi
|
|
1447
1447
|
|
|
1448
|
+
# Pre-validation: config.toml이 500 bytes 미만이면 이미 손상된 상태일 수 있음 — 스킵
|
|
1449
|
+
local config_size
|
|
1450
|
+
config_size=$(wc -c < "$config" 2>/dev/null | tr -d ' ') || config_size=0
|
|
1451
|
+
if [[ "$config_size" -lt 500 ]]; then
|
|
1452
|
+
echo "[tfx-route] 경고: config.toml 크기 ${config_size} bytes — 손상 의심, swap 스킵 (수동 확인 필요)" >&2
|
|
1453
|
+
return 0
|
|
1454
|
+
fi
|
|
1455
|
+
|
|
1448
1456
|
# 백업 생성 (이미 있으면 다른 워커가 swap 중 — 건드리지 않음)
|
|
1449
1457
|
if [[ -f "$backup" ]]; then
|
|
1450
1458
|
echo "[tfx-route] config.toml swap 스킵: 다른 워커가 사용 중 ($backup)" >&2
|
|
@@ -1454,6 +1462,7 @@ _codex_config_swap() {
|
|
|
1454
1462
|
|
|
1455
1463
|
# awk로 필터링: 비허용 MCP 서버 섹션 제거, 나머지 그대로 유지.
|
|
1456
1464
|
# keep="" 은 진입 가드에서 return 됐지만 defense-in-depth 유지.
|
|
1465
|
+
local tmp_filtered="${config}.filter.$$"
|
|
1457
1466
|
awk -v keep="$allowed_pat" '
|
|
1458
1467
|
BEGIN { skip=0 }
|
|
1459
1468
|
/^\[mcp_servers\./ {
|
|
@@ -1464,7 +1473,26 @@ _codex_config_swap() {
|
|
|
1464
1473
|
}
|
|
1465
1474
|
/^\[/ && !/^\[mcp_servers\./ { skip=0 }
|
|
1466
1475
|
!skip { print }
|
|
1467
|
-
' "$backup" > "$
|
|
1476
|
+
' "$backup" > "$tmp_filtered"
|
|
1477
|
+
|
|
1478
|
+
# Output sanity check: 필터 결과가 비었거나 백업의 30% 미만이면 적용 거부
|
|
1479
|
+
local filtered_size backup_size threshold
|
|
1480
|
+
filtered_size=$(wc -c < "$tmp_filtered" 2>/dev/null | tr -d ' ') || filtered_size=0
|
|
1481
|
+
backup_size=$(wc -c < "$backup" 2>/dev/null | tr -d ' ') || backup_size=1
|
|
1482
|
+
threshold=$(( backup_size * 30 / 100 ))
|
|
1483
|
+
if [[ "$filtered_size" -eq 0 || "$filtered_size" -lt "$threshold" ]]; then
|
|
1484
|
+
echo "[tfx-route] 경고: 필터 결과 크기 ${filtered_size} bytes (백업 ${backup_size} bytes의 30% 미만) — 적용 거부, 백업에서 복원" >&2
|
|
1485
|
+
rm -f "$tmp_filtered" 2>/dev/null
|
|
1486
|
+
rm -f "$backup" 2>/dev/null
|
|
1487
|
+
return 1
|
|
1488
|
+
fi
|
|
1489
|
+
|
|
1490
|
+
# 검증 통과 — atomic rename으로 적용
|
|
1491
|
+
if ! mv "$tmp_filtered" "$config"; then
|
|
1492
|
+
echo "[tfx-route] 경고: 필터 결과 적용 실패 (atomic rename), 백업 보존: $backup" >&2
|
|
1493
|
+
rm -f "$tmp_filtered" 2>/dev/null
|
|
1494
|
+
return 1
|
|
1495
|
+
fi
|
|
1468
1496
|
|
|
1469
1497
|
local kept
|
|
1470
1498
|
kept=$(echo "$allowed_pat" | tr '|' '\n' | wc -l | tr -d ' ')
|
|
@@ -1475,6 +1503,15 @@ _codex_config_swap() {
|
|
|
1475
1503
|
# `cat > $config` 는 cat 실행 전에 dest 가 truncate 되어 mid-stream 실패 시
|
|
1476
1504
|
# 빈/부분 파일이 남는다. 같은 디렉토리 내 mv 는 POSIX 상 atomic 이므로
|
|
1477
1505
|
# 실패해도 기존 config 와 backup 모두 보존된다.
|
|
1506
|
+
|
|
1507
|
+
# Restore sanity check: 백업 자체가 비었거나 500 bytes 미만이면 복원 중단
|
|
1508
|
+
local backup_restore_size
|
|
1509
|
+
backup_restore_size=$(wc -c < "$backup" 2>/dev/null | tr -d ' ') || backup_restore_size=0
|
|
1510
|
+
if [[ "$backup_restore_size" -lt 500 ]]; then
|
|
1511
|
+
echo "[tfx-route] 경고: backup 크기 ${backup_restore_size} bytes — 손상 의심, 복원 중단. 수동 확인 필요: $backup" >&2
|
|
1512
|
+
return 1
|
|
1513
|
+
fi
|
|
1514
|
+
|
|
1478
1515
|
local tmp="${config}.restore.$$"
|
|
1479
1516
|
if ! cp "$backup" "$tmp"; then
|
|
1480
1517
|
echo "[tfx-route] 경고: config.toml 복원 실패 (temp copy). backup 보존: $backup" >&2
|