triflux 4.2.0 → 4.2.2

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.
@@ -355,7 +355,7 @@ function readJson(filePath, fallback) {
355
355
  function writeJsonSafe(filePath, data) {
356
356
  try {
357
357
  mkdirSync(dirname(filePath), { recursive: true });
358
- writeFileSync(filePath, JSON.stringify(data));
358
+ writeFileSync(filePath, JSON.stringify(data), { mode: 0o600 });
359
359
  } catch { /* 쓰기 실패 무시 */ }
360
360
  }
361
361
 
@@ -731,7 +731,16 @@ function readClaudeUsageSnapshot() {
731
731
  return { data: stripStaleResets(cache.data), shouldRefresh: ageMs >= backoffMs };
732
732
  }
733
733
  const isFresh = ageMs < getClaudeUsageStaleMs();
734
- return { data: cache.data, shouldRefresh: !isFresh };
734
+ // resets_at이 지난 윈도우의 percent를 0으로 보정 (stale 캐시 방지)
735
+ const data = { ...cache.data };
736
+ const now = Date.now();
737
+ if (data.fiveHourResetsAt && new Date(data.fiveHourResetsAt).getTime() <= now) {
738
+ data.fiveHourPercent = 0;
739
+ }
740
+ if (data.weeklyResetsAt && new Date(data.weeklyResetsAt).getTime() <= now) {
741
+ data.weeklyPercent = 0;
742
+ }
743
+ return { data, shouldRefresh: !isFresh };
735
744
  }
736
745
 
737
746
  // 2차: 에러 backoff — 최근 에러 시 재시도 억제 (무한 spawn 방지)
@@ -996,6 +1005,22 @@ function getGeminiEmail() {
996
1005
  } catch { return null; }
997
1006
  }
998
1007
 
1008
+ // resets_at이 지난 윈도우의 used_percent를 0으로 보정
1009
+ function expireStaleCodexBuckets(buckets) {
1010
+ if (!buckets) return buckets;
1011
+ const nowSec = Math.floor(Date.now() / 1000);
1012
+ for (const bucket of Object.values(buckets)) {
1013
+ if (!bucket) continue;
1014
+ if (bucket.primary?.resets_at && bucket.primary.resets_at <= nowSec) {
1015
+ bucket.primary.used_percent = 0;
1016
+ }
1017
+ if (bucket.secondary?.resets_at && bucket.secondary.resets_at <= nowSec) {
1018
+ bucket.secondary.used_percent = 0;
1019
+ }
1020
+ }
1021
+ return buckets;
1022
+ }
1023
+
999
1024
  // ============================================================================
1000
1025
  // Codex 세션 JSONL에서 실제 rate limits 추출
1001
1026
  // 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
@@ -1061,6 +1086,7 @@ function getCodexRateLimits() {
1061
1086
  const main = mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
1062
1087
  if (main && !main.tokens) main.tokens = syntheticBucket.tokens;
1063
1088
  }
1089
+ expireStaleCodexBuckets(mergedBuckets);
1064
1090
  return mergedBuckets;
1065
1091
  }
1066
1092
  }
