@triflux/core 10.42.0 → 10.43.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.
@@ -0,0 +1,216 @@
1
+ // codex app-server account/rateLimits/read 능동 프로브 + 정규화.
2
+ // 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
3
+
4
+ import { spawn } from "node:child_process";
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+
16
+ function readJwtExpSec(token) {
17
+ try {
18
+ const payload = JSON.parse(
19
+ Buffer.from(token.split(".")[1], "base64url").toString("utf-8"),
20
+ );
21
+ return Number(payload.exp) || 0;
22
+ } catch {
23
+ return 0;
24
+ }
25
+ }
26
+
27
+ function readJsonFile(path) {
28
+ try {
29
+ return JSON.parse(readFileSync(path, "utf-8"));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ const EXP_MARGIN_SEC = 600;
36
+
37
+ function accessTokenUsable(auth, nowMs) {
38
+ const token = auth?.tokens?.access_token;
39
+ if (!token) return false;
40
+ const exp = readJwtExpSec(token);
41
+ return exp > Math.floor(nowMs / 1000) + EXP_MARGIN_SEC;
42
+ }
43
+
44
+ export function recordProbe(stateFilePath, key, nowMs) {
45
+ const state = readJsonFile(stateFilePath) || {};
46
+ state[key] = nowMs;
47
+ try {
48
+ mkdirSync(dirname(stateFilePath), { recursive: true });
49
+ writeFileSync(stateFilePath, JSON.stringify(state));
50
+ } catch {
51
+ // 상태 기록 실패는 무시 — 다음 사이클에 재시도
52
+ }
53
+ }
54
+
55
+ export function listProbeTargets({
56
+ codexAuthPath,
57
+ brokerCacheDir,
58
+ stateFilePath,
59
+ ttlMs,
60
+ nowMs,
61
+ }) {
62
+ const state = readJsonFile(stateFilePath) || {};
63
+ const due = (key) =>
64
+ !(Number(state[key]) > 0 && nowMs - Number(state[key]) < ttlMs);
65
+ const targets = [];
66
+
67
+ const currentAuth = readJsonFile(codexAuthPath);
68
+ if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
69
+ targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
70
+ }
71
+
72
+ if (brokerCacheDir && existsSync(brokerCacheDir)) {
73
+ let files = [];
74
+ try {
75
+ files = readdirSync(brokerCacheDir).filter((file) =>
76
+ /^codex-auth-.+\.json$/.test(file),
77
+ );
78
+ } catch {
79
+ files = [];
80
+ }
81
+ for (const file of files) {
82
+ const key = file.replace(/^codex-auth-/, "").replace(/\.json$/, "");
83
+ if (!due(key)) continue;
84
+ const auth = readJsonFile(join(brokerCacheDir, file));
85
+ if (!auth || !accessTokenUsable(auth, nowMs)) continue;
86
+ const home = mkdtempSync(join(tmpdir(), `tfx-probe-home-${key}-`));
87
+ writeFileSync(join(home, "auth.json"), JSON.stringify(auth));
88
+ targets.push({ key, codexHome: home });
89
+ }
90
+ }
91
+ return targets;
92
+ }
93
+
94
+ function toSnakeBucket(bucket) {
95
+ if (!bucket) return null;
96
+ return {
97
+ used_percent: bucket.usedPercent ?? null,
98
+ window_minutes: bucket.windowDurationMins ?? null,
99
+ resets_at: bucket.resetsAt ?? null,
100
+ };
101
+ }
102
+
103
+ function toSnapshot(entry, nowIso) {
104
+ return {
105
+ limitId: entry.limitId,
106
+ limitName: entry.limitName ?? null,
107
+ primary: toSnakeBucket(entry.primary),
108
+ secondary: toSnakeBucket(entry.secondary),
109
+ credits: entry.credits ?? null,
110
+ tokens: null,
111
+ contextWindow: null,
112
+ timestamp: nowIso,
113
+ probe: true,
114
+ };
115
+ }
116
+
117
+ export function normalizeAppServerRateLimits(result, nowIso) {
118
+ if (!result) return [];
119
+ const byId = result.rateLimitsByLimitId;
120
+ if (byId && typeof byId === "object" && Object.keys(byId).length > 0) {
121
+ return Object.values(byId)
122
+ .filter((e) => e?.limitId)
123
+ .map((e) => toSnapshot(e, nowIso));
124
+ }
125
+ if (result.rateLimits?.limitId) {
126
+ return [toSnapshot(result.rateLimits, nowIso)];
127
+ }
128
+ return [];
129
+ }
130
+
131
+ export function probeRateLimitsViaAppServer({
132
+ codexHome = null,
133
+ spawnBin = "codex",
134
+ spawnArgs = ["app-server"],
135
+ timeoutMs = 10000,
136
+ now = new Date(),
137
+ } = {}) {
138
+ return new Promise((resolve) => {
139
+ let child;
140
+ try {
141
+ child = spawn(spawnBin, spawnArgs, {
142
+ stdio: ["pipe", "pipe", "ignore"],
143
+ env: codexHome
144
+ ? { ...process.env, CODEX_HOME: codexHome }
145
+ : process.env,
146
+ });
147
+ } catch {
148
+ resolve(null);
149
+ return;
150
+ }
151
+ let settled = false;
152
+ let buf = "";
153
+ const finish = (value) => {
154
+ if (settled) return;
155
+ settled = true;
156
+ clearTimeout(timer);
157
+ try {
158
+ child.kill();
159
+ } catch {
160
+ // 이미 종료
161
+ }
162
+ resolve(value);
163
+ };
164
+ const timer = setTimeout(() => finish(null), timeoutMs);
165
+ const send = (obj) => {
166
+ try {
167
+ child.stdin.write(`${JSON.stringify(obj)}\n`);
168
+ } catch {
169
+ finish(null);
170
+ }
171
+ };
172
+ child.on("error", () => finish(null));
173
+ child.on("exit", () => finish(null));
174
+ child.stdout.on("data", (data) => {
175
+ buf += data.toString();
176
+ let idx = buf.indexOf("\n");
177
+ while (idx >= 0) {
178
+ const line = buf.slice(0, idx);
179
+ buf = buf.slice(idx + 1);
180
+ idx = buf.indexOf("\n");
181
+ if (!line.trim()) continue;
182
+ let msg;
183
+ try {
184
+ msg = JSON.parse(line);
185
+ } catch {
186
+ continue;
187
+ }
188
+ if (msg.id === 1 && msg.result) {
189
+ send({
190
+ jsonrpc: "2.0",
191
+ id: 2,
192
+ method: "account/rateLimits/read",
193
+ params: {},
194
+ });
195
+ } else if (msg.id === 2) {
196
+ if (msg.error) finish(null);
197
+ else {
198
+ finish(normalizeAppServerRateLimits(msg.result, now.toISOString()));
199
+ }
200
+ }
201
+ }
202
+ });
203
+ send({
204
+ jsonrpc: "2.0",
205
+ id: 1,
206
+ method: "initialize",
207
+ params: {
208
+ clientInfo: {
209
+ name: "triflux-hud-probe",
210
+ title: "triflux HUD quota probe",
211
+ version: "1.0.0",
212
+ },
213
+ },
214
+ });
215
+ });
216
+ }
@@ -8,7 +8,11 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import {
10
10
  CODEX_AUTH_PATH,
11
+ CODEX_BROKER_AUTH_CACHE_DIR,
11
12
  CODEX_MIN_BUCKETS,
13
+ CODEX_PROBE_STATE_PATH,
14
+ CODEX_PROBE_TIMEOUT_MS,
15
+ CODEX_PROBE_TTL_MS,
12
16
  CODEX_QUOTA_CACHE_PATH,
13
17
  CODEX_QUOTA_STALE_MS,
14
18
  CODEX_REFRESH_FLAG,
@@ -16,6 +20,11 @@ import {
16
20
  SPAWN_LOCK_TTL_MS,
17
21
  } from "../constants.mjs";
18
22
  import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
23
+ import {
24
+ listProbeTargets,
25
+ probeRateLimitsViaAppServer,
26
+ recordProbe,
27
+ } from "./codex-probe.mjs";
19
28
 
20
29
  // window_minutes 기반 5h/1w 슬롯 분류
21
30
  // 5h: ≤360min (300min 표준, ±20% 허용)
@@ -50,9 +59,11 @@ export function getCodexEmail() {
50
59
  }
51
60
 
52
61
  // resets_at이 지난 윈도우의 used_percent를 0으로 보정
53
- export function expireStaleCodexBuckets(buckets) {
62
+ export function expireStaleCodexBuckets(
63
+ buckets,
64
+ nowSec = Math.floor(Date.now() / 1000),
65
+ ) {
54
66
  if (!buckets) return buckets;
55
- const nowSec = Math.floor(Date.now() / 1000);
56
67
  const result = {};
57
68
  for (const [key, bucket] of Object.entries(buckets)) {
58
69
  if (!bucket) {
@@ -63,13 +74,13 @@ export function expireStaleCodexBuckets(buckets) {
63
74
  if (bucket.primary?.resets_at && bucket.primary.resets_at <= nowSec) {
64
75
  updated = {
65
76
  ...updated,
66
- primary: { ...updated.primary, used_percent: 0 },
77
+ primary: { ...updated.primary, used_percent: 0, expired: true },
67
78
  };
68
79
  }
69
80
  if (bucket.secondary?.resets_at && bucket.secondary.resets_at <= nowSec) {
70
81
  updated = {
71
82
  ...updated,
72
- secondary: { ...updated.secondary, used_percent: 0 },
83
+ secondary: { ...updated.secondary, used_percent: 0, expired: true },
73
84
  };
74
85
  }
75
86
  result[key] = updated;
@@ -77,51 +88,178 @@ export function expireStaleCodexBuckets(buckets) {
77
88
  return result;
78
89
  }
79
90
 
91
+ const RESET_CLUSTER_TOLERANCE_SEC = 90;
92
+
93
+ function isNewerSnapshot(candidate, current) {
94
+ if (!current) return true;
95
+ const candidateTimestamp = Date.parse(candidate.timestamp);
96
+ const currentTimestamp = Date.parse(current.timestamp);
97
+ return (
98
+ Number.isFinite(candidateTimestamp) &&
99
+ (!Number.isFinite(currentTimestamp) ||
100
+ candidateTimestamp > currentTimestamp)
101
+ );
102
+ }
103
+
104
+ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
105
+ const groups = [];
106
+ const ungrouped = [];
107
+
108
+ for (const snapshot of snapshots) {
109
+ const resetAt = Number(snapshot[windowKey]?.resets_at);
110
+ if (!Number.isFinite(resetAt)) {
111
+ ungrouped.push(snapshot);
112
+ continue;
113
+ }
114
+
115
+ let group = groups.find(
116
+ (candidate) =>
117
+ Math.max(candidate.maxResetAt, resetAt) -
118
+ Math.min(candidate.minResetAt, resetAt) <=
119
+ RESET_CLUSTER_TOLERANCE_SEC,
120
+ );
121
+ if (!group) {
122
+ group = {
123
+ minResetAt: resetAt,
124
+ maxResetAt: resetAt,
125
+ latest: snapshot,
126
+ };
127
+ groups.push(group);
128
+ continue;
129
+ }
130
+
131
+ group.minResetAt = Math.min(group.minResetAt, resetAt);
132
+ group.maxResetAt = Math.max(group.maxResetAt, resetAt);
133
+ if (isNewerSnapshot(snapshot, group.latest)) group.latest = snapshot;
134
+ }
135
+
136
+ const activeGroups = groups.filter(
137
+ (group) => Number(group.latest[windowKey]?.resets_at) > nowSec,
138
+ );
139
+ let selected = null;
140
+ if (activeGroups.length > 0) {
141
+ selected = activeGroups
142
+ .map((group) => group.latest)
143
+ .sort((a, b) => {
144
+ const usedDifference =
145
+ Number(b[windowKey]?.used_percent || 0) -
146
+ Number(a[windowKey]?.used_percent || 0);
147
+ if (usedDifference !== 0) return usedDifference;
148
+ return Date.parse(b.timestamp) - Date.parse(a.timestamp);
149
+ })[0];
150
+ } else {
151
+ for (const snapshot of [
152
+ ...groups.map((group) => group.latest),
153
+ ...ungrouped,
154
+ ]) {
155
+ if (isNewerSnapshot(snapshot, selected)) selected = snapshot;
156
+ }
157
+ }
158
+
159
+ return { snapshot: selected, activeGroupCount: activeGroups.length };
160
+ }
161
+
162
+ function selectCodexSnapshot(snapshots, nowSec) {
163
+ const primarySelection = selectCodexWindowSnapshot(
164
+ snapshots,
165
+ nowSec,
166
+ "primary",
167
+ );
168
+ const secondarySelection = selectCodexWindowSnapshot(
169
+ snapshots,
170
+ nowSec,
171
+ "secondary",
172
+ );
173
+ const primarySnapshot = primarySelection.snapshot;
174
+ if (!primarySnapshot) return null;
175
+
176
+ const secondarySnapshot = secondarySelection.snapshot;
177
+ const selected = {
178
+ ...primarySnapshot,
179
+ secondary: secondarySnapshot?.secondary || null,
180
+ secondaryTimestamp:
181
+ secondarySnapshot?.timestamp || primarySnapshot.timestamp,
182
+ };
183
+ if (
184
+ primarySelection.activeGroupCount >= 2 ||
185
+ secondarySelection.activeGroupCount >= 2
186
+ ) {
187
+ selected.mixedWindows = true;
188
+ }
189
+ return selected;
190
+ }
191
+
192
+ function mergeRateLimitSnapshots(snapshots, nowSec) {
193
+ const merged = {};
194
+ const codexSnapshots = [];
195
+ for (const snapshot of snapshots) {
196
+ if (snapshot.limitId === "codex") {
197
+ codexSnapshots.push(snapshot);
198
+ continue;
199
+ }
200
+ if (isNewerSnapshot(snapshot, merged[snapshot.limitId])) {
201
+ merged[snapshot.limitId] = snapshot;
202
+ }
203
+ }
204
+
205
+ const codex = selectCodexSnapshot(codexSnapshots, nowSec);
206
+ if (codex) merged.codex = codex;
207
+ return merged;
208
+ }
209
+
80
210
  // ============================================================================
81
211
  // Codex 세션 JSONL에서 실제 rate limits 추출
82
212
  // 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
83
213
  // 최근 7일간 세션 파일을 스캔해 가장 최신 rate_limits 버킷을 수집한다.
84
214
  // 합성 버킷(token_count 기반)은 2일 이내 데이터만 허용하여 stale 방지.
85
215
  // ============================================================================
86
- export function getCodexRateLimits() {
87
- const now = new Date();
216
+ export function getCodexRateLimits({
217
+ sessionsRoot = join(homedir(), ".codex", "sessions"),
218
+ now = new Date(),
219
+ maxLinesPerFile = 800,
220
+ extraSnapshots = [],
221
+ } = {}) {
88
222
  let syntheticBucket = null; // 최근 token_count에서 합성 (행 활성화 + 토큰 데이터용)
223
+ const recentSnapshots = [...extraSnapshots];
224
+ const nowSec = Math.floor(now.getTime() / 1000);
89
225
 
90
226
  // 7일간 스캔: 실제 rate_limits 우선, 합성 버킷은 폴백
91
227
  for (let dayOffset = 0; dayOffset <= 6; dayOffset++) {
92
228
  const d = new Date(now.getTime() - dayOffset * 86_400_000);
93
229
  const sessDir = join(
94
- homedir(),
95
- ".codex",
96
- "sessions",
230
+ sessionsRoot,
97
231
  String(d.getFullYear()),
98
232
  String(d.getMonth() + 1).padStart(2, "0"),
99
233
  String(d.getDate()).padStart(2, "0"),
100
234
  );
101
- if (!existsSync(sessDir)) continue;
102
- let files;
103
- try {
104
- files = readdirSync(sessDir)
105
- .filter((f) => f.endsWith(".jsonl"))
106
- .sort()
107
- .reverse();
108
- } catch {
109
- continue;
235
+ let files = [];
236
+ if (existsSync(sessDir)) {
237
+ try {
238
+ files = readdirSync(sessDir)
239
+ .filter((f) => f.endsWith(".jsonl"))
240
+ .sort()
241
+ .reverse();
242
+ } catch {
243
+ files = [];
244
+ }
110
245
  }
111
246
 
112
- const mergedBuckets = {};
247
+ const daySnapshots = [];
113
248
  for (const file of files) {
114
249
  try {
115
250
  const content = readFileSync(join(sessDir, file), "utf-8");
251
+ const found = {};
116
252
  const lines = content.trim().split("\n").reverse();
253
+ let scanned = 0;
117
254
  for (const line of lines) {
255
+ if (++scanned > maxLinesPerFile) break;
118
256
  try {
119
257
  const evt = JSON.parse(line);
120
258
  const rl = evt?.payload?.rate_limits;
121
- if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
259
+ if (rl?.limit_id && !found[rl.limit_id]) {
122
260
  // window_minutes 기준으로 5h/1w 슬롯 정규화
123
261
  const { primary, secondary } = normalizeBuckets(rl);
124
- mergedBuckets[rl.limit_id] = {
262
+ found[rl.limit_id] = {
125
263
  limitId: rl.limit_id,
126
264
  limitName: rl.limit_name,
127
265
  primary,
@@ -152,21 +290,28 @@ export function getCodexRateLimits() {
152
290
  } catch {
153
291
  /* 라인 파싱 실패 무시 */
154
292
  }
155
- if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
293
+ if (Object.keys(found).length >= CODEX_MIN_BUCKETS) break;
156
294
  }
295
+ daySnapshots.push(...Object.values(found));
157
296
  } catch {
158
297
  /* 파일 읽기 실패 무시 */
159
298
  }
160
299
  }
161
- // 실제 rate_limits 발견 → 토큰 데이터 병합 후 즉시 반환
300
+
301
+ if (dayOffset <= 1) {
302
+ recentSnapshots.push(...daySnapshots);
303
+ if (dayOffset === 0) continue;
304
+ }
305
+
306
+ const snapshots = dayOffset <= 1 ? recentSnapshots : daySnapshots;
307
+ const mergedBuckets = mergeRateLimitSnapshots(snapshots, nowSec);
162
308
  if (Object.keys(mergedBuckets).length > 0) {
163
309
  if (syntheticBucket) {
164
310
  const main =
165
311
  mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
166
312
  if (main && !main.tokens) main.tokens = syntheticBucket.tokens;
167
313
  }
168
- expireStaleCodexBuckets(mergedBuckets);
169
- return mergedBuckets;
314
+ return expireStaleCodexBuckets(mergedBuckets, nowSec);
170
315
  }
171
316
  }
172
317
  // 실제 rate_limits 없음 → 합성 버킷이라도 반환 (행 활성화)
@@ -178,15 +323,47 @@ export function readCodexRateLimitSnapshot() {
178
323
  if (!cache?.buckets) {
179
324
  return { buckets: null, shouldRefresh: true };
180
325
  }
181
- expireStaleCodexBuckets(cache.buckets);
326
+ const buckets = expireStaleCodexBuckets(cache.buckets);
182
327
  const ts = Number(cache.timestamp);
183
328
  const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
184
329
  const isFresh = ageMs < CODEX_QUOTA_STALE_MS;
185
- return { buckets: cache.buckets, shouldRefresh: !isFresh };
330
+ return { buckets, shouldRefresh: !isFresh };
186
331
  }
187
332
 
188
- export function refreshCodexRateLimitsCache() {
189
- const buckets = getCodexRateLimits();
333
+ export async function collectProbeSnapshots(now = new Date()) {
334
+ if (process.env.TFX_CODEX_PROBE === "0") return [];
335
+ let targets = [];
336
+ try {
337
+ targets = listProbeTargets({
338
+ codexAuthPath: CODEX_AUTH_PATH,
339
+ brokerCacheDir: CODEX_BROKER_AUTH_CACHE_DIR,
340
+ stateFilePath: CODEX_PROBE_STATE_PATH,
341
+ ttlMs: Number(process.env.TFX_CODEX_PROBE_TTL_MS) || CODEX_PROBE_TTL_MS,
342
+ nowMs: now.getTime(),
343
+ });
344
+ } catch {
345
+ return [];
346
+ }
347
+ const settled = await Promise.allSettled(
348
+ targets.map(async (target) => {
349
+ const snapshots = await probeRateLimitsViaAppServer({
350
+ codexHome: target.codexHome,
351
+ timeoutMs: CODEX_PROBE_TIMEOUT_MS,
352
+ now,
353
+ });
354
+ recordProbe(CODEX_PROBE_STATE_PATH, target.key, now.getTime());
355
+ return snapshots || [];
356
+ }),
357
+ );
358
+ return settled.flatMap((result) =>
359
+ result.status === "fulfilled" ? result.value : [],
360
+ );
361
+ }
362
+
363
+ export async function refreshCodexRateLimitsCache() {
364
+ const now = new Date();
365
+ const extraSnapshots = await collectProbeSnapshots(now);
366
+ const buckets = getCodexRateLimits({ now, extraSnapshots });
190
367
  // buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
191
368
  writeJsonSafe(CODEX_QUOTA_CACHE_PATH, { timestamp: Date.now(), buckets });
192
369
  return buckets;
package/hud/renderers.mjs CHANGED
@@ -680,13 +680,11 @@ export function getProviderRow(
680
680
  ? colorByProvider(weekP, formatPercentCell(weekP), provFn)
681
681
  : dim(formatPlaceholderPercentCell());
682
682
  const fiveReset =
683
- formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
683
+ formatResetRemaining(main.primary?.resets_at) || "n/a";
684
684
  const weekReset =
685
- formatResetRemainingDayHour(
686
- main.secondary?.resets_at,
687
- SEVEN_DAY_MS,
688
- ) || "n/a";
685
+ formatResetRemainingDayHour(main.secondary?.resets_at) || "n/a";
689
686
  quotaSection = `${dim("5h:")}${fCell} ${dim(formatTimeCell(fiveReset))} ${dim("1w:")}${wCell} ${dim(formatTimeCellDH(weekReset))}`;
687
+ if (main.mixedWindows) quotaSection += dim("*");
690
688
  }
691
689
  }
692
690
  if (provider === "gemini" && realQuota?.type === "gemini") {
@@ -742,11 +740,9 @@ export function getProviderRow(
742
740
  main.secondary?.used_percent != null
743
741
  ? clampPercent(main.secondary.used_percent)
744
742
  : null;
745
- const fiveReset =
746
- formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
743
+ const fiveReset = formatResetRemaining(main.primary?.resets_at) || "n/a";
747
744
  const weekReset =
748
- formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) ||
749
- "n/a";
745
+ formatResetRemainingDayHour(main.secondary?.resets_at) || "n/a";
750
746
  const fCell =
751
747
  fiveP != null
752
748
  ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn)
@@ -768,6 +764,7 @@ export function getProviderRow(
768
764
  `${dim(formatTimeCell(fiveReset))} ` +
769
765
  `${dim("1w:")}${wBar}${wCell} ` +
770
766
  `${dim(formatTimeCellDH(weekReset))}`;
767
+ if (main.mixedWindows) quotaSection += dim("*");
771
768
  }
772
769
  }
773
770
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.42.0",
3
+ "version": "10.43.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // 출처: scripts/tfx-route.sh route_agent() L1052-L1059, run_codex_exec() L2036-L2046.
7
7
  // .claude/rules/tfx-update-logic.md Antigravity CLI 1.0.0 정밀 sanity matrix.
8
+ // timeoutSec은 lane 예상 소요 시간(advisory)이다. hard ceiling 집행은 Bash route가 소유한다.
8
9
 
9
10
  export const id = "agy";
10
11
  export const cliType = "antigravity";
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // 출처: scripts/tfx-route.sh route_agent() L1062-L1063 (explore|claude),
7
7
  // L1196-L1203 (TFX_CLI_MODE=gemini fallback), TFX_VERIFIER_OVERRIDE=claude.
8
+ // timeoutSec은 lane 예상 소요 시간(advisory)이다. hard ceiling 집행은 Bash route가 소유한다.
8
9
 
9
10
  export const id = "claude";
10
11
  export const cliType = "claude-native";