@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
package/hud/providers/codex.mjs
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readSync,
|
|
14
|
+
} from "node:fs";
|
|
7
15
|
import { join } from "node:path";
|
|
8
16
|
import {
|
|
9
17
|
CODEX_MIN_BUCKETS,
|
|
@@ -253,6 +261,110 @@ function mergeRateLimitSnapshots(snapshots, nowSec) {
|
|
|
253
261
|
return merged;
|
|
254
262
|
}
|
|
255
263
|
|
|
264
|
+
const EXTRA_BYTES_AFTER_FIRST_BUCKET = 64 * 1024;
|
|
265
|
+
const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
|
|
266
|
+
|
|
267
|
+
// 파일 끝에서 줄 단위로 거슬러 읽는다. 호출자가 멈추는 지점까지만 디스크를 읽는다.
|
|
268
|
+
// 0x0A 로만 나누므로 멀티바이트 UTF-8 이 청크 경계에서 깨지지 않는다.
|
|
269
|
+
// 파일 양끝의 공백은 문자열 trim() 과 같게 버리고 중간의 빈 줄은 유지해,
|
|
270
|
+
// 옛 content.trim().split("\n").reverse() 와 같은 줄을 낸다.
|
|
271
|
+
export function createReverseLineReader(
|
|
272
|
+
path,
|
|
273
|
+
chunkBytes = REVERSE_READ_CHUNK_BYTES,
|
|
274
|
+
) {
|
|
275
|
+
const fd = openSync(path, "r");
|
|
276
|
+
let pos;
|
|
277
|
+
try {
|
|
278
|
+
pos = fstatSync(fd).size;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
closeSync(fd);
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
let carryParts = []; // 아직 줄 시작을 못 만난 뒷부분 조각. 나중에 읽은(앞쪽) 조각이 뒤에 쌓인다
|
|
284
|
+
let queue = [];
|
|
285
|
+
let leadingBlank = -1; // 파일 맨 앞의 빈 줄 수. 끝까지 읽은 뒤에만 정해진다
|
|
286
|
+
let trimmingTail = true;
|
|
287
|
+
let yielded = false;
|
|
288
|
+
let emptyGiven = false;
|
|
289
|
+
|
|
290
|
+
function fill() {
|
|
291
|
+
const size = Math.min(chunkBytes, pos);
|
|
292
|
+
const chunk = Buffer.alloc(size);
|
|
293
|
+
pos -= size;
|
|
294
|
+
readSync(fd, chunk, 0, size, pos);
|
|
295
|
+
reader.bytesRead += size;
|
|
296
|
+
const pieces = [];
|
|
297
|
+
let start = 0;
|
|
298
|
+
for (
|
|
299
|
+
let i = chunk.indexOf(0x0a);
|
|
300
|
+
i !== -1;
|
|
301
|
+
i = chunk.indexOf(0x0a, start)
|
|
302
|
+
) {
|
|
303
|
+
pieces.push(chunk.subarray(start, i));
|
|
304
|
+
start = i + 1;
|
|
305
|
+
}
|
|
306
|
+
pieces.push(chunk.subarray(start));
|
|
307
|
+
if (pieces.length === 1 && pos > 0) {
|
|
308
|
+
carryParts.push(chunk);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const last = pieces.length - 1;
|
|
312
|
+
pieces[last] = Buffer.concat([pieces[last], ...carryParts.reverse()]);
|
|
313
|
+
carryParts = pos > 0 ? [pieces.shift()] : [];
|
|
314
|
+
queue = pieces.map((piece) => piece.toString("utf-8"));
|
|
315
|
+
if (pos === 0) {
|
|
316
|
+
leadingBlank = 0;
|
|
317
|
+
while (leadingBlank < queue.length && queue[leadingBlank].trim() === "") {
|
|
318
|
+
leadingBlank++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 옛 trim() 은 파일 맨 앞의 공백과 빈 줄을 지운다. 이미 읽은 줄만으로 알 수 있을 때만 적용해
|
|
324
|
+
// 앞선 큰 줄을 미리 읽지 않는다.
|
|
325
|
+
const isFileHead = () => leadingBlank >= 0 && queue.length <= leadingBlank;
|
|
326
|
+
|
|
327
|
+
const reader = {
|
|
328
|
+
bytesRead: 0,
|
|
329
|
+
// 줄이 더 없으면 null
|
|
330
|
+
next() {
|
|
331
|
+
let line = null;
|
|
332
|
+
while (queue.length > 0 || pos > 0) {
|
|
333
|
+
if (queue.length === 0) {
|
|
334
|
+
fill();
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
line = queue.pop();
|
|
338
|
+
const blank = line.trim() === "";
|
|
339
|
+
if (blank && (trimmingTail || isFileHead())) {
|
|
340
|
+
line = null;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
if (line === null) {
|
|
346
|
+
if (yielded || emptyGiven) return null;
|
|
347
|
+
emptyGiven = true; // 빈 파일도 옛 split 처럼 빈 줄 하나를 낸다
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
yielded = true;
|
|
351
|
+
if (trimmingTail) {
|
|
352
|
+
trimmingTail = false;
|
|
353
|
+
line = line.trimEnd();
|
|
354
|
+
}
|
|
355
|
+
return isFileHead() ? line.trimStart() : line;
|
|
356
|
+
},
|
|
357
|
+
close() {
|
|
358
|
+
try {
|
|
359
|
+
closeSync(fd);
|
|
360
|
+
} catch {
|
|
361
|
+
/* 이미 닫힘 */
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
return reader;
|
|
366
|
+
}
|
|
367
|
+
|
|
256
368
|
// ============================================================================
|
|
257
369
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
258
370
|
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
@@ -298,50 +410,65 @@ export function getCodexRateLimits({
|
|
|
298
410
|
const daySnapshots = [];
|
|
299
411
|
for (const file of files) {
|
|
300
412
|
try {
|
|
301
|
-
const content = readFileSync(join(sessDir, file), "utf-8");
|
|
302
413
|
const found = {};
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
414
|
+
const reader = createReverseLineReader(join(sessDir, file));
|
|
415
|
+
try {
|
|
416
|
+
let scanned = 0;
|
|
417
|
+
let bytesAfterFirstBucket = 0;
|
|
418
|
+
// 다음 줄을 읽기 전에 한도를 본다. 안 볼 줄을 만들려고 큰 줄을 읽지 않기 위해서다.
|
|
419
|
+
while (
|
|
420
|
+
scanned < maxLinesPerFile &&
|
|
421
|
+
bytesAfterFirstBucket < EXTRA_BYTES_AFTER_FIRST_BUCKET
|
|
422
|
+
) {
|
|
423
|
+
const line = reader.next();
|
|
424
|
+
if (line === null) break;
|
|
425
|
+
scanned++;
|
|
426
|
+
// 같은 시점의 다른 limit_id 는 인접해 기록된다. 첫 버킷을 찾은 뒤에는 일정 바이트만 더 보고 멈춰,
|
|
427
|
+
// 단일 버킷 계정에서 800줄(파일 거의 전체)을 끝까지 읽지 않는다.
|
|
428
|
+
if (Object.keys(found).length > 0) {
|
|
429
|
+
bytesAfterFirstBucket += Buffer.byteLength(line) + 1;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const evt = JSON.parse(line);
|
|
433
|
+
const rl = evt?.payload?.rate_limits;
|
|
434
|
+
if (rl?.limit_id && !found[rl.limit_id]) {
|
|
435
|
+
// window_minutes 기준으로 5h/1w 슬롯 정규화
|
|
436
|
+
const { primary, secondary } = normalizeBuckets(rl);
|
|
437
|
+
found[rl.limit_id] = {
|
|
438
|
+
limitId: rl.limit_id,
|
|
439
|
+
limitName: rl.limit_name,
|
|
440
|
+
primary,
|
|
441
|
+
secondary,
|
|
442
|
+
credits: rl.credits,
|
|
443
|
+
tokens: evt.payload?.info?.total_token_usage,
|
|
444
|
+
contextWindow: evt.payload?.info?.model_context_window,
|
|
445
|
+
timestamp: evt.timestamp,
|
|
446
|
+
};
|
|
447
|
+
} else if (
|
|
448
|
+
dayOffset <= 1 &&
|
|
449
|
+
!rl &&
|
|
450
|
+
evt?.payload?.info?.total_token_usage &&
|
|
451
|
+
!syntheticBucket
|
|
452
|
+
) {
|
|
453
|
+
// 2일 이내 token_count: 합성 버킷 (rate_limits가 null일 때 행 활성화용, stale 방지)
|
|
454
|
+
syntheticBucket = {
|
|
455
|
+
limitId: "codex",
|
|
456
|
+
limitName: "codex-session",
|
|
457
|
+
primary: null,
|
|
458
|
+
secondary: null,
|
|
459
|
+
credits: null,
|
|
460
|
+
tokens: evt.payload.info.total_token_usage,
|
|
461
|
+
contextWindow: evt.payload.info.model_context_window,
|
|
462
|
+
timestamp: evt.timestamp,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
/* 라인 파싱 실패 무시 */
|
|
340
467
|
}
|
|
341
|
-
|
|
342
|
-
/* 라인 파싱 실패 무시 */
|
|
468
|
+
if (Object.keys(found).length >= CODEX_MIN_BUCKETS) break;
|
|
343
469
|
}
|
|
344
|
-
|
|
470
|
+
} finally {
|
|
471
|
+
reader.close();
|
|
345
472
|
}
|
|
346
473
|
daySnapshots.push(...Object.values(found));
|
|
347
474
|
} catch {
|
package/hud/utils.mjs
CHANGED
|
@@ -86,10 +86,6 @@ export function padAnsiRight(text, width) {
|
|
|
86
86
|
return padAnsi(text, width, "right");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function padAnsiLeft(text, width) {
|
|
90
|
-
return padAnsi(text, width, "left");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
89
|
export function fitText(text, width) {
|
|
94
90
|
const t = String(text || "");
|
|
95
91
|
if (t.length <= width) return t;
|
|
@@ -186,20 +182,6 @@ export function formatTokenCount(n) {
|
|
|
186
182
|
return String(n);
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
export function getContextPercent(stdin) {
|
|
190
|
-
const nativePercent = stdin?.context_window?.used_percentage;
|
|
191
|
-
if (typeof nativePercent === "number" && Number.isFinite(nativePercent))
|
|
192
|
-
return clampPercent(nativePercent);
|
|
193
|
-
const usage = stdin?.context_window?.current_usage || {};
|
|
194
|
-
const totalTokens =
|
|
195
|
-
Number(usage.input_tokens || 0) +
|
|
196
|
-
Number(usage.cache_creation_input_tokens || 0) +
|
|
197
|
-
Number(usage.cache_read_input_tokens || 0);
|
|
198
|
-
const capacity = Number(stdin?.context_window?.context_window_size || 0);
|
|
199
|
-
if (!capacity || capacity <= 0) return 0;
|
|
200
|
-
return clampPercent((totalTokens / capacity) * 100);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
185
|
// 과거 리셋 시간 → 다음 주기로 순환하여 미래 시점 반환
|
|
204
186
|
// elapsed가 cycleMs의 정수배일 때 ceil은 target=now를 반환해 diff=0이 되므로
|
|
205
187
|
// floor+1로 항상 다음 사이클을 가리키도록 한다.
|
|
@@ -240,11 +222,6 @@ export function formatResetRemaining(isoOrUnix, cycleMs = 0) {
|
|
|
240
222
|
return `${totalHours}h${String(minutes).padStart(2, "0")}m`;
|
|
241
223
|
}
|
|
242
224
|
|
|
243
|
-
export function isResetPast(isoOrUnix) {
|
|
244
|
-
const date = parseResetDate(isoOrUnix);
|
|
245
|
-
return date != null && date.getTime() <= Date.now();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
225
|
export function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
|
|
249
226
|
const diffMs = getRemainingResetMs(isoOrUnix, cycleMs);
|
|
250
227
|
if (diffMs == null || diffMs <= 0) return "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflux/core",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.55.0",
|
|
4
4
|
"description": "triflux core — CLI routing, adapters. Zero native dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "hub/index.mjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./scripts/*": "./scripts/*"
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=22"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@iarna/toml": "^2.2.5"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
// 설정이 바뀔 때마다 백업이 쌓여 끝없이 늘었다. 같은 대상의 백업은 최근 몇 개만 남긴다.
|
|
12
|
+
export const BACKUP_KEEP = 5;
|
|
13
|
+
|
|
14
|
+
// 시각 suffix 만 받는다. label 이 더 긴 다른 백업(예: <label>-manual-...)은 남긴다.
|
|
15
|
+
const STAMP_SUFFIX = /^[\dT]+(?:-\d+)*$/i;
|
|
16
|
+
|
|
17
|
+
// 읽는 사이 다른 프로세스가 지웠으면 같은 것으로 보고 아래에서 다시 쓴다. 다른 읽기 오류는 그대로 던진다.
|
|
18
|
+
function sameContent(path, content) {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(path, "utf8") === content;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.code === "ENOENT") return true;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// `<파일>.<label>-<시각>` 으로 백업하고, 같은 label 의 오래된 백업을 지운다.
|
|
28
|
+
export function writeRotatedBackup(
|
|
29
|
+
path,
|
|
30
|
+
content,
|
|
31
|
+
{ label, suffix, mode, keep = BACKUP_KEEP },
|
|
32
|
+
) {
|
|
33
|
+
const directory = dirname(path);
|
|
34
|
+
const prefix = `${basename(path)}.${label}-`;
|
|
35
|
+
// 같은 시각 이름에 다른 원문이 이미 있으면 번호를 붙여 따로 남긴다.
|
|
36
|
+
let backupPath = join(directory, `${prefix}${suffix}`);
|
|
37
|
+
for (let i = 1; existsSync(backupPath); i++) {
|
|
38
|
+
if (sameContent(backupPath, content)) break;
|
|
39
|
+
backupPath = join(directory, `${prefix}${suffix}-${i}`);
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(backupPath))
|
|
42
|
+
writeFileSync(backupPath, content, { encoding: "utf8", mode });
|
|
43
|
+
const backups = readdirSync(directory)
|
|
44
|
+
.filter(
|
|
45
|
+
(name) =>
|
|
46
|
+
name.startsWith(prefix) && STAMP_SUFFIX.test(name.slice(prefix.length)),
|
|
47
|
+
)
|
|
48
|
+
.flatMap((name) => {
|
|
49
|
+
// 다른 프로세스가 같은 백업을 먼저 지웠을 수 있다.
|
|
50
|
+
try {
|
|
51
|
+
return [{ name, mtime: statSync(join(directory, name)).mtimeMs }];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.sort((a, b) => b.mtime - a.mtime || b.name.localeCompare(a.name));
|
|
57
|
+
for (const { name } of backups.slice(keep))
|
|
58
|
+
rmSync(join(directory, name), { force: true });
|
|
59
|
+
return backupPath;
|
|
60
|
+
}
|
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
mkdirSync,
|
|
4
4
|
readFileSync,
|
|
5
5
|
renameSync,
|
|
6
|
+
statSync,
|
|
6
7
|
writeFileSync,
|
|
7
8
|
} from "node:fs";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { dirname, join } from "node:path";
|
|
11
|
+
import { writeRotatedBackup } from "./backup-rotation.mjs";
|
|
10
12
|
|
|
11
13
|
const EFFORT_BY_SUFFIX = {
|
|
12
14
|
ultra: "ultra",
|
|
@@ -80,10 +82,6 @@ function readProfileScalar(raw, key) {
|
|
|
80
82
|
* naming convention when the file is absent. Returns the keys that resolve, in
|
|
81
83
|
* `key="value"` TOML-scalar form ready to be shell-quoted by the caller.
|
|
82
84
|
*
|
|
83
|
-
* NOTE: the Codex MCP transport does the structured-arg equivalent in
|
|
84
|
-
* hub/workers/codex-mcp.mjs (resolveCodexProfileConfig → model + config object);
|
|
85
|
-
* this returns the CLI `-c` string form for the same per-profile file.
|
|
86
|
-
*
|
|
87
85
|
* @param {string} profileName
|
|
88
86
|
* @param {{ codexHome?: string }} [opts]
|
|
89
87
|
* @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
|
|
@@ -230,7 +228,8 @@ export function sanitizeCodexProfileConfig(
|
|
|
230
228
|
skippedProfileFiles.push(profileName);
|
|
231
229
|
continue;
|
|
232
230
|
}
|
|
233
|
-
|
|
231
|
+
// 옮겨 온 프로필 본문에 MCP env 키가 있을 수 있다.
|
|
232
|
+
writeFileSync(profilePath, body, { encoding: "utf8", mode: 0o600 });
|
|
234
233
|
migratedProfiles.push(profileName);
|
|
235
234
|
}
|
|
236
235
|
}
|
|
@@ -254,6 +253,15 @@ function stamp(now = () => new Date()) {
|
|
|
254
253
|
.replace("T", "-");
|
|
255
254
|
}
|
|
256
255
|
|
|
256
|
+
// config.toml 에는 MCP env 키가 들어갈 수 있다. 다시 쓸 때 원본 권한을 지키고 없으면 0600 으로 만든다.
|
|
257
|
+
export function privateFileMode(path) {
|
|
258
|
+
try {
|
|
259
|
+
return statSync(path).mode & 0o777;
|
|
260
|
+
} catch {
|
|
261
|
+
return 0o600;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
257
265
|
export function sanitizeCodexProfileConfigFile(
|
|
258
266
|
configPath,
|
|
259
267
|
{
|
|
@@ -268,7 +276,12 @@ export function sanitizeCodexProfileConfigFile(
|
|
|
268
276
|
const source = readFileSync(configPath, "utf8");
|
|
269
277
|
const sanitized = sanitizeCodexProfileConfig(source, { codexHome });
|
|
270
278
|
if (!sanitized.changed) return sanitized;
|
|
271
|
-
|
|
279
|
+
const mode = privateFileMode(configPath);
|
|
280
|
+
writeRotatedBackup(configPath, source, {
|
|
281
|
+
label: backupPrefix,
|
|
282
|
+
suffix: stamp(now),
|
|
283
|
+
mode,
|
|
284
|
+
});
|
|
272
285
|
// Atomic replace. config.toml is shared global state: a codex session may be
|
|
273
286
|
// reading it while another entry point (tfx-route, the codex session hook, or
|
|
274
287
|
// a parallel swarm worker) sanitizes it. Write the sanitized content to a
|
|
@@ -278,7 +291,7 @@ export function sanitizeCodexProfileConfigFile(
|
|
|
278
291
|
// without corruption (the content is identical anyway since the migration is
|
|
279
292
|
// deterministic). The pid keeps temp names unique across separate processes.
|
|
280
293
|
const tmpPath = `${configPath}.tmp-${process.pid}-${stamp(now)}`;
|
|
281
|
-
writeFileSync(tmpPath, sanitized.toml, "utf8");
|
|
294
|
+
writeFileSync(tmpPath, sanitized.toml, { encoding: "utf8", mode });
|
|
282
295
|
renameSync(tmpPath, configPath);
|
|
283
296
|
return sanitized;
|
|
284
297
|
}
|
|
@@ -58,9 +58,8 @@ recover_codex_stdout() {
|
|
|
58
58
|
|
|
59
59
|
# ── MCP transport 채널 사망 판정 ──
|
|
60
60
|
# Codex MCP transport 채널이 실행 중 죽으면 codex exec 가 exit 0 + 빈 stdout 으로
|
|
61
|
-
# 끝나는 변형이
|
|
62
|
-
#
|
|
63
|
-
# 검출한다. 아우터 가드가 exit 0 이어도 결과를 실패로 승격하는 근거로 쓴다.
|
|
61
|
+
# 끝나는 변형이 있다. stderr 의 하드 크래시 서명으로 이 사망을 검출한다.
|
|
62
|
+
# 아우터 가드가 exit 0 이어도 결과를 실패로 승격하는 근거로 쓴다.
|
|
64
63
|
# 인자: [stderr_log] (미지정 시 $STDERR_LOG). 반환: 0=크래시 서명 있음, 1=없음.
|
|
65
64
|
codex_stdout_transport_crashed() {
|
|
66
65
|
local stderr_log="${1:-${STDERR_LOG:-}}"
|