@triflux/core 10.80.0 → 10.80.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.
@@ -43,6 +43,18 @@ export function getClaudeEmail() {
43
43
  return config?.oauthAccount?.emailAddress || null;
44
44
  }
45
45
 
46
+ export function getClaudeAccount() {
47
+ const config = readJson(
48
+ join(process.env.CLAUDE_CONFIG_DIR || homedir(), ".claude.json"),
49
+ null,
50
+ );
51
+ return (
52
+ config?.oauthAccount?.accountUuid ||
53
+ config?.oauthAccount?.emailAddress ||
54
+ null
55
+ );
56
+ }
57
+
46
58
  export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
47
59
  export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
48
60
  const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
@@ -542,6 +554,19 @@ export function readClaudeUsageSnapshot() {
542
554
  cache?.errorType === "rate_limit" &&
543
555
  Number.isFinite(schedule.nextRefreshAt) &&
544
556
  Date.now() < schedule.nextRefreshAt;
557
+ // account 가 없는 옛 버전 캐시는 같은 계정으로 본다.
558
+ const accountChanged =
559
+ cache != null && "account" in cache && cache.account !== getClaudeAccount();
560
+ if ((accountChanged || cache?.recheck === true) && !staleBackoffActive) {
561
+ schedule.shouldRefresh = true;
562
+ }
563
+ if (accountChanged || (cache?.recheck === true && !cache.data)) {
564
+ return {
565
+ data: null,
566
+ shouldRefresh: schedule.shouldRefresh,
567
+ isStale: staleBackoffActive,
568
+ };
569
+ }
545
570
 
546
571
  // 1차: 자체 캐시에 유효 데이터가 있는 경우
547
572
  if (cache?.data) {
@@ -621,6 +646,7 @@ export function writeClaudeUsageCache(
621
646
  data,
622
647
  errorInfo = null,
623
648
  pollState = null,
649
+ { account = getClaudeAccount(), recheck = false } = {},
624
650
  ) {
625
651
  const state =
626
652
  pollState ||
@@ -639,6 +665,8 @@ export function writeClaudeUsageCache(
639
665
  : computeClaudeUsagePollState({ outcome: "success" }));
640
666
  const entry = {
641
667
  timestamp: Date.now(),
668
+ account,
669
+ recheck,
642
670
  data,
643
671
  error: !!errorInfo,
644
672
  errorType: errorInfo?.type || null, // "rate_limit" | "auth" | "network" | "unknown"
@@ -648,9 +676,13 @@ export function writeClaudeUsageCache(
648
676
  nextRefreshAt: Date.now() + state.delayMs,
649
677
  };
650
678
  // 에러 시 기존 유효 데이터 보존 (--% n/a 방지)
651
- if (errorInfo && data == null) {
679
+ if (errorInfo) {
652
680
  const prev = readJson(CLAUDE_USAGE_CACHE_PATH, null);
653
- if (prev?.data) {
681
+ if (prev) {
682
+ entry.account = prev.account;
683
+ entry.recheck = prev.recheck === true;
684
+ }
685
+ if (data == null && prev?.data) {
654
686
  entry.data = prev.data;
655
687
  entry.stale = true;
656
688
  }
@@ -664,6 +696,7 @@ export async function fetchClaudeUsage(forceRefresh = false) {
664
696
  return existingSnapshot.data || null;
665
697
  }
666
698
  const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
699
+ const account = getClaudeAccount();
667
700
  const consecutive429s = Number.isFinite(cache?.consecutive429s)
668
701
  ? cache.consecutive429s
669
702
  : 0;
@@ -725,10 +758,26 @@ export async function fetchClaudeUsage(forceRefresh = false) {
725
758
  const pollState = usage
726
759
  ? computeClaudeUsagePollState({ outcome: "success" })
727
760
  : null;
761
+ const accountChanged = cache?.account !== account;
762
+ const reset =
763
+ !accountChanged &&
764
+ ["fiveHour", "weekly"].some((window) => {
765
+ const beforePercent = cache?.data?.[`${window}Percent`];
766
+ const afterPercent = usage?.[`${window}Percent`];
767
+ const beforeReset = Date.parse(cache?.data?.[`${window}ResetsAt`]);
768
+ const afterReset = Date.parse(usage?.[`${window}ResetsAt`]);
769
+ return (
770
+ (beforePercent != null &&
771
+ afterPercent != null &&
772
+ afterPercent < beforePercent) ||
773
+ Math.abs(afterReset - beforeReset) > 60 * 60 * 1000
774
+ );
775
+ });
728
776
  writeClaudeUsageCache(
729
777
  usage,
730
778
  usage ? null : { type: "unknown", status: 0 },
731
779
  pollState,
780
+ { account, recheck: accountChanged || reset },
732
781
  );
733
782
  return usage;
734
783
  }
@@ -736,11 +785,22 @@ export async function fetchClaudeUsage(forceRefresh = false) {
736
785
  export function scheduleClaudeUsageRefresh() {
737
786
  const scriptPath = process.argv[1];
738
787
  if (!scriptPath) return;
788
+ const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
789
+ if (
790
+ cache?.errorType === "rate_limit" &&
791
+ !getSnapshotSchedule(cache).shouldRefresh
792
+ )
793
+ return;
794
+ const directRefresh =
795
+ (cache != null &&
796
+ "account" in cache &&
797
+ cache.account !== getClaudeAccount()) ||
798
+ cache?.recheck === true;
739
799
 
740
800
  // OMC 플러그인이 이미 fresh 데이터를 가지고 있으면 HUD 리프레시 불필요 (429 방지)
741
801
  try {
742
802
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
743
- if (omcCache?.data?.fiveHourPercent != null) {
803
+ if (!directRefresh && omcCache?.data?.fiveHourPercent != null) {
744
804
  const omcAge = Number.isFinite(omcCache.timestamp)
745
805
  ? Date.now() - omcCache.timestamp
746
806
  : Infinity;
@@ -49,6 +49,7 @@ export function listProbeTargets({
49
49
  stateFilePath,
50
50
  ttlMs,
51
51
  nowMs,
52
+ force = false,
52
53
  }) {
53
54
  const state = readJsonFile(stateFilePath) || {};
54
55
  const due = (key) =>
@@ -56,7 +57,11 @@ export function listProbeTargets({
56
57
  const targets = [];
57
58
 
58
59
  const currentAuth = readJsonFile(codexAuthPath);
59
- if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
60
+ if (
61
+ currentAuth &&
62
+ accessTokenUsable(currentAuth, nowMs) &&
63
+ (force || due("current"))
64
+ ) {
60
65
  targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
61
66
  }
62
67
  return targets;
@@ -600,11 +600,14 @@ export function readCodexRateLimitSnapshot() {
600
600
  return {
601
601
  buckets,
602
602
  promptCache: cache.promptCache ?? null,
603
- shouldRefresh: !isFresh,
603
+ shouldRefresh:
604
+ !isFresh ||
605
+ cache.owner !== getCodexProbeOwner() ||
606
+ cache.recheck === true,
604
607
  };
605
608
  }
606
609
 
607
- export async function collectProbeSnapshots(now = new Date()) {
610
+ export async function collectProbeSnapshots(now = new Date(), force = false) {
608
611
  if (process.env.TFX_CODEX_PROBE === "0") return [];
609
612
  let targets = [];
610
613
  try {
@@ -613,6 +616,7 @@ export async function collectProbeSnapshots(now = new Date()) {
613
616
  stateFilePath: CODEX_PROBE_STATE_PATH,
614
617
  ttlMs: Number(process.env.TFX_CODEX_PROBE_TTL_MS) || CODEX_PROBE_TTL_MS,
615
618
  nowMs: now.getTime(),
619
+ force,
616
620
  });
617
621
  } catch {
618
622
  return [];
@@ -658,11 +662,13 @@ export async function refreshCodexRateLimitsCache({
658
662
  owner = getCodexProbeOwner(),
659
663
  } = {}) {
660
664
  const now = new Date();
661
- const probed = await collect(now);
665
+ const previous = readJson(cachePath, null);
666
+ const ownerChanged = previous?.owner !== owner;
667
+ const probed = await collect(now, ownerChanged || previous?.recheck === true);
662
668
  const probe =
663
669
  probed.length > 0
664
670
  ? { owner, at: now.getTime(), snapshots: probed }
665
- : carryProbe(readJson(cachePath, null)?.probe, owner, now.getTime());
671
+ : carryProbe(previous?.probe, owner, now.getTime());
666
672
  const buckets = getCodexRateLimits({
667
673
  sessionsRoot,
668
674
  now,
@@ -671,9 +677,25 @@ export async function refreshCodexRateLimitsCache({
671
677
  const promptCache = readCodexPromptCache(
672
678
  sessionsRoot || join(getCodexHome(), "sessions"),
673
679
  );
680
+ const reset = ["primary", "secondary"].some((window) => {
681
+ const before = previous?.buckets?.codex?.[window];
682
+ const after = buckets?.codex?.[window];
683
+ return (
684
+ before &&
685
+ after &&
686
+ ((before.used_percent != null &&
687
+ after.used_percent != null &&
688
+ after.used_percent < before.used_percent) ||
689
+ (before.resets_at != null &&
690
+ after.resets_at != null &&
691
+ Math.abs(after.resets_at - before.resets_at) > 3600))
692
+ );
693
+ });
674
694
  // buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
675
695
  writeJsonSafe(cachePath, {
676
696
  timestamp: Date.now(),
697
+ owner,
698
+ recheck: ownerChanged || reset,
677
699
  buckets,
678
700
  promptCache,
679
701
  probe,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.80.0",
3
+ "version": "10.80.1",
4
4
  "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",