@triflux/core 10.53.0 → 10.55.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/hooks/codex-session-hook.mjs +1 -17
- package/hub/cli-adapter-base.mjs +3 -424
- package/hub/index.mjs +0 -5
- package/hub/lib/hosts-compat.mjs +18 -64
- package/hub/lib/private-tmp.mjs +35 -0
- package/hub/lib/process-utils.mjs +19 -220
- package/hub/lib/prompt-tmp.mjs +6 -24
- package/hub/lib/spawn-trace.mjs +112 -5
- package/hub/lib/timeout-defaults.mjs +34 -6
- package/hub/lib/worker-lifecycle.mjs +3 -0
- package/hub/platform.mjs +3 -1
- package/hub/team/agent-map.json +29 -0
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +329 -0
- package/hub/team/psmux.mjs +1765 -0
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +541 -0
- package/hud/constants.mjs +0 -3
- package/hud/providers/claude.mjs +35 -15
- package/hud/providers/codex.mjs +169 -42
- package/hud/utils.mjs +0 -23
- package/package.json +2 -2
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/scripts/lib/tmr-install.mjs +183 -0
- package/hooks/agy-session-hook.mjs +0 -155
- package/hooks/claude-cwd-projection-refresh.mjs +0 -11
- package/hooks/hook-orchestrator.mjs +0 -11
- package/hooks/lib/resolve-root.mjs +0 -62
- package/hub/codex-adapter.mjs +0 -193
- package/hub/codex-preflight.mjs +0 -219
- package/hub/lib/cto-env.mjs +0 -70
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
- package/scripts/lib/session-presence.mjs +0 -151
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { IS_WINDOWS
|
|
14
|
+
import { IS_WINDOWS } from "../platform.mjs";
|
|
15
15
|
|
|
16
16
|
const CLEANUP_SCRIPT_DIR = join(tmpdir(), "tfx-process-utils");
|
|
17
17
|
const SCAN_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "scan-processes.ps1");
|
|
@@ -54,80 +54,6 @@ export function isPidAlive(pid) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
* 동기적 sleep. Atomics.wait 우선, 불가 시 busy-wait 폴백.
|
|
59
|
-
*/
|
|
60
|
-
function sleepSyncMs(ms) {
|
|
61
|
-
try {
|
|
62
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
|
-
} catch {
|
|
64
|
-
const end = Date.now() + ms;
|
|
65
|
-
while (Date.now() < end) {
|
|
66
|
-
/* spin */
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 고아 PID 목록에 SIGTERM → 3초 대기 → SIGKILL 에스컬레이션을 적용한다.
|
|
73
|
-
* PID 재사용 레이스 방어: SIGTERM 전 alive 확인, SIGKILL 전 재검증.
|
|
74
|
-
* @param {number[]} orphanPids
|
|
75
|
-
* @param {Map<number, {ppid: number, name: string}>} [procMap] PID 재사용 감지용 스냅샷
|
|
76
|
-
* @returns {number} killed count
|
|
77
|
-
*/
|
|
78
|
-
function killWithEscalation(orphanPids, procMap) {
|
|
79
|
-
if (orphanPids.length === 0) return 0;
|
|
80
|
-
|
|
81
|
-
// SIGTERM 전 alive 스냅샷 — 이미 죽은 PID는 카운트에서 제외
|
|
82
|
-
const aliveBeforeKill = new Set(orphanPids.filter((pid) => isPidAlive(pid)));
|
|
83
|
-
|
|
84
|
-
for (const pid of aliveBeforeKill) {
|
|
85
|
-
killProcess(pid, { signal: "SIGTERM" });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
sleepSyncMs(3000);
|
|
89
|
-
|
|
90
|
-
let killed = 0;
|
|
91
|
-
for (const pid of aliveBeforeKill) {
|
|
92
|
-
if (isPidAlive(pid)) {
|
|
93
|
-
// PID 재사용 방어: procMap이 있으면 스캔 시점의 ppid와 현재 ppid 비교
|
|
94
|
-
// ppid가 변경되었으면 PID가 재사용된 것이므로 kill하지 않���
|
|
95
|
-
if (procMap) {
|
|
96
|
-
const snapshot = procMap.get(pid);
|
|
97
|
-
if (snapshot) {
|
|
98
|
-
try {
|
|
99
|
-
const current = execSync(
|
|
100
|
-
IS_WINDOWS
|
|
101
|
-
? `powershell -NoProfile -WindowStyle Hidden -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' -ErrorAction SilentlyContinue).ParentProcessId"`
|
|
102
|
-
: `ps -o ppid= -p ${pid}`,
|
|
103
|
-
{
|
|
104
|
-
encoding: "utf8",
|
|
105
|
-
timeout: 3000,
|
|
106
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
107
|
-
windowsHide: true,
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
const currentPpid = Number.parseInt(current.trim(), 10);
|
|
111
|
-
if (Number.isFinite(currentPpid) && currentPpid !== snapshot.ppid) {
|
|
112
|
-
continue; // PID 재사용 감지 — skip
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// 조회 실패 시 안전하게 skip
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
killProcess(pid, {
|
|
121
|
-
signal: "SIGKILL",
|
|
122
|
-
force: true,
|
|
123
|
-
tree: IS_WINDOWS,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (!isPidAlive(pid)) killed++;
|
|
127
|
-
}
|
|
128
|
-
return killed;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
57
|
/**
|
|
132
58
|
* PowerShell 헬퍼 스크립트를 임시 디렉토리에 생성한다.
|
|
133
59
|
* bash의 $_ 이스케이핑 문제를 피하기 위해 -File로 실행.
|
|
@@ -261,12 +187,12 @@ function hasLiveCliDescendant(pid, procMap) {
|
|
|
261
187
|
}
|
|
262
188
|
|
|
263
189
|
/**
|
|
264
|
-
*
|
|
190
|
+
* `tfx doctor --fix` 가 쓰는 이름. cleanupOrphanRuntimeProcesses 와 같다.
|
|
265
191
|
* @param {Parameters<typeof cleanupOrphanRuntimeProcesses>[0]} opts
|
|
266
|
-
* @returns {
|
|
192
|
+
* @returns {ReturnType<typeof cleanupOrphanRuntimeProcesses>}
|
|
267
193
|
*/
|
|
268
194
|
export function cleanupOrphanNodeProcesses(opts = {}) {
|
|
269
|
-
return cleanupOrphanRuntimeProcesses(
|
|
195
|
+
return cleanupOrphanRuntimeProcesses(opts);
|
|
270
196
|
}
|
|
271
197
|
|
|
272
198
|
function normalizePowerShellJson(output) {
|
|
@@ -297,10 +223,6 @@ function normalizeName(name) {
|
|
|
297
223
|
return String(name || "").toLowerCase();
|
|
298
224
|
}
|
|
299
225
|
|
|
300
|
-
function normalizeCommandLine(commandLine) {
|
|
301
|
-
return String(commandLine || "").replace(/\//g, "\\");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
226
|
function processRecordFromCim(record) {
|
|
305
227
|
const pid = normalizePid(record?.ProcessId ?? record?.pid);
|
|
306
228
|
if (!pid) return null;
|
|
@@ -460,16 +382,6 @@ function matchesPattern(commandLine, pattern, { exact = false } = {}) {
|
|
|
460
382
|
return exact ? commandLine === text : commandLine.includes(text);
|
|
461
383
|
}
|
|
462
384
|
|
|
463
|
-
function hasExactGbrainServe(commandLine) {
|
|
464
|
-
const normalized = commandLine.trim();
|
|
465
|
-
return (
|
|
466
|
-
/^("?[^"\s]*bun(?:\.exe)?"?\s+)?gbrain\s+serve$/i.test(normalized) ||
|
|
467
|
-
/^("?[^"\s]*bun(?:\.exe)?"?\s+)"?[^"]*gbrain[\\/]+src[\\/]+cli\.ts"?\s+serve$/i.test(
|
|
468
|
-
normalized,
|
|
469
|
-
)
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
385
|
/**
|
|
474
386
|
* Return a process tree rooted at `rootPid`.
|
|
475
387
|
*
|
|
@@ -651,75 +563,41 @@ export function findProcessesByCommandLine(
|
|
|
651
563
|
}
|
|
652
564
|
|
|
653
565
|
/**
|
|
654
|
-
*
|
|
566
|
+
* Windows 고아 node/bash/cmd/uvx 런타임을 정리한다.
|
|
655
567
|
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* behavior by targeting scoped orphan node runtimes only.
|
|
568
|
+
* 조상 체인이 살아 있거나 Claude/Codex/Gemini 자손이 있으면 건드리지 않는다.
|
|
569
|
+
* 운영 호출자가 Windows `tfx doctor --fix` 하나라 다른 OS 에서는 아무것도 하지 않는다.
|
|
659
570
|
*
|
|
660
|
-
* @param {{
|
|
571
|
+
* @param {{isWindows?: boolean, spawnSyncFn?: typeof spawnSync, killFn?: typeof process.kill, protectedPids?: Set<number>}} opts
|
|
661
572
|
* @returns {{killed: number, remaining: number, killedProcesses: Array<{pid: number, ppid: number, name: string, commandLine: string, killReason: string}>}}
|
|
662
573
|
*/
|
|
663
574
|
export function cleanupOrphanRuntimeProcesses({
|
|
664
|
-
legacy = false,
|
|
665
|
-
includeConhost = false,
|
|
666
|
-
sessionIds = [],
|
|
667
575
|
isWindows = IS_WINDOWS,
|
|
668
576
|
spawnSyncFn = spawnSync,
|
|
669
577
|
killFn = process.kill,
|
|
670
578
|
protectedPids,
|
|
671
579
|
} = {}) {
|
|
672
|
-
if (!isWindows) return
|
|
580
|
+
if (!isWindows) return { killed: 0, remaining: 0, killedProcesses: [] };
|
|
673
581
|
|
|
674
582
|
const protectedSet = getProtectedPids({
|
|
675
583
|
protectedPids,
|
|
676
584
|
isWindows,
|
|
677
585
|
spawnSyncFn,
|
|
678
|
-
includeAncestorScan:
|
|
586
|
+
includeAncestorScan: true,
|
|
679
587
|
});
|
|
680
588
|
const processes = getProcessSnapshot({ isWindows, spawnSyncFn });
|
|
589
|
+
const procMap = new Map(processes.map((proc) => [proc.pid, proc]));
|
|
681
590
|
let killed = 0;
|
|
682
591
|
const killedProcesses = [];
|
|
683
|
-
const procMap = legacy
|
|
684
|
-
? new Map(processes.map((proc) => [proc.pid, proc]))
|
|
685
|
-
: new Map(processes.map((proc) => [proc.pid, proc]));
|
|
686
592
|
|
|
687
593
|
for (const proc of processes) {
|
|
688
|
-
if (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (
|
|
696
|
-
LEGACY_ORPHAN_KILLABLE_NAMES.has(name) &&
|
|
697
|
-
!hasLiveAncestorChain(proc.pid, procMap, protectedSet) &&
|
|
698
|
-
!hasLiveCliDescendant(proc.pid, procMap)
|
|
699
|
-
) {
|
|
700
|
-
shouldKill = true;
|
|
701
|
-
killReason = "legacy_orphan_ancestor_chain_dead";
|
|
702
|
-
}
|
|
703
|
-
} else if (name === "bun.exe") {
|
|
704
|
-
if (
|
|
705
|
-
hasExactGbrainServe(proc.commandLine) &&
|
|
706
|
-
!hasLiveAncestorChain(proc.pid, procMap, protectedSet) &&
|
|
707
|
-
!hasLiveCliDescendant(proc.pid, procMap)
|
|
708
|
-
) {
|
|
709
|
-
shouldKill = true;
|
|
710
|
-
killReason = "bun_gbrain_serve_orphan";
|
|
711
|
-
}
|
|
712
|
-
} else if (includeConhost && name === "conhost.exe") {
|
|
713
|
-
if (
|
|
714
|
-
sessionIds.length > 0 &&
|
|
715
|
-
sessionIds.some((id) => id && commandLine.includes(id))
|
|
716
|
-
) {
|
|
717
|
-
shouldKill = true;
|
|
718
|
-
killReason = "conhost_session_match";
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (!shouldKill) continue;
|
|
594
|
+
if (
|
|
595
|
+
protectedSet.has(proc.pid) ||
|
|
596
|
+
!LEGACY_ORPHAN_KILLABLE_NAMES.has(normalizeName(proc.name)) ||
|
|
597
|
+
hasLiveAncestorChain(proc.pid, procMap, protectedSet) ||
|
|
598
|
+
hasLiveCliDescendant(proc.pid, procMap)
|
|
599
|
+
)
|
|
600
|
+
continue;
|
|
723
601
|
try {
|
|
724
602
|
killFn(proc.pid, "SIGKILL");
|
|
725
603
|
killed++;
|
|
@@ -728,7 +606,7 @@ export function cleanupOrphanRuntimeProcesses({
|
|
|
728
606
|
ppid: proc.ppid,
|
|
729
607
|
name: proc.name,
|
|
730
608
|
commandLine: String(proc.commandLine || "").slice(0, 200),
|
|
731
|
-
killReason,
|
|
609
|
+
killReason: "legacy_orphan_ancestor_chain_dead",
|
|
732
610
|
});
|
|
733
611
|
} catch {}
|
|
734
612
|
}
|
|
@@ -818,82 +696,3 @@ export function cleanupStaleFsmonitorDaemons({
|
|
|
818
696
|
|
|
819
697
|
return { killed, stale };
|
|
820
698
|
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Unix/macOS 고아 프로세스 정리.
|
|
824
|
-
* `ps -eo pid,ppid,comm` 기반 프로세스 맵 → 동일한 조상 체인 판정 → SIGKILL 에스컬레이션.
|
|
825
|
-
* @returns {{ killed: number, remaining: number }}
|
|
826
|
-
*/
|
|
827
|
-
function cleanupOrphansUnix() {
|
|
828
|
-
const myPid = process.pid;
|
|
829
|
-
|
|
830
|
-
const protectedPids = new Set();
|
|
831
|
-
protectedPids.add(myPid);
|
|
832
|
-
|
|
833
|
-
// 현재 프로세스의 조상 트리 보호
|
|
834
|
-
try {
|
|
835
|
-
let current = myPid;
|
|
836
|
-
for (let i = 0; i < 10; i++) {
|
|
837
|
-
protectedPids.add(current);
|
|
838
|
-
const output = execSync(`ps -o ppid= -p ${current}`, {
|
|
839
|
-
encoding: "utf8",
|
|
840
|
-
timeout: 3000,
|
|
841
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
842
|
-
});
|
|
843
|
-
const ppid = Number.parseInt(output.trim(), 10);
|
|
844
|
-
if (!Number.isFinite(ppid) || ppid <= 1) break;
|
|
845
|
-
current = ppid;
|
|
846
|
-
}
|
|
847
|
-
} catch {}
|
|
848
|
-
|
|
849
|
-
// 프로세스 맵 구축 (런타임 + CLI — 체인 추적 정확도를 위해 CLI도 포함)
|
|
850
|
-
const procMap = new Map();
|
|
851
|
-
try {
|
|
852
|
-
const output = execSync("ps -eo pid,ppid,comm", {
|
|
853
|
-
encoding: "utf8",
|
|
854
|
-
timeout: 10000,
|
|
855
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
856
|
-
});
|
|
857
|
-
for (const line of output.split("\n").slice(1)) {
|
|
858
|
-
const parts = line.trim().split(/\s+/);
|
|
859
|
-
if (parts.length < 3) continue;
|
|
860
|
-
const pid = Number.parseInt(parts[0], 10);
|
|
861
|
-
const ppid = Number.parseInt(parts[1], 10);
|
|
862
|
-
const name = parts.slice(2).join(" ");
|
|
863
|
-
if (
|
|
864
|
-
Number.isFinite(pid) &&
|
|
865
|
-
pid > 0 &&
|
|
866
|
-
/^(node|bash|sh|python|codex|claude|gemini|uvx)/.test(name)
|
|
867
|
-
) {
|
|
868
|
-
procMap.set(pid, { ppid, name });
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
} catch {}
|
|
872
|
-
|
|
873
|
-
// kill 대상: node, python, codex, claude, uvx — bash/sh는 사용자 인터랙티브 쉘 가능성
|
|
874
|
-
const killableUnix = /^(node|python|codex|claude|gemini|uvx)/;
|
|
875
|
-
|
|
876
|
-
// 고아 판정 + SIGKILL 에스컬레이션
|
|
877
|
-
const orphanPids = [];
|
|
878
|
-
for (const [pid, info] of procMap) {
|
|
879
|
-
if (protectedPids.has(pid)) continue;
|
|
880
|
-
if (!killableUnix.test(info.name)) continue;
|
|
881
|
-
if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
|
|
882
|
-
if (hasLiveCliDescendant(pid, procMap)) continue;
|
|
883
|
-
orphanPids.push(pid);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const killed = killWithEscalation(orphanPids, procMap);
|
|
887
|
-
|
|
888
|
-
let remaining = 0;
|
|
889
|
-
try {
|
|
890
|
-
const output = execSync("ps -eo comm | grep -c '^node$'", {
|
|
891
|
-
encoding: "utf8",
|
|
892
|
-
timeout: 3000,
|
|
893
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
894
|
-
});
|
|
895
|
-
remaining = Number.parseInt(output.trim(), 10) || 0;
|
|
896
|
-
} catch {}
|
|
897
|
-
|
|
898
|
-
return { killed, remaining };
|
|
899
|
-
}
|
package/hub/lib/prompt-tmp.mjs
CHANGED
|
@@ -1,33 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// macOS `codex exec` 회귀 fix 보조 helper. codex CLI v0.130.0 의 macOS hook
|
|
4
|
-
// lifecycle (oh-my-codex/codex-native-hook.js) 이 매우 긴 argv inline prompt
|
|
5
|
-
// (4KB+) 를 받으면 SessionStart Failed 로 즉시 exit. PR #243 의 stdin EAGAIN
|
|
6
|
-
// async stream fix 후속 회귀로 추정.
|
|
7
|
-
//
|
|
8
|
-
// 대안: prompt 를 임시 파일에 쓰고 `< /tmp/...` stdin redirect 로 전달.
|
|
9
|
-
// Manual `echo prompt | codex exec ...` (stdin pipe) 가 정상 작동하는 fact 와
|
|
10
|
-
// 정합.
|
|
11
|
-
//
|
|
12
|
-
// cleanup 정책: best-effort. OS `/tmp` 가 자동 cleanup 하고, 이 모듈은
|
|
13
|
-
// 매 호출마다 unique path 사용 (timestamp + pid + counter). orphan 파일은
|
|
14
|
-
// 추후 doctor 가 prune.
|
|
1
|
+
// 프롬프트를 argv 로 넘기면 ps 에 보이므로 stdin 리다이렉트용 파일로 넘긴다.
|
|
2
|
+
// 파일은 매번 새 이름이고 정리는 OS 임시 디렉터리 정리에 맡긴다.
|
|
15
3
|
|
|
16
|
-
import {
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
18
5
|
import { join } from "node:path";
|
|
6
|
+
import { privateTmpDir } from "./private-tmp.mjs";
|
|
19
7
|
|
|
20
|
-
const PROMPT_TMP_DIR = join(tmpdir(), "triflux-codex-prompt");
|
|
21
8
|
let counter = 0;
|
|
22
9
|
|
|
23
10
|
export function writePromptToTmpFile(prompt) {
|
|
24
|
-
mkdirSync(PROMPT_TMP_DIR, { recursive: true });
|
|
25
11
|
const id = `${Date.now()}-${process.pid}-${counter++}`;
|
|
26
|
-
const file = join(
|
|
27
|
-
writeFileSync(file, String(prompt ?? ""), "utf8");
|
|
12
|
+
const file = join(privateTmpDir("triflux-codex-prompt"), `prompt-${id}.txt`);
|
|
13
|
+
writeFileSync(file, String(prompt ?? ""), { encoding: "utf8", mode: 0o600 });
|
|
28
14
|
return file;
|
|
29
15
|
}
|
|
30
|
-
|
|
31
|
-
export function getPromptTmpDir() {
|
|
32
|
-
return PROMPT_TMP_DIR;
|
|
33
|
-
}
|
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import * as childProcess from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
createWriteStream,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from "node:fs";
|
|
4
11
|
import { homedir } from "node:os";
|
|
5
12
|
import { join } from "node:path";
|
|
6
13
|
|
|
7
14
|
const LOG_DIR = join(homedir(), ".triflux", "logs");
|
|
15
|
+
const RETENTION_DAYS = 7;
|
|
8
16
|
const DEDUPE_WINDOW_MS = 5_000;
|
|
9
17
|
const RATE_WINDOW_MS = 1_000;
|
|
10
18
|
const DEFAULT_MAX_SPAWN_PER_SEC = 100;
|
|
19
|
+
const MAX_PLAIN_TRACE_VALUE = 64;
|
|
11
20
|
// multi-worker headless dispatch 가 1 초 안에 ~25-30+ psmux/tmux 명령을 호출한다.
|
|
12
21
|
// 2-worker dispatchBatch 가 default 30 limit 을 초과해 `rate_limit` throw 로 mac
|
|
13
22
|
// 호환성 회귀 (smoke test 발견, 2026-05-15). 100 으로 상향 — 폭주 안전망은
|
|
@@ -54,7 +63,8 @@ function ensureLogStream() {
|
|
|
54
63
|
return logStream;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
mkdirSync(LOG_DIR, { recursive: true });
|
|
66
|
+
mkdirSync(LOG_DIR, { recursive: true, mode: 0o700 });
|
|
67
|
+
restrictMode(LOG_DIR, 0o700);
|
|
58
68
|
|
|
59
69
|
if (logStream) {
|
|
60
70
|
try {
|
|
@@ -65,20 +75,115 @@ function ensureLogStream() {
|
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
logDay = day;
|
|
68
|
-
|
|
78
|
+
pruneOldTraceLogs(LOG_DIR, day);
|
|
79
|
+
logStream = createWriteStream(getLogPath(day), { flags: "a", mode: 0o600 });
|
|
80
|
+
restrictMode(getLogPath(day), 0o600);
|
|
69
81
|
logStream.on("error", () => {
|
|
70
82
|
/* ignore logging failures */
|
|
71
83
|
});
|
|
72
84
|
return logStream;
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
// 날짜별 파일이 끝없이 쌓이지 않게, 그날 첫 파일을 열 때 보관 기간이 지난 파일을 지운다.
|
|
88
|
+
export function pruneOldTraceLogs(logDir, today) {
|
|
89
|
+
const cutoff = new Date(Date.parse(today) - RETENTION_DAYS * 86_400_000)
|
|
90
|
+
.toISOString()
|
|
91
|
+
.slice(0, 10);
|
|
92
|
+
try {
|
|
93
|
+
for (const name of readdirSync(logDir)) {
|
|
94
|
+
const day = /^spawn-trace-(\d{4}-\d{2}-\d{2})\.jsonl$/u.exec(name)?.[1];
|
|
95
|
+
if (day && day < cutoff) rmSync(join(logDir, name), { force: true });
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 예전 버전이 0644 로 만든 로그도 좁힌다. Windows 는 chmod 가 의미 없어 실패를 무시한다.
|
|
103
|
+
function restrictMode(path, mode) {
|
|
104
|
+
try {
|
|
105
|
+
chmodSync(path, mode);
|
|
106
|
+
} catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function redacted(value) {
|
|
112
|
+
const digest = createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
113
|
+
return `<redacted len=${value.length} sha256=${digest}>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 프롬프트가 로그에 남지 않게, 공백이 있거나 긴 값은 길이와 해시만 남긴다.
|
|
117
|
+
export function redactTraceValue(value) {
|
|
118
|
+
if (typeof value !== "string") return value;
|
|
119
|
+
if (value.length <= MAX_PLAIN_TRACE_VALUE && !/\s/u.test(value)) return value;
|
|
120
|
+
return redacted(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const PROMPT_FLAGS = new Set(["--print", "--prompt", "--message", "--seed"]);
|
|
124
|
+
const SEND_KEYS_VALUE_FLAGS = new Set(["t", "N", "c"]);
|
|
125
|
+
const NAMED_KEY =
|
|
126
|
+
/^(Enter|Escape|Tab|BSpace|Space|Up|Down|Left|Right|[CM]-.)$/u;
|
|
127
|
+
|
|
128
|
+
// send-keys 옵션을 getopt 처럼 읽어 키 자리의 시작과 -l(문자 그대로 입력) 여부를 찾는다.
|
|
129
|
+
function parseSendKeys(args, from) {
|
|
130
|
+
let literal = false;
|
|
131
|
+
let i = from;
|
|
132
|
+
while (i < args.length && /^-./u.test(String(args[i]))) {
|
|
133
|
+
const flag = String(args[i]);
|
|
134
|
+
i += 1;
|
|
135
|
+
if (flag === "--") break;
|
|
136
|
+
for (let j = 1; j < flag.length; j += 1) {
|
|
137
|
+
if (flag[j] === "l") literal = true;
|
|
138
|
+
if (SEND_KEYS_VALUE_FLAGS.has(flag[j])) {
|
|
139
|
+
if (j === flag.length - 1) i += 1;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { keysFrom: i, literal };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 짧은 토큰도 키 입력과 프롬프트 자리에 오면 항상 가린다. 길이만 보면 짧은 비밀이 샌다.
|
|
148
|
+
// -l 이면 Enter 같은 이름도 글자 그대로 입력되므로 가린다.
|
|
149
|
+
function redactArgs(args) {
|
|
150
|
+
const sendKeysAt = args.indexOf("send-keys");
|
|
151
|
+
const { keysFrom, literal } =
|
|
152
|
+
sendKeysAt >= 0
|
|
153
|
+
? parseSendKeys(args, sendKeysAt + 1)
|
|
154
|
+
: { keysFrom: -1, literal: false };
|
|
155
|
+
return args.map((arg, index) => {
|
|
156
|
+
if (typeof arg !== "string") return arg;
|
|
157
|
+
const promptFlag = arg.split("=")[0];
|
|
158
|
+
if (PROMPT_FLAGS.has(promptFlag) && arg.includes("=")) {
|
|
159
|
+
return `${promptFlag}=${redacted(arg.slice(promptFlag.length + 1))}`;
|
|
160
|
+
}
|
|
161
|
+
const afterPromptFlag = PROMPT_FLAGS.has(args[index - 1]);
|
|
162
|
+
const isKey =
|
|
163
|
+
keysFrom >= 0 && index >= keysFrom && (literal || !NAMED_KEY.test(arg));
|
|
164
|
+
return afterPromptFlag || isKey ? redacted(arg) : redactTraceValue(arg);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function redactTraceEntry(entry) {
|
|
169
|
+
return {
|
|
170
|
+
...entry,
|
|
171
|
+
command: redactTraceValue(entry.command),
|
|
172
|
+
args: Array.isArray(entry.args) ? redactArgs(entry.args) : entry.args,
|
|
173
|
+
// execFileSync 오류 메시지에는 명령과 인자 전체가 들어간다.
|
|
174
|
+
...(typeof entry.error === "string"
|
|
175
|
+
? { error: redactTraceValue(entry.error) }
|
|
176
|
+
: {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
75
180
|
function appendTrace(data, { sync = false } = {}) {
|
|
76
|
-
const entry = {
|
|
181
|
+
const entry = redactTraceEntry({
|
|
77
182
|
ts: nowIso(),
|
|
78
183
|
session_id: process.env.TRIFLUX_SESSION_ID ?? null,
|
|
79
184
|
parent_pid: process.pid,
|
|
80
185
|
...data,
|
|
81
|
-
};
|
|
186
|
+
});
|
|
82
187
|
|
|
83
188
|
try {
|
|
84
189
|
ensureLogStream().write(`${JSON.stringify(entry)}\n`);
|
|
@@ -261,6 +366,7 @@ function trackChild(child, meta) {
|
|
|
261
366
|
child.once("error", (error) => {
|
|
262
367
|
finalize("error", {
|
|
263
368
|
error: error.message,
|
|
369
|
+
error_code: error.code ?? null,
|
|
264
370
|
});
|
|
265
371
|
});
|
|
266
372
|
|
|
@@ -532,6 +638,7 @@ export function execFileSync(file, args, options) {
|
|
|
532
638
|
signal: error?.signal ?? null,
|
|
533
639
|
duration_ms: Date.now() - startedAt,
|
|
534
640
|
error: error?.message,
|
|
641
|
+
error_code: error?.code ?? null,
|
|
535
642
|
sync: true,
|
|
536
643
|
},
|
|
537
644
|
{ sync: true },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// hub 조율 평면 타임아웃/TTL의 단일 기준값. 환경은 호출 시점에 읽는다.
|
|
2
|
+
import { readMachineProfile } from "../../scripts/lib/machine-profile.mjs";
|
|
2
3
|
|
|
3
4
|
export const MIN_DURATION_MS = 1_000;
|
|
4
5
|
export const MAX_DURATION_MS = 86_400_000;
|
|
@@ -10,22 +11,49 @@ export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
|
|
|
10
11
|
export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
|
|
11
12
|
export const DEFAULT_HARD_CEILING_SEC = 21_600;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// 셸(tfx-route.sh)과 같은 우선순위: 명시 env, 그다음 machine profile, 그다음 기본값.
|
|
15
|
+
function readSetting(env, name) {
|
|
16
|
+
if (env?.[name] !== undefined) return env[name];
|
|
17
|
+
return readMachineProfile({ env }).values[name];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readSeconds(env, name, fallbackSec, { allowZero = false } = {}) {
|
|
21
|
+
const raw = String(readSetting(env, name) ?? "").trim();
|
|
22
|
+
// 셸의 ${VAR:-기본값} 처럼 빈 값은 기본값이다. Number("") 가 0 이 되는 것을 막는다.
|
|
23
|
+
if (!raw) return fallbackSec;
|
|
24
|
+
const value = Number(raw);
|
|
25
|
+
const min = allowZero ? 0 : 1;
|
|
26
|
+
return Number.isInteger(value) && value >= min ? value : fallbackSec;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export function resolveStallInterventionMs(env = process.env) {
|
|
19
30
|
return (
|
|
20
|
-
|
|
31
|
+
readSeconds(env, "TFX_STALL_THRESHOLD", DEFAULT_STALL_INTERVENTION_SEC) *
|
|
21
32
|
1000
|
|
22
33
|
);
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
// 0 은 셸과 같이 상한 없음이다.
|
|
25
37
|
export function resolveHardCeilingMs(env = process.env) {
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const sec = readSeconds(
|
|
39
|
+
env,
|
|
40
|
+
"TFX_HARD_CEILING_SEC",
|
|
41
|
+
DEFAULT_HARD_CEILING_SEC,
|
|
42
|
+
{
|
|
43
|
+
allowZero: true,
|
|
44
|
+
},
|
|
28
45
|
);
|
|
46
|
+
return sec === 0 ? Number.POSITIVE_INFINITY : sec * 1000;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 셸 heartbeat 와 같은 값 해석. classify 는 무활동이어도 끝내지 않는다.
|
|
50
|
+
export function resolveStallKill(env = process.env) {
|
|
51
|
+
const value = String(readSetting(env, "TFX_STALL_KILL") || "kill")
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
if (["1", "on", "kill"].includes(value)) return "kill";
|
|
55
|
+
if (["intervene", "ladder"].includes(value)) return "intervene";
|
|
56
|
+
return "classify";
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
export function resolveWorkerLeaseTtlMs(env = process.env) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
resolveHardCeilingMs,
|
|
4
4
|
resolveStallInterventionMs,
|
|
5
|
+
resolveStallKill,
|
|
5
6
|
} from "./timeout-defaults.mjs";
|
|
6
7
|
|
|
7
8
|
export { resolveHardCeilingMs, resolveStallInterventionMs };
|
|
@@ -19,6 +20,7 @@ export function createActivityLifecycle({
|
|
|
19
20
|
enabled = isActivityLifecycleEnabled(),
|
|
20
21
|
interventionMs = resolveStallInterventionMs(),
|
|
21
22
|
hardCeilingMs = resolveHardCeilingMs(),
|
|
23
|
+
stallKill = resolveStallKill(),
|
|
22
24
|
maxInterventions = 1,
|
|
23
25
|
onIntervene,
|
|
24
26
|
now = Date.now,
|
|
@@ -77,6 +79,7 @@ export function createActivityLifecycle({
|
|
|
77
79
|
}
|
|
78
80
|
if (current - lastActivityAt < interventionMs || pendingIntervention)
|
|
79
81
|
return "";
|
|
82
|
+
if (stallKill === "classify") return "";
|
|
80
83
|
if (await intervene(context)) return "";
|
|
81
84
|
timeoutReason = "inactivity";
|
|
82
85
|
return timeoutReason;
|
package/hub/platform.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile, execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { privateTmpRoot } from "./lib/private-tmp.mjs";
|
|
4
5
|
|
|
5
6
|
export const IS_WINDOWS = process.platform === "win32";
|
|
6
7
|
export const IS_MAC = process.platform === "darwin";
|
|
@@ -209,7 +210,8 @@ export function pipePath(name, pid = process.pid, options = {}) {
|
|
|
209
210
|
return `\\\\.\\pipe\\${suffix}`;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
// 공용 /tmp 의 소켓은 다른 사용자가 접속하거나 선점할 수 있다. 짧은 경로를 위해 /tmp 를 기준으로 둔다.
|
|
214
|
+
const baseDir = options.tempDir || privateTmpRoot({ base: TEMP_DIR });
|
|
213
215
|
return path.posix.join(baseDir, `${suffix}.sock`);
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"executor": "codex",
|
|
3
|
+
"build-fixer": "codex",
|
|
4
|
+
"cleanup": "codex",
|
|
5
|
+
"deslop": "codex",
|
|
6
|
+
"debugger": "codex",
|
|
7
|
+
"deep-executor": "codex",
|
|
8
|
+
"architect": "codex",
|
|
9
|
+
"planner": "codex",
|
|
10
|
+
"critic": "codex",
|
|
11
|
+
"analyst": "codex",
|
|
12
|
+
"code-reviewer": "codex",
|
|
13
|
+
"security-reviewer": "codex",
|
|
14
|
+
"quality-reviewer": "codex",
|
|
15
|
+
"scientist": "codex",
|
|
16
|
+
"scientist-deep": "codex",
|
|
17
|
+
"document-specialist": "codex",
|
|
18
|
+
"spark": "codex",
|
|
19
|
+
"designer": "antigravity",
|
|
20
|
+
"writer": "antigravity",
|
|
21
|
+
"explore": "claude",
|
|
22
|
+
"verifier": "codex",
|
|
23
|
+
"test-engineer": "codex",
|
|
24
|
+
"qa-tester": "codex",
|
|
25
|
+
"codex": "codex",
|
|
26
|
+
"antigravity": "antigravity",
|
|
27
|
+
"agy": "antigravity",
|
|
28
|
+
"claude": "claude"
|
|
29
|
+
}
|
|
@@ -125,10 +125,12 @@ export async function resolveCodexRolloutFile({
|
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
// 프롬프트는 argv 에 두면 ps 에 보여서 넣지 않는다. 실행하는 쪽이 prompt 를 stdin 으로 넘긴다.
|
|
129
|
+
// claude -p 는 프롬프트 인자가 없으면, codex exec resume 은 `-` 를 받으면 stdin 을 읽는다.
|
|
130
|
+
export function buildResumeArgv({ cli, sessionId } = {}) {
|
|
129
131
|
if (!sessionId) return null;
|
|
130
|
-
if (cli === "claude") return ["claude", "-p", "--resume", sessionId
|
|
131
|
-
if (cli === "codex") return ["codex", "exec", "resume", sessionId,
|
|
132
|
+
if (cli === "claude") return ["claude", "-p", "--resume", sessionId];
|
|
133
|
+
if (cli === "codex") return ["codex", "exec", "resume", sessionId, "-"];
|
|
132
134
|
return null;
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -474,7 +476,7 @@ export function createInterventionLadder({
|
|
|
474
476
|
sessionId,
|
|
475
477
|
prompt: reinstructPrompt,
|
|
476
478
|
target,
|
|
477
|
-
argv: buildResumeArgv({ cli, sessionId
|
|
479
|
+
argv: buildResumeArgv({ cli, sessionId }),
|
|
478
480
|
});
|
|
479
481
|
return resumed?.ok === false
|
|
480
482
|
? { ok: false, detail: "resume_dispatch_failed" }
|