triflux 2.4.7 → 3.0.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/README.ko.md +9 -7
- package/README.md +9 -7
- package/bin/tfx-doctor.mjs +1 -1
- package/bin/tfx-setup.mjs +1 -1
- package/bin/triflux.mjs +25 -13
- package/hud/hud-qos-status.mjs +221 -108
- package/package.json +1 -1
- package/scripts/cli-route.sh +2 -967
- package/scripts/mcp-check.mjs +2 -2
- package/scripts/notion-read.mjs +104 -49
- package/scripts/setup.mjs +10 -5
- package/scripts/tfx-batch-stats.mjs +96 -0
- package/scripts/tfx-route-post.mjs +366 -0
- package/scripts/tfx-route.sh +448 -0
- package/skills/tfx-auto/SKILL.md +31 -31
- package/skills/tfx-codex/SKILL.md +1 -1
- package/skills/tfx-doctor/SKILL.md +2 -2
- package/skills/tfx-gemini/SKILL.md +1 -1
- package/skills/tfx-setup/SKILL.md +1 -1
package/scripts/mcp-check.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// MCP 인벤토리 — 백그라운드 비동기 실행
|
|
3
|
-
// Codex/Gemini의 MCP 서버 상태를 캐싱하여
|
|
3
|
+
// Codex/Gemini의 MCP 서버 상태를 캐싱하여 tfx-route.sh가 동적 힌트 생성에 사용
|
|
4
4
|
//
|
|
5
5
|
// 출력: ~/.claude/cache/mcp-inventory.json
|
|
6
|
-
// 사용:
|
|
6
|
+
// 사용: tfx-route.sh의 get_mcp_hint()에서 읽음
|
|
7
7
|
|
|
8
8
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
9
9
|
import { join } from "path";
|
package/scripts/notion-read.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// notion-read.mjs v1.
|
|
2
|
+
// notion-read.mjs v1.2 — Notion 대형 페이지 리더 (Codex/Gemini/Claude MCP 위임)
|
|
3
3
|
//
|
|
4
4
|
// Codex/Gemini/Claude CLI에 설치된 Notion MCP를 활용하여 대형 페이지를 마크다운으로 추출.
|
|
5
5
|
// 폴백 체인: Codex(무료) → Gemini(무료) → Claude(최후) → 에러
|
|
6
|
+
// 이관 모드(--delegate): Claude(notion-guest 우선) 단독 실행 + 결과 파일 저장
|
|
6
7
|
//
|
|
7
8
|
// 사용법:
|
|
8
9
|
// node notion-read.mjs <notion-url-or-page-id> [옵션]
|
|
@@ -14,27 +15,28 @@
|
|
|
14
15
|
// --cli, -c <codex|gemini> CLI 강제 지정 (기본: 자동 + 폴백)
|
|
15
16
|
// --depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
|
|
16
17
|
// --guest notion-guest 통합 사용 (기본: notion)
|
|
18
|
+
// --delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
|
|
17
19
|
|
|
18
|
-
import { execSync } from
|
|
19
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from
|
|
20
|
-
import { join, dirname } from
|
|
21
|
-
import { homedir, tmpdir } from
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';
|
|
22
|
+
import { join, dirname, resolve } from 'path';
|
|
23
|
+
import { homedir, tmpdir } from 'os';
|
|
22
24
|
|
|
23
|
-
const VERSION =
|
|
24
|
-
const CLAUDE_DIR = join(homedir(),
|
|
25
|
-
const MCP_CACHE = join(CLAUDE_DIR,
|
|
26
|
-
const LOG_FILE = join(CLAUDE_DIR,
|
|
27
|
-
const ACC_FILE = join(CLAUDE_DIR,
|
|
25
|
+
const VERSION = '1.2';
|
|
26
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
27
|
+
const MCP_CACHE = join(CLAUDE_DIR, 'cache', 'mcp-inventory.json');
|
|
28
|
+
const LOG_FILE = join(CLAUDE_DIR, 'logs', 'tfx-route-stats.jsonl');
|
|
29
|
+
const ACC_FILE = join(CLAUDE_DIR, 'cache', 'sv-accumulator.json');
|
|
28
30
|
|
|
29
31
|
// ── ANSI 색상 ──
|
|
30
|
-
const AMBER =
|
|
31
|
-
const GREEN =
|
|
32
|
-
const RED =
|
|
33
|
-
const YELLOW =
|
|
34
|
-
const DIM =
|
|
35
|
-
const BOLD =
|
|
36
|
-
const RESET =
|
|
37
|
-
const GRAY =
|
|
32
|
+
const AMBER = '\x1b[38;5;214m';
|
|
33
|
+
const GREEN = '\x1b[38;5;82m';
|
|
34
|
+
const RED = '\x1b[38;5;196m';
|
|
35
|
+
const YELLOW = '\x1b[33m';
|
|
36
|
+
const DIM = '\x1b[2m';
|
|
37
|
+
const BOLD = '\x1b[1m';
|
|
38
|
+
const RESET = '\x1b[0m';
|
|
39
|
+
const GRAY = '\x1b[38;5;245m';
|
|
38
40
|
|
|
39
41
|
// ── URL 파싱 ──
|
|
40
42
|
function parseNotionUrl(input) {
|
|
@@ -165,24 +167,24 @@ ${mcpServer} MCP 서버의 도구를 사용하라.
|
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
// ── CLI 실행 (임시 파일 + execSync — Windows .cmd 호환) ──
|
|
168
|
-
function runWithCli(cliType, prompt, timeout) {
|
|
169
|
-
const cliName = cliType ===
|
|
170
|
+
function runWithCli(cliType, prompt, timeout, runMode = 'fg') {
|
|
171
|
+
const cliName = cliType === 'claude' ? 'claude' : cliType === 'codex' ? 'codex' : 'gemini';
|
|
170
172
|
if (!cliExists(cliName)) {
|
|
171
|
-
return { success: false, output:
|
|
173
|
+
return { success: false, output: '', error: `${cliType} CLI 미설치`, cli: cliType };
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
// 프롬프트를 임시 파일에 저장 (shell escaping 회피)
|
|
175
177
|
const promptFile = join(tmpdir(), `notion-prompt-${Date.now()}.md`);
|
|
176
|
-
writeFileSync(promptFile, prompt,
|
|
177
|
-
const promptPath = promptFile.replace(/\\/g,
|
|
178
|
+
writeFileSync(promptFile, prompt, 'utf8');
|
|
179
|
+
const promptPath = promptFile.replace(/\\/g, '/');
|
|
178
180
|
|
|
179
181
|
// CLI에 전달할 짧은 메타 프롬프트
|
|
180
182
|
const metaPrompt = `Read the file at ${promptPath} and execute all instructions in it exactly as described. Output only the final markdown result.`;
|
|
181
183
|
|
|
182
184
|
let cmd;
|
|
183
|
-
if (cliType ===
|
|
185
|
+
if (cliType === 'codex') {
|
|
184
186
|
cmd = `codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "${metaPrompt}"`;
|
|
185
|
-
} else if (cliType ===
|
|
187
|
+
} else if (cliType === 'gemini') {
|
|
186
188
|
cmd = `gemini -m gemini-3-flash-preview -y --allowed-mcp-server-names notion,notion-guest --prompt "${metaPrompt}"`;
|
|
187
189
|
} else {
|
|
188
190
|
// Claude CLI — print 모드 (MCP 도구 자동 접근)
|
|
@@ -192,16 +194,16 @@ function runWithCli(cliType, prompt, timeout) {
|
|
|
192
194
|
console.error(`${AMBER}▸${RESET} ${cliType}로 실행 중... (timeout: ${timeout}s)`);
|
|
193
195
|
const startTime = Date.now();
|
|
194
196
|
|
|
195
|
-
let stdout =
|
|
196
|
-
let stderr =
|
|
197
|
+
let stdout = '';
|
|
198
|
+
let stderr = '';
|
|
197
199
|
let exitCode = 0;
|
|
198
200
|
|
|
199
201
|
try {
|
|
200
202
|
stdout = execSync(cmd, {
|
|
201
|
-
encoding:
|
|
203
|
+
encoding: 'utf8',
|
|
202
204
|
timeout: (timeout + 30) * 1000,
|
|
203
205
|
maxBuffer: 10 * 1024 * 1024,
|
|
204
|
-
stdio: [
|
|
206
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
205
207
|
cwd: process.cwd(),
|
|
206
208
|
});
|
|
207
209
|
} catch (e) {
|
|
@@ -216,7 +218,7 @@ function runWithCli(cliType, prompt, timeout) {
|
|
|
216
218
|
try { unlinkSync(promptFile); } catch {}
|
|
217
219
|
|
|
218
220
|
// 실행 로그 기록
|
|
219
|
-
logExecution(cliType, exitCode, elapsed, timeout, stderr);
|
|
221
|
+
logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode);
|
|
220
222
|
|
|
221
223
|
if (exitCode === 0 && stdout) {
|
|
222
224
|
return { success: true, output: stdout, cli: cliType, elapsed };
|
|
@@ -262,31 +264,31 @@ function cleanCodexOutput(raw) {
|
|
|
262
264
|
return texts.join("\n");
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
// ── 실행 로그 (
|
|
266
|
-
function logExecution(cliType, exitCode, elapsed, timeout, stderr) {
|
|
267
|
+
// ── 실행 로그 (tfx-route.sh 호환) ──
|
|
268
|
+
function logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode = 'fg') {
|
|
267
269
|
try {
|
|
268
270
|
const logDir = dirname(LOG_FILE);
|
|
269
271
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
270
272
|
|
|
271
273
|
const ts = new Date().toISOString();
|
|
272
|
-
const status = exitCode === 0 ?
|
|
274
|
+
const status = exitCode === 0 ? 'success' : exitCode === 124 ? 'timeout' : 'failed';
|
|
273
275
|
const entry = JSON.stringify({
|
|
274
276
|
ts,
|
|
275
|
-
agent:
|
|
277
|
+
agent: 'notion-read',
|
|
276
278
|
cli: cliType,
|
|
277
|
-
effort: cliType ===
|
|
278
|
-
run_mode:
|
|
279
|
-
opus_oversight:
|
|
279
|
+
effort: cliType === 'codex' ? 'high' : cliType === 'claude' ? 'sonnet' : 'flash',
|
|
280
|
+
run_mode: runMode,
|
|
281
|
+
opus_oversight: 'false',
|
|
280
282
|
status,
|
|
281
283
|
exit_code: exitCode,
|
|
282
284
|
elapsed_sec: elapsed,
|
|
283
285
|
timeout_sec: timeout,
|
|
284
|
-
mcp_profile:
|
|
286
|
+
mcp_profile: runMode === 'delegate' ? 'notion-guest' : 'notion',
|
|
285
287
|
input_tokens: 0,
|
|
286
288
|
output_tokens: 0,
|
|
287
289
|
total_tokens: 0,
|
|
288
290
|
});
|
|
289
|
-
appendFileSync(LOG_FILE, entry +
|
|
291
|
+
appendFileSync(LOG_FILE, entry + '\n');
|
|
290
292
|
} catch {}
|
|
291
293
|
}
|
|
292
294
|
|
|
@@ -309,6 +311,7 @@ function main() {
|
|
|
309
311
|
--depth, -d <n> 중첩 블록 최대 깊이 (기본: 3)
|
|
310
312
|
--comments 블록/페이지 댓글 포함
|
|
311
313
|
--guest notion-guest 통합 사용
|
|
314
|
+
--delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
|
|
312
315
|
|
|
313
316
|
${BOLD}폴백 체인${RESET}
|
|
314
317
|
Codex(무료) → Gemini(무료) → Claude(최후) → 에러
|
|
@@ -318,6 +321,8 @@ function main() {
|
|
|
318
321
|
tfx notion-read abc123def456... --output page.md --comments
|
|
319
322
|
tfx notion-read abc123def456... --cli gemini --timeout 900
|
|
320
323
|
tfx notion-read abc123def456... --guest --comments
|
|
324
|
+
tfx notion-read abc123def456... --delegate
|
|
325
|
+
tfx notion-read abc123def456... --delegate --output .notion-cache/page.md
|
|
321
326
|
`);
|
|
322
327
|
return;
|
|
323
328
|
}
|
|
@@ -330,6 +335,7 @@ function main() {
|
|
|
330
335
|
let depth = 3;
|
|
331
336
|
let useGuest = false;
|
|
332
337
|
let includeComments = false;
|
|
338
|
+
let delegateMode = false;
|
|
333
339
|
|
|
334
340
|
for (let i = 1; i < args.length; i++) {
|
|
335
341
|
switch (args[i]) {
|
|
@@ -355,6 +361,9 @@ function main() {
|
|
|
355
361
|
case "--comments":
|
|
356
362
|
includeComments = true;
|
|
357
363
|
break;
|
|
364
|
+
case '--delegate':
|
|
365
|
+
delegateMode = true;
|
|
366
|
+
break;
|
|
358
367
|
}
|
|
359
368
|
}
|
|
360
369
|
|
|
@@ -368,7 +377,62 @@ function main() {
|
|
|
368
377
|
console.error(
|
|
369
378
|
`${AMBER}▸${RESET} 페이지: ${parsed.pageId}${parsed.blockId ? ` (블록: ${parsed.blockId})` : ""}`,
|
|
370
379
|
);
|
|
371
|
-
console.error(`${GRAY} 통합: ${useGuest ?
|
|
380
|
+
console.error(`${GRAY} 통합: ${delegateMode ? 'notion-guest(우선)' : useGuest ? 'notion-guest' : 'notion'} | 깊이: ${depth} | 댓글: ${includeComments ? 'O' : 'X'} | 타임아웃: ${timeout}s${RESET}`);
|
|
381
|
+
|
|
382
|
+
// 프롬프트 생성
|
|
383
|
+
const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
|
|
384
|
+
// Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
|
|
385
|
+
const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
|
|
386
|
+
.replace(
|
|
387
|
+
'notion MCP 서버의 도구를 사용하라.',
|
|
388
|
+
'가능하면 notion-guest MCP 서버를 먼저 사용하라. 실패하면 notion MCP 서버를 사용하라.',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// delegate 모드: Claude 단독 + notion-guest 우선 + 파일 저장
|
|
392
|
+
if (delegateMode) {
|
|
393
|
+
console.error(`${AMBER}▸${RESET} delegate 모드 활성화: Claude로 notion-guest 우선 접근`);
|
|
394
|
+
|
|
395
|
+
const delegatePrompt = `${claudePrompt}
|
|
396
|
+
|
|
397
|
+
### delegate 모드 추가 지시
|
|
398
|
+
- notion-guest MCP 서버를 최우선으로 먼저 시도하라.
|
|
399
|
+
- notion-guest가 실패하거나 미구성일 때만 notion 서버로 폴백하라.
|
|
400
|
+
- 도구 호출 결과를 바탕으로 최종 마크다운만 출력하라.`;
|
|
401
|
+
|
|
402
|
+
const delegateResult = runWithCli('claude', delegatePrompt, timeout, 'delegate');
|
|
403
|
+
if (!delegateResult.success) {
|
|
404
|
+
console.error(`${RED}✗${RESET} delegate 모드 실패: ${delegateResult.error}`);
|
|
405
|
+
if (delegateResult.stderr) {
|
|
406
|
+
console.error(`${GRAY} stderr: ${delegateResult.stderr.slice(0, 250)}${RESET}`);
|
|
407
|
+
}
|
|
408
|
+
console.error(`${GRAY} 대안: --delegate 없이 실행해 기존 폴백 체인을 사용하세요.${RESET}`);
|
|
409
|
+
console.error(`${GRAY} 예: tfx notion-read ${parsed.pageId} --comments${RESET}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const delegateOutput = delegateResult.output.trim();
|
|
414
|
+
const isDelegateFailureOutput =
|
|
415
|
+
(delegateOutput.includes('조회 실패') || delegateOutput.includes('읽기 실패') || delegateOutput.includes('not_found')) &&
|
|
416
|
+
delegateOutput.length < 500;
|
|
417
|
+
|
|
418
|
+
if (delegateOutput.length <= 100 || isDelegateFailureOutput) {
|
|
419
|
+
console.error(`${RED}✗${RESET} delegate 모드 실패: Claude 결과가 비정상적입니다.`);
|
|
420
|
+
console.error(`${GRAY} 대안: --delegate 없이 실행해 Codex/Gemini/Claude 폴백 체인을 사용하세요.${RESET}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const delegateTarget = outputFile || join('.notion-cache', `${parsed.pageId}.md`);
|
|
425
|
+
const delegateDir = dirname(delegateTarget);
|
|
426
|
+
if (delegateDir && delegateDir !== '.' && !existsSync(delegateDir)) {
|
|
427
|
+
mkdirSync(delegateDir, { recursive: true });
|
|
428
|
+
}
|
|
429
|
+
writeFileSync(delegateTarget, delegateOutput, 'utf8');
|
|
430
|
+
|
|
431
|
+
const savedPath = resolve(delegateTarget);
|
|
432
|
+
console.error(`${GREEN}✓${RESET} delegate 결과 저장: ${savedPath}`);
|
|
433
|
+
console.error(`${GRAY} 후속 작업 참조 경로: ${savedPath}${RESET}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
372
436
|
|
|
373
437
|
// MCP 가용성 확인
|
|
374
438
|
const mcpAvail = getNotionMcpClis(useGuest);
|
|
@@ -400,15 +464,6 @@ function main() {
|
|
|
400
464
|
cliOrder.push("claude");
|
|
401
465
|
}
|
|
402
466
|
|
|
403
|
-
// 프롬프트 생성
|
|
404
|
-
const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
|
|
405
|
-
// Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
|
|
406
|
-
const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
|
|
407
|
-
.replace(
|
|
408
|
-
"404 에러가 나면 notion-guest 서버로 재시도하라.",
|
|
409
|
-
"404 에러가 나면 반드시 notion-guest 서버로 재시도하라. notion, notion-guest, claude_ai_Notion 등 사용 가능한 모든 Notion MCP 서버를 시도하라.",
|
|
410
|
-
);
|
|
411
|
-
|
|
412
467
|
// 실행 + 폴백
|
|
413
468
|
let lastResult = null;
|
|
414
469
|
let notionAccessFailed = false;
|
package/scripts/setup.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// triflux 세션 시작 시 자동 설정 스크립트
|
|
3
|
-
// -
|
|
3
|
+
// - tfx-route.sh를 ~/.claude/scripts/에 동기화
|
|
4
4
|
// - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
|
|
5
5
|
// - skills/를 ~/.claude/skills/에 동기화
|
|
6
6
|
|
|
@@ -15,9 +15,14 @@ const CLAUDE_DIR = join(homedir(), ".claude");
|
|
|
15
15
|
|
|
16
16
|
const SYNC_MAP = [
|
|
17
17
|
{
|
|
18
|
-
src: join(PLUGIN_ROOT, "scripts", "
|
|
19
|
-
dst: join(CLAUDE_DIR, "scripts", "
|
|
20
|
-
label: "
|
|
18
|
+
src: join(PLUGIN_ROOT, "scripts", "tfx-route.sh"),
|
|
19
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
20
|
+
label: "tfx-route.sh",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
src: join(PLUGIN_ROOT, "scripts", "tfx-route-post.mjs"),
|
|
24
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
25
|
+
label: "tfx-route-post.mjs",
|
|
21
26
|
},
|
|
22
27
|
{
|
|
23
28
|
src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
|
|
@@ -212,7 +217,7 @@ ${B}╔════════════════════════
|
|
|
212
217
|
${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
|
|
213
218
|
${B}╚═══════════════════════════════════════════════╝${R}
|
|
214
219
|
|
|
215
|
-
${G}✓${R}
|
|
220
|
+
${G}✓${R} tfx-route.sh → ~/.claude/scripts/
|
|
216
221
|
${G}✓${R} hud-qos-status → ~/.claude/hud/
|
|
217
222
|
${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
|
|
218
223
|
${G}✓${R} HUD statusLine → settings.json
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tfx-batch-stats.mjs v1.0 — batch-events.jsonl 소비자
|
|
3
|
+
//
|
|
4
|
+
// tfx-route-post.mjs가 기록한 AIMD 이벤트를 읽어서:
|
|
5
|
+
// 1. 에이전트별 성공/실패/타임아웃 통계 집계
|
|
6
|
+
// 2. AIMD(Additive Increase / Multiplicative Decrease) batch_size 계산
|
|
7
|
+
//
|
|
8
|
+
// 사용법:
|
|
9
|
+
// node tfx-batch-stats.mjs stats [--recent] 에이전트별 통계 (--recent: 30분)
|
|
10
|
+
// node tfx-batch-stats.mjs batch 현재 권장 batch_size
|
|
11
|
+
// node tfx-batch-stats.mjs agent <name> 특정 에이전트 통계
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
|
|
17
|
+
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
18
|
+
const EVENTS_FILE = join(CACHE_DIR, "batch-events.jsonl");
|
|
19
|
+
|
|
20
|
+
// AIMD 파라미터
|
|
21
|
+
const AIMD_INITIAL = 3;
|
|
22
|
+
const AIMD_MIN = 1;
|
|
23
|
+
const AIMD_MAX = 10;
|
|
24
|
+
const AIMD_INC = 1; // 성공 시 +1
|
|
25
|
+
const AIMD_DEC = 0.5; // 실패 시 ×0.5
|
|
26
|
+
const WINDOW_MS = 30 * 60 * 1000; // 30분 윈도우
|
|
27
|
+
|
|
28
|
+
// ── 이벤트 읽기 ──
|
|
29
|
+
export function readBatchEvents(opts = {}) {
|
|
30
|
+
const { sinceMs = 0, agent = null } = opts;
|
|
31
|
+
try {
|
|
32
|
+
const lines = readFileSync(EVENTS_FILE, "utf-8").trim().split("\n").filter(Boolean);
|
|
33
|
+
return lines
|
|
34
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
35
|
+
.filter((e) => e && (!sinceMs || e.ts >= sinceMs) && (!agent || e.agent === agent));
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 에이전트별 통계 ──
|
|
42
|
+
export function getAgentStats(opts = {}) {
|
|
43
|
+
const events = readBatchEvents(opts);
|
|
44
|
+
const stats = {};
|
|
45
|
+
|
|
46
|
+
for (const ev of events) {
|
|
47
|
+
if (!stats[ev.agent]) stats[ev.agent] = { success: 0, fail: 0, timeout: 0, total: 0 };
|
|
48
|
+
const s = stats[ev.agent];
|
|
49
|
+
if (ev.result === "success" || ev.result === "success_with_warnings") s.success++;
|
|
50
|
+
else if (ev.result === "timeout") s.timeout++;
|
|
51
|
+
else s.fail++;
|
|
52
|
+
s.total++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const s of Object.values(stats)) {
|
|
56
|
+
s.successRate = s.total > 0 ? +(s.success / s.total).toFixed(2) : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { total: events.length, agents: stats };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── AIMD batch_size 계산 ──
|
|
63
|
+
// 최근 윈도우 이벤트를 순회하며 성공 시 +1, 실패 시 ×0.5
|
|
64
|
+
export function getAimdBatchSize() {
|
|
65
|
+
const since = Date.now() - WINDOW_MS;
|
|
66
|
+
const events = readBatchEvents({ sinceMs: since });
|
|
67
|
+
if (events.length === 0) return AIMD_INITIAL;
|
|
68
|
+
|
|
69
|
+
let batch = AIMD_INITIAL;
|
|
70
|
+
for (const ev of events) {
|
|
71
|
+
if (ev.result === "success" || ev.result === "success_with_warnings") {
|
|
72
|
+
batch = Math.min(AIMD_MAX, batch + AIMD_INC);
|
|
73
|
+
} else {
|
|
74
|
+
batch = Math.max(AIMD_MIN, Math.floor(batch * AIMD_DEC));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return batch;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── CLI 진입점 ──
|
|
81
|
+
const scriptName = process.argv[1] || "";
|
|
82
|
+
if (scriptName.endsWith("tfx-batch-stats.mjs")) {
|
|
83
|
+
const cmd = process.argv[2] || "stats";
|
|
84
|
+
const recent = process.argv.includes("--recent");
|
|
85
|
+
const sinceMs = recent ? Date.now() - WINDOW_MS : 0;
|
|
86
|
+
|
|
87
|
+
if (cmd === "batch") {
|
|
88
|
+
console.log(JSON.stringify({ batchSize: getAimdBatchSize(), window: "30m" }));
|
|
89
|
+
} else if (cmd === "agent") {
|
|
90
|
+
const name = process.argv[3];
|
|
91
|
+
if (!name) { console.error("에이전트명 필수: node tfx-batch-stats.mjs agent executor"); process.exit(1); }
|
|
92
|
+
console.log(JSON.stringify(getAgentStats({ sinceMs, agent: name }), null, 2));
|
|
93
|
+
} else {
|
|
94
|
+
console.log(JSON.stringify(getAgentStats({ sinceMs }), null, 2));
|
|
95
|
+
}
|
|
96
|
+
}
|