triflux 4.2.0 → 4.2.1

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.1",
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": {