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.
- package/hud/hud-qos-status.mjs +38 -2
- package/package.json +1 -1
- package/scripts/preflight-cache.mjs +49 -3
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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
|
|