triflux 4.1.4 → 4.2.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/package.json +1 -1
- package/scripts/setup.mjs +38 -11
- package/scripts/tfx-route-post.mjs +63 -5
- package/scripts/tfx-route.sh +147 -11
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -19,14 +19,14 @@ const REQUIRED_CODEX_PROFILES = [
|
|
|
19
19
|
{
|
|
20
20
|
name: "high",
|
|
21
21
|
lines: [
|
|
22
|
-
'model = "gpt-5.
|
|
22
|
+
'model = "gpt-5.3-codex"',
|
|
23
23
|
'model_reasoning_effort = "high"',
|
|
24
24
|
],
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: "xhigh",
|
|
28
28
|
lines: [
|
|
29
|
-
'model = "gpt-5.
|
|
29
|
+
'model = "gpt-5.3-codex"',
|
|
30
30
|
'model_reasoning_effort = "xhigh"',
|
|
31
31
|
],
|
|
32
32
|
},
|
|
@@ -147,6 +147,16 @@ function hasProfileSection(tomlContent, profileName) {
|
|
|
147
147
|
return new RegExp(section, "m").test(tomlContent);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function replaceProfileSection(tomlContent, profileName, lines) {
|
|
151
|
+
const header = `[profiles.${profileName}]`;
|
|
152
|
+
const sectionRe = new RegExp(
|
|
153
|
+
`^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*\\n(?:(?!\\[)[^\\n]*\\n?)*`,
|
|
154
|
+
"m",
|
|
155
|
+
);
|
|
156
|
+
const replacement = `${header}\n${lines.join("\n")}\n`;
|
|
157
|
+
return tomlContent.replace(sectionRe, replacement);
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
function ensureCodexProfiles() {
|
|
151
161
|
try {
|
|
152
162
|
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
@@ -156,27 +166,38 @@ function ensureCodexProfiles() {
|
|
|
156
166
|
: "";
|
|
157
167
|
|
|
158
168
|
let updated = original;
|
|
159
|
-
let
|
|
169
|
+
let changed = 0;
|
|
160
170
|
|
|
161
171
|
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (updated
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
const desired = `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
173
|
+
|
|
174
|
+
if (hasProfileSection(updated, profile.name)) {
|
|
175
|
+
// 기존 프로필이 있으면 강제 갱신
|
|
176
|
+
const before = updated;
|
|
177
|
+
updated = replaceProfileSection(updated, profile.name, profile.lines);
|
|
178
|
+
if (updated !== before) changed++;
|
|
179
|
+
} else {
|
|
180
|
+
// 없으면 추가
|
|
181
|
+
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
182
|
+
if (updated.trim().length > 0) updated += "\n";
|
|
183
|
+
updated += desired;
|
|
184
|
+
changed++;
|
|
185
|
+
}
|
|
168
186
|
}
|
|
169
187
|
|
|
170
|
-
if (
|
|
188
|
+
if (changed > 0) {
|
|
171
189
|
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
172
190
|
}
|
|
173
191
|
|
|
174
|
-
return
|
|
192
|
+
return changed;
|
|
175
193
|
} catch {
|
|
176
194
|
return 0;
|
|
177
195
|
}
|
|
178
196
|
}
|
|
179
197
|
|
|
198
|
+
export { replaceProfileSection, hasProfileSection };
|
|
199
|
+
|
|
200
|
+
async function main() {
|
|
180
201
|
let synced = 0;
|
|
181
202
|
|
|
182
203
|
for (const { src, dst, label } of SYNC_MAP) {
|
|
@@ -557,3 +578,9 @@ ${D}https://github.com/tellang/triflux${R}
|
|
|
557
578
|
}
|
|
558
579
|
|
|
559
580
|
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
584
|
+
main();
|
|
585
|
+
}
|
|
586
|
+
|
|
@@ -118,6 +118,36 @@ function filterCodexOutput(rawOutput) {
|
|
|
118
118
|
return result.join("\n");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function cleanTuiArtifacts(output, cliType) {
|
|
122
|
+
if (!output) return output;
|
|
123
|
+
|
|
124
|
+
const normalizedCliType = cliType || "";
|
|
125
|
+
|
|
126
|
+
let cleaned = output
|
|
127
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
128
|
+
.replace(/\x1b\][^\x07]*\x07/g, "")
|
|
129
|
+
.replace(/\x1b\[[0-9;]*[mGKHJsu]/g, "");
|
|
130
|
+
|
|
131
|
+
cleaned = cleaned.replace(/\r/g, "");
|
|
132
|
+
|
|
133
|
+
if (normalizedCliType.startsWith("codex")) {
|
|
134
|
+
cleaned = cleaned
|
|
135
|
+
.replace(/^[^\S\n]*[╭╮╰╯│─┌┐└┘├┤┬┴┼].*$/gm, "")
|
|
136
|
+
.replace(/^[^\S\n]*[›❯]\s*$/gm, "")
|
|
137
|
+
.replace(/^\s*codex\s*$/gm, "")
|
|
138
|
+
.replace(/^[^\S\n]*[›❯]\s*Applied.*$/gm, "");
|
|
139
|
+
} else if (normalizedCliType.startsWith("gemini")) {
|
|
140
|
+
cleaned = cleaned.replace(/^[^\S\n]*[╭╮╰╯│─═].*$/gm, "").replace(/^[^\S\n]*>\s*$/gm, "");
|
|
141
|
+
} else if (normalizedCliType.startsWith("claude")) {
|
|
142
|
+
cleaned = cleaned.replace(/^[^\S\n]*[━─]{5,}.*$/gm, "");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
146
|
+
cleaned = cleaned.trim();
|
|
147
|
+
|
|
148
|
+
return cleaned;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
// ── 실행 로그 기록 (JSONL, append-only) ──
|
|
122
152
|
function logExecution(params) {
|
|
123
153
|
const logFile = join(LOG_DIR, "tfx-route-stats.jsonl");
|
|
@@ -311,7 +341,19 @@ function main() {
|
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
// 3. 실행 로그
|
|
314
|
-
logExecution({
|
|
344
|
+
logExecution({
|
|
345
|
+
agent,
|
|
346
|
+
cli: cliType,
|
|
347
|
+
effort,
|
|
348
|
+
run_mode: runMode,
|
|
349
|
+
opus,
|
|
350
|
+
status,
|
|
351
|
+
exit_code: exitCode,
|
|
352
|
+
elapsed,
|
|
353
|
+
timeout,
|
|
354
|
+
mcp_profile: mcpProfile,
|
|
355
|
+
tokens,
|
|
356
|
+
});
|
|
315
357
|
|
|
316
358
|
// 4. 성공 시 토큰 누적
|
|
317
359
|
if (exitCode === 0) accumulateTokens(cliType, tokens);
|
|
@@ -344,12 +386,19 @@ function main() {
|
|
|
344
386
|
console.log("status: success");
|
|
345
387
|
}
|
|
346
388
|
console.log("=== OUTPUT ===");
|
|
347
|
-
|
|
389
|
+
let filtered = cliType === "codex" ? filterCodexOutput(rawOutput) : rawOutput;
|
|
390
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
391
|
+
filtered = cleanTuiArtifacts(filtered, cliType);
|
|
392
|
+
}
|
|
348
393
|
console.log(truncateOutput(filtered, maxBytes));
|
|
349
394
|
} else if (exitCode === 124) {
|
|
350
395
|
console.log(`status: timeout (${timeout}s 초과)`);
|
|
351
396
|
console.log("=== PARTIAL OUTPUT ===");
|
|
352
|
-
|
|
397
|
+
let partialFiltered = rawOutput;
|
|
398
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
399
|
+
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
400
|
+
}
|
|
401
|
+
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
353
402
|
console.log("=== STDERR ===");
|
|
354
403
|
console.log(stderrContent.split("\n").slice(-10).join("\n"));
|
|
355
404
|
} else {
|
|
@@ -358,9 +407,18 @@ function main() {
|
|
|
358
407
|
console.log(stderrContent.split("\n").slice(-20).join("\n"));
|
|
359
408
|
if (rawOutput) {
|
|
360
409
|
console.log("=== PARTIAL OUTPUT ===");
|
|
361
|
-
|
|
410
|
+
let partialFiltered = rawOutput;
|
|
411
|
+
if (a.clean_tui !== "false" && process.env.TFX_CLEAN_TUI !== "0") {
|
|
412
|
+
partialFiltered = cleanTuiArtifacts(partialFiltered, cliType);
|
|
413
|
+
}
|
|
414
|
+
console.log(truncateOutput(partialFiltered, maxBytes));
|
|
362
415
|
}
|
|
363
416
|
}
|
|
364
417
|
}
|
|
365
418
|
|
|
366
|
-
|
|
419
|
+
import { fileURLToPath } from "url";
|
|
420
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
421
|
+
main();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { cleanTuiArtifacts };
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# tfx-route.sh v2.
|
|
2
|
+
# tfx-route.sh v2.4 — CLI 라우팅 래퍼 (triflux)
|
|
3
3
|
#
|
|
4
4
|
# v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
|
|
5
5
|
# v2.0: tfx-route.sh 리네임
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# - Gemini health check 지수 백오프 (30×1s → 5×exp)
|
|
10
10
|
# - 컨텍스트 파일 5번째 인자 지원
|
|
11
11
|
#
|
|
12
|
-
VERSION="2.
|
|
12
|
+
VERSION="2.4"
|
|
13
13
|
#
|
|
14
14
|
# 사용법:
|
|
15
15
|
# tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
|
|
@@ -417,6 +417,54 @@ RESULT_EOF
|
|
|
417
417
|
fi
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
detect_quota_exceeded() {
|
|
421
|
+
local stdout_file="$1"
|
|
422
|
+
local stderr_file="$2"
|
|
423
|
+
local -a patterns=(
|
|
424
|
+
"usage limit exceeded" "rate limit exceeded" "rate limit reached"
|
|
425
|
+
"try again at" "purchase more credits"
|
|
426
|
+
"quota exceeded" "RESOURCE_EXHAUSTED" "rateLimitExceeded" "Too Many Requests"
|
|
427
|
+
"rate_limit_error" "overloaded_error" "insufficient_quota"
|
|
428
|
+
)
|
|
429
|
+
local pattern
|
|
430
|
+
for pattern in "${patterns[@]}"; do
|
|
431
|
+
if grep -qi "$pattern" "$stdout_file" 2>/dev/null || grep -qi "$pattern" "$stderr_file" 2>/dev/null; then
|
|
432
|
+
echo "[tfx-quota] 감지: '$pattern' in $CLI_TYPE" >&2
|
|
433
|
+
return 0
|
|
434
|
+
fi
|
|
435
|
+
done
|
|
436
|
+
return 1
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
auto_reroute() {
|
|
440
|
+
local failed_cli="$1"
|
|
441
|
+
local target_cli=""
|
|
442
|
+
case "$failed_cli" in
|
|
443
|
+
codex) target_cli="gemini"; echo "[tfx-quota] Codex → Gemini 자동 전환" >&2 ;;
|
|
444
|
+
gemini) target_cli="codex"; echo "[tfx-quota] Gemini → Codex 자동 전환" >&2 ;;
|
|
445
|
+
*) echo "[tfx-quota] $failed_cli 대체 CLI 없음" >&2; return 1 ;;
|
|
446
|
+
esac
|
|
447
|
+
|
|
448
|
+
# 대상 CLI 존재 확인 (P2: command not found 방지)
|
|
449
|
+
local target_bin
|
|
450
|
+
case "$target_cli" in
|
|
451
|
+
codex) target_bin="$CODEX_BIN" ;;
|
|
452
|
+
gemini) target_bin="$GEMINI_BIN" ;;
|
|
453
|
+
esac
|
|
454
|
+
if ! command -v "$target_bin" &>/dev/null; then
|
|
455
|
+
echo "[tfx-quota] $target_cli CLI 미설치 — 자동 전환 불가" >&2
|
|
456
|
+
return 1
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
local quota_marker="$TFX_TMP/tfx-quota-${failed_cli}-$(date +%Y%m%d)"
|
|
460
|
+
echo "$(date +%s)" >> "$quota_marker"
|
|
461
|
+
ORIGINAL_AGENT="$AGENT_TYPE"
|
|
462
|
+
ORIGINAL_CLI_ARGS="$CLI_ARGS"
|
|
463
|
+
export TFX_REROUTED_FROM="$CLI_TYPE"
|
|
464
|
+
TFX_CLI_MODE="$target_cli" exec bash "${BASH_SOURCE[0]}" \
|
|
465
|
+
"$AGENT_TYPE" "$PROMPT" "$MCP_PROFILE" "$USER_TIMEOUT" "$CONTEXT_FILE"
|
|
466
|
+
}
|
|
467
|
+
|
|
420
468
|
capture_workspace_signature() {
|
|
421
469
|
if ! command -v git &>/dev/null; then
|
|
422
470
|
return 1
|
|
@@ -543,7 +591,7 @@ route_agent() {
|
|
|
543
591
|
TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
|
|
544
592
|
TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
|
|
545
593
|
TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
|
|
546
|
-
TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-
|
|
594
|
+
TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-exec}"
|
|
547
595
|
# Codex 요금제 자동 감지 (preflight 캐시 → auth.json JWT)
|
|
548
596
|
# 환경변수 명시 설정 시 우선, 미설정 시 캐시에서 읽기, 캐시도 없으면 pro
|
|
549
597
|
if [[ -z "${TFX_CODEX_PLAN:-}" ]]; then
|
|
@@ -845,6 +893,44 @@ emit_claude_native_metadata() {
|
|
|
845
893
|
echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
|
|
846
894
|
}
|
|
847
895
|
|
|
896
|
+
# heartbeat_monitor PID [INTERVAL] [STALL_THRESHOLD]
|
|
897
|
+
# - PID: 감시할 워커 프로세스 PID
|
|
898
|
+
# - INTERVAL: heartbeat 출력 간격 (초, 기본 10)
|
|
899
|
+
# - STALL_THRESHOLD: stall 경고 임계값 (초, 기본 60)
|
|
900
|
+
# 환경변수: TFX_HEARTBEAT (0이면 비활성화), TFX_HEARTBEAT_INTERVAL, TFX_STALL_THRESHOLD
|
|
901
|
+
heartbeat_monitor() {
|
|
902
|
+
[[ "${TFX_HEARTBEAT:-1}" -eq 0 ]] && return 0
|
|
903
|
+
local pid="$1"
|
|
904
|
+
local interval="${2:-${TFX_HEARTBEAT_INTERVAL:-10}}"
|
|
905
|
+
local stall_threshold="${3:-${TFX_STALL_THRESHOLD:-60}}"
|
|
906
|
+
local last_size=0 stall_count=0
|
|
907
|
+
|
|
908
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
909
|
+
sleep "$interval"
|
|
910
|
+
local current_size=0
|
|
911
|
+
[[ -f "$STDOUT_LOG" ]] && current_size=$(wc -c < "$STDOUT_LOG" 2>/dev/null || echo 0)
|
|
912
|
+
# P3: stderr 활동도 포함하여 거짓 STALL 방지
|
|
913
|
+
local stderr_size=0
|
|
914
|
+
[[ -f "$STDERR_LOG" ]] && stderr_size=$(wc -c < "$STDERR_LOG" 2>/dev/null || echo 0)
|
|
915
|
+
current_size=$((current_size + stderr_size))
|
|
916
|
+
local elapsed=$(($(date +%s) - TIMESTAMP))
|
|
917
|
+
|
|
918
|
+
if [[ "$current_size" -gt "$last_size" ]]; then
|
|
919
|
+
stall_count=0
|
|
920
|
+
echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=active" >&2
|
|
921
|
+
else
|
|
922
|
+
stall_count=$((stall_count + interval))
|
|
923
|
+
if [[ "$stall_count" -ge "$stall_threshold" ]]; then
|
|
924
|
+
echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=STALL stall=${stall_count}s" >&2
|
|
925
|
+
else
|
|
926
|
+
echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=quiet stall=${stall_count}s" >&2
|
|
927
|
+
fi
|
|
928
|
+
fi
|
|
929
|
+
last_size=$current_size
|
|
930
|
+
done
|
|
931
|
+
echo "[tfx-heartbeat] pid=$pid terminated" >&2
|
|
932
|
+
}
|
|
933
|
+
|
|
848
934
|
resolve_worker_runner_script() {
|
|
849
935
|
if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
|
|
850
936
|
printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
|
|
@@ -864,6 +950,8 @@ run_stream_worker() {
|
|
|
864
950
|
local prompt="$2"
|
|
865
951
|
local use_tee_flag="$3"
|
|
866
952
|
shift 3
|
|
953
|
+
local exit_code_local=0
|
|
954
|
+
local worker_pid hb_pid
|
|
867
955
|
|
|
868
956
|
local runner_script
|
|
869
957
|
if ! runner_script=$(resolve_worker_runner_script); then
|
|
@@ -886,10 +974,18 @@ run_stream_worker() {
|
|
|
886
974
|
)
|
|
887
975
|
|
|
888
976
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
889
|
-
printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
|
|
977
|
+
printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
890
978
|
else
|
|
891
|
-
printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
|
|
979
|
+
printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
892
980
|
fi
|
|
981
|
+
worker_pid=$!
|
|
982
|
+
|
|
983
|
+
heartbeat_monitor "$worker_pid" &
|
|
984
|
+
hb_pid=$!
|
|
985
|
+
|
|
986
|
+
wait "$worker_pid" || exit_code_local=$?
|
|
987
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
988
|
+
return "$exit_code_local"
|
|
893
989
|
}
|
|
894
990
|
|
|
895
991
|
run_legacy_gemini() {
|
|
@@ -939,6 +1035,7 @@ run_legacy_gemini() {
|
|
|
939
1035
|
done
|
|
940
1036
|
|
|
941
1037
|
local exit_code_local=0
|
|
1038
|
+
local hb_pid
|
|
942
1039
|
if [[ "$health_ok" == "false" ]]; then
|
|
943
1040
|
wait "$pid" 2>/dev/null
|
|
944
1041
|
echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
|
|
@@ -950,7 +1047,10 @@ run_legacy_gemini() {
|
|
|
950
1047
|
pid=$!
|
|
951
1048
|
fi
|
|
952
1049
|
|
|
1050
|
+
heartbeat_monitor "$pid" &
|
|
1051
|
+
hb_pid=$!
|
|
953
1052
|
wait "$pid" || exit_code_local=$?
|
|
1053
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
954
1054
|
return "$exit_code_local"
|
|
955
1055
|
}
|
|
956
1056
|
|
|
@@ -985,6 +1085,7 @@ run_codex_exec() {
|
|
|
985
1085
|
local prompt="$1"
|
|
986
1086
|
local use_tee_flag="$2"
|
|
987
1087
|
local exit_code_local=0
|
|
1088
|
+
local worker_pid hb_pid
|
|
988
1089
|
local -a codex_args=()
|
|
989
1090
|
read -r -a codex_args <<< "$CLI_ARGS"
|
|
990
1091
|
if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
|
|
@@ -992,10 +1093,17 @@ run_codex_exec() {
|
|
|
992
1093
|
fi
|
|
993
1094
|
|
|
994
1095
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
995
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
|
|
1096
|
+
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
996
1097
|
else
|
|
997
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG"
|
|
1098
|
+
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
998
1099
|
fi
|
|
1100
|
+
worker_pid=$!
|
|
1101
|
+
|
|
1102
|
+
heartbeat_monitor "$worker_pid" &
|
|
1103
|
+
hb_pid=$!
|
|
1104
|
+
|
|
1105
|
+
wait "$worker_pid" || exit_code_local=$?
|
|
1106
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
999
1107
|
|
|
1000
1108
|
if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
|
|
1001
1109
|
# stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
|
|
@@ -1029,6 +1137,7 @@ run_codex_mcp() {
|
|
|
1029
1137
|
local use_tee_flag="$2"
|
|
1030
1138
|
local mcp_script node_bin
|
|
1031
1139
|
local exit_code_local=0
|
|
1140
|
+
local worker_pid hb_pid
|
|
1032
1141
|
|
|
1033
1142
|
if ! mcp_script=$(resolve_codex_mcp_script); then
|
|
1034
1143
|
echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
|
|
@@ -1078,10 +1187,17 @@ run_codex_mcp() {
|
|
|
1078
1187
|
esac
|
|
1079
1188
|
|
|
1080
1189
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1081
|
-
timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
|
|
1190
|
+
timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1082
1191
|
else
|
|
1083
|
-
timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
|
|
1192
|
+
timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1084
1193
|
fi
|
|
1194
|
+
worker_pid=$!
|
|
1195
|
+
|
|
1196
|
+
heartbeat_monitor "$worker_pid" &
|
|
1197
|
+
hb_pid=$!
|
|
1198
|
+
|
|
1199
|
+
wait "$worker_pid" || exit_code_local=$?
|
|
1200
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1085
1201
|
|
|
1086
1202
|
# 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
|
|
1087
1203
|
if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
|
|
@@ -1197,6 +1313,7 @@ FALLBACK_EOF
|
|
|
1197
1313
|
echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
|
|
1198
1314
|
fi
|
|
1199
1315
|
[[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
|
|
1316
|
+
[[ -n "${TFX_REROUTED_FROM:-}" ]] && echo "[tfx-route] rerouted_from=$TFX_REROUTED_FROM" >&2
|
|
1200
1317
|
|
|
1201
1318
|
# Per-process 에이전트 등록
|
|
1202
1319
|
register_agent
|
|
@@ -1327,6 +1444,14 @@ EOF
|
|
|
1327
1444
|
fi
|
|
1328
1445
|
fi
|
|
1329
1446
|
|
|
1447
|
+
# 쿼타 감지 + 자동 re-route
|
|
1448
|
+
if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
|
|
1449
|
+
if [[ "${TFX_QUOTA_REROUTE:-1}" -ne 0 ]] && [[ -z "${TFX_REROUTED_FROM:-}" ]] && detect_quota_exceeded "$STDOUT_LOG" "$STDERR_LOG"; then
|
|
1450
|
+
export TFX_REROUTED_FROM="$CLI_TYPE"
|
|
1451
|
+
auto_reroute "$CLI_TYPE"
|
|
1452
|
+
fi
|
|
1453
|
+
fi
|
|
1454
|
+
|
|
1330
1455
|
# 팀 모드: task complete + 리드 보고
|
|
1331
1456
|
if [[ -n "$TFX_TEAM_NAME" ]]; then
|
|
1332
1457
|
if [[ "$exit_code" -eq 0 ]]; then
|
|
@@ -1359,18 +1484,29 @@ EOF
|
|
|
1359
1484
|
--mcp-profile "$MCP_PROFILE" \
|
|
1360
1485
|
--stderr-log "$STDERR_LOG" \
|
|
1361
1486
|
--stdout-log "$STDOUT_LOG" \
|
|
1487
|
+
--rerouted-from "${TFX_REROUTED_FROM:-}" \
|
|
1362
1488
|
--max-bytes "$MAX_STDOUT_BYTES" \
|
|
1363
|
-
--tee-active "$use_tee"
|
|
1489
|
+
--tee-active "$use_tee" \
|
|
1490
|
+
--clean-tui "${TFX_CLEAN_TUI:-true}"
|
|
1364
1491
|
else
|
|
1365
1492
|
# post.mjs 없으면 기본 출력 (fallback)
|
|
1366
1493
|
echo "=== TFX-ROUTE RESULT ==="
|
|
1367
1494
|
echo "agent: $AGENT_TYPE"
|
|
1368
1495
|
echo "cli: $CLI_TYPE"
|
|
1496
|
+
[[ -n "${TFX_REROUTED_FROM:-}" ]] && echo "rerouted_from: $TFX_REROUTED_FROM"
|
|
1369
1497
|
echo "exit_code: $exit_code"
|
|
1370
1498
|
echo "elapsed: ${elapsed}s"
|
|
1371
1499
|
echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
|
|
1372
1500
|
echo "=== OUTPUT ==="
|
|
1373
|
-
|
|
1501
|
+
if [[ "${TFX_CLEAN_TUI:-1}" != "0" ]]; then
|
|
1502
|
+
cat "$STDOUT_LOG" 2>/dev/null \
|
|
1503
|
+
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
|
|
1504
|
+
| sed '/^[[:space:]]*[╭╮╰╯│─┌┐└┘├┤┬┴┼]/d' \
|
|
1505
|
+
| sed '/^[[:space:]]*[›❯][[:space:]]*$/d' \
|
|
1506
|
+
| head -c "$MAX_STDOUT_BYTES"
|
|
1507
|
+
else
|
|
1508
|
+
cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
|
|
1509
|
+
fi
|
|
1374
1510
|
fi
|
|
1375
1511
|
|
|
1376
1512
|
return "$exit_code"
|