@@ -1213,6 +1239,15 @@ function readGeminiQuotaSnapshot(accountId, authContext) {
1213
1239
  const isFresh = ageMs < GEMINI_QUOTA_STALE_MS;
1214
1240
 
1215
1241
  if (keyMatched) {
1242
+ // resetTime이 지난 버킷의 remainingFraction을 1로 보정 (stale 캐시 방지)
1243
+ if (Array.isArray(cache.buckets)) {
1244
+ const now = Date.now();
1245
+ for (const b of cache.buckets) {
1246
+ if (b?.resetTime && new Date(b.resetTime).getTime() <= now) {
1247
+ b.remainingFraction = 1;
1248
+ }
1249
+ }
1250
+ }
1216
1251
  return { quota: cache, shouldRefresh: !isFresh };
1217
1252
  }
1218
1253
  if (isLegacyCache) {
@@ -1250,6 +1285,7 @@ function readCodexRateLimitSnapshot() {
1250
1285
  if (!cache?.buckets) {
1251
1286
  return { buckets: null, shouldRefresh: true };
1252
1287
  }
1288
+ expireStaleCodexBuckets(cache.buckets);
1253
1289
  const ts = Number(cache.timestamp);
1254
1290
  const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
1255
1291
  const isFresh = ageMs < CODEX_QUOTA_STALE_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,22 +2,58 @@
2
2
  // scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
3
3
 
4
4
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
5
- import { join } from "node:path";
5
+ import { join, dirname } from "node:path";
6
6
  import { homedir } from "node:os";
7
- import { execSync } from "node:child_process";
7
+ import { execSync, spawn } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const PKG_ROOT = join(dirname(__filename), "..");
12
+
13
+ // 동기 대기 (Atomics.wait — Node.js main thread에서 사용 가능)
14
+ const _sab = new Int32Array(new SharedArrayBuffer(4));
15
+ function sleepSync(ms) { Atomics.wait(_sab, 0, 0, ms); }
8
16
 
9
17
  const CACHE_DIR = join(homedir(), ".claude", "cache");
10
18
  const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
11
19
  const CACHE_TTL_MS = 30_000; // 30초
12
20
 
13
21
  function checkHub() {
22
+ // 1차 시도
14
23
  try {
15
24
  const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8", windowsHide: true });
16
25
  const data = JSON.parse(res);
17
26
  return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
27
+ } catch {}
28
+
29
+ // Hub 미응답 → 자동 재시작 시도 (bridge.mjs tryRestartHub 동기 버전)
30
+ const serverPath = join(PKG_ROOT, "hub", "server.mjs");
31
+ if (!existsSync(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
32
+
33
+ try {
34
+ const child = spawn(process.execPath, [serverPath], {
35
+ detached: true,
36
+ stdio: "ignore",
37
+ windowsHide: true,
38
+ });
39
+ child.unref();
18
40
  } catch {
19
- return { ok: false, state: "unreachable" };
41
+ return { ok: false, state: "unreachable", restart: "spawn_failed" };
20
42
  }
43
+
44
+ // 최대 4초 폴링 (500ms × 8)
45
+ for (let i = 0; i < 8; i++) {
46
+ sleepSync(500);
47
+ try {
48
+ const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 1000, encoding: "utf8", windowsHide: true });
49
+ const data = JSON.parse(res);
50
+ if (data?.hub?.state === "healthy") {
51
+ return { ok: true, state: "healthy", pid: data?.pid, restarted: true };
52
+ }
53
+ } catch {}
54
+ }
55
+
56
+ return { ok: false, state: "unreachable", restart: "timeout" };
21
57
  }
22
58
 
23
59
  function checkRoute() {
@@ -63,6 +99,14 @@ function runPreflight() {
63
99
  ok: false,
64
100
  };
65
101
  result.ok = result.hub.ok && result.route.ok;
102
+
103
+ // CLI 가용성 → available_agents (triage에서 참조)
104
+ const agents = [];
105
+ if (result.codex.ok) agents.push("codex");
106
+ if (result.gemini.ok) agents.push("gemini");
107
+ agents.push("claude"); // claude는 항상 가용
108
+ result.available_agents = agents;
109
+
66
110
  return result;
67
111
  }
68
112
 
@@ -84,7 +128,9 @@ if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
84
128
  const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
85
129
  const details = [];
86
130
  if (!result.hub.ok) details.push("hub:" + result.hub.state);
131
+ else if (result.hub.restarted) details.push("hub:restarted");
87
132
  if (!result.route.ok) details.push("route:missing");
133
+ if (result.available_agents.length === 1) details.push("agents:claude-only");
88
134
  console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
89
135
  }
90
136