@triflux/core 10.44.0 → 10.45.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.
package/hub/router.mjs CHANGED
@@ -376,6 +376,9 @@ export function createRouter(store) {
376
376
  const queuesByAgent = new Map();
377
377
  const liveMessages = new Map();
378
378
  const roleStates = new Map();
379
+ const hubInstanceId = uuidv7();
380
+ const storeFingerprint =
381
+ store.fingerprint || `${store.type || "unknown"}:unknown`;
379
382
  const updateRegistrationIdentity = store.db?.prepare
380
383
  ? store.db.prepare(`
381
384
  UPDATE agents SET
@@ -392,6 +395,70 @@ export function createRouter(store) {
392
395
  let latencyIdx = 0;
393
396
  const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
394
397
 
398
+ function hubIdentity() {
399
+ return {
400
+ hub_instance_id: hubInstanceId,
401
+ store_fingerprint: storeFingerprint,
402
+ };
403
+ }
404
+
405
+ function validateCallerIdentity(input = {}) {
406
+ if (
407
+ input.hub_instance_id != null &&
408
+ input.hub_instance_id !== hubInstanceId
409
+ ) {
410
+ return {
411
+ ok: false,
412
+ error: {
413
+ code: "STALE_HUB_INSTANCE",
414
+ message: "caller is bound to a previous hub instance",
415
+ },
416
+ data: hubIdentity(),
417
+ };
418
+ }
419
+ if (
420
+ input.store_fingerprint != null &&
421
+ input.store_fingerprint !== storeFingerprint
422
+ ) {
423
+ return {
424
+ ok: false,
425
+ error: {
426
+ code: "STORE_FINGERPRINT_MISMATCH",
427
+ message: "caller store fingerprint does not match this hub",
428
+ },
429
+ data: hubIdentity(),
430
+ };
431
+ }
432
+ return null;
433
+ }
434
+
435
+ function isControlPlaneRegistration(args = {}) {
436
+ const metadata = args.metadata || {};
437
+ const values = [
438
+ ...(Array.isArray(args.capabilities) ? args.capabilities : []),
439
+ ...(Array.isArray(args.topics) ? args.topics : []),
440
+ metadata.role,
441
+ ...(Array.isArray(metadata.roles) ? metadata.roles : []),
442
+ ]
443
+ .flat()
444
+ .map((value) => String(value || "").toLowerCase());
445
+ return (
446
+ metadata.control_plane === true ||
447
+ values.some((value) => value === "cto" || value === "role:cto")
448
+ );
449
+ }
450
+
451
+ function staleOwnerFailure(agentId, result) {
452
+ return {
453
+ ok: false,
454
+ error: {
455
+ code: "STALE_PRESENCE_OWNER",
456
+ message: `stale presence owner: ${agentId}`,
457
+ },
458
+ data: { ...result, ...hubIdentity() },
459
+ };
460
+ }
461
+
395
462
  function ensureAgentQueue(agentId) {
396
463
  let queue = queuesByAgent.get(agentId);
397
464
  if (!queue) {
@@ -1120,6 +1187,18 @@ export function createRouter(store) {
1120
1187
  deliveryEmitter,
1121
1188
 
1122
1189
  registerAgent(args) {
1190
+ const identityFailure = validateCallerIdentity(args);
1191
+ if (identityFailure) return identityFailure;
1192
+ if (isControlPlaneRegistration(args) && store.type !== "sqlite") {
1193
+ return {
1194
+ ok: false,
1195
+ error: {
1196
+ code: "CONTROL_PLANE_STORE_DEGRADED",
1197
+ message: "control-plane presence requires durable SQLite storage",
1198
+ },
1199
+ data: { ...hubIdentity(), store_type: store.type || "unknown" },
1200
+ };
1201
+ }
1123
1202
  const identity = normalizeRegistrationIdentity(args);
1124
1203
  const result = store.registerAgent(args);
1125
1204
  if (!registerPresenceIsEffective(result)) {
@@ -1139,11 +1218,23 @@ export function createRouter(store) {
1139
1218
  transferBacklog: true,
1140
1219
  });
1141
1220
  }
1142
- return { ok: true, data: { ...result, hub_pid: process.pid } };
1221
+ return {
1222
+ ok: true,
1223
+ data: { ...result, hub_pid: process.pid, ...hubIdentity() },
1224
+ };
1143
1225
  },
1144
1226
 
1145
- refreshAgentLease(agentId, ttlMs = 30000) {
1146
- const result = store.refreshLease(agentId, ttlMs);
1227
+ refreshAgentLease(
1228
+ agentId,
1229
+ ttlMs = 30000,
1230
+ presenceGeneration,
1231
+ callerIdentity = {},
1232
+ ) {
1233
+ const identityFailure = validateCallerIdentity(callerIdentity);
1234
+ if (identityFailure) return identityFailure;
1235
+ const result = store.refreshLease(agentId, ttlMs, presenceGeneration);
1236
+ if (result?.effective?.updated === false)
1237
+ return staleOwnerFailure(agentId, result);
1147
1238
  refreshRoleCandidateForAgent(agentId);
1148
1239
  for (const roleName of ROLE_TOPICS) {
1149
1240
  ensureRoleLeader(roleName, {
@@ -1151,7 +1242,7 @@ export function createRouter(store) {
1151
1242
  transferBacklog: true,
1152
1243
  });
1153
1244
  }
1154
- return result;
1245
+ return { ok: true, data: result };
1155
1246
  },
1156
1247
 
1157
1248
  subscribeAgent(agentId, topics, { replace = false } = {}) {
@@ -1170,11 +1261,15 @@ export function createRouter(store) {
1170
1261
  return listRuntimeTopics(agentId);
1171
1262
  },
1172
1263
 
1173
- updateAgentStatus(agentId, status) {
1264
+ updateAgentStatus(agentId, status, presenceGeneration) {
1174
1265
  if (status === "offline") {
1175
1266
  runtimeTopics.delete(agentId);
1176
1267
  }
1177
- const updated = store.updateAgentStatus(agentId, status);
1268
+ const updated = store.updateAgentStatus(
1269
+ agentId,
1270
+ status,
1271
+ presenceGeneration,
1272
+ );
1178
1273
  refreshRoleCandidateForAgent(agentId);
1179
1274
  for (const roleName of ROLE_TOPICS) {
1180
1275
  ensureRoleLeader(roleName, {
@@ -1185,6 +1280,39 @@ export function createRouter(store) {
1185
1280
  return updated;
1186
1281
  },
1187
1282
 
1283
+ transitionAgentStatus(
1284
+ agentId,
1285
+ status,
1286
+ presenceGeneration,
1287
+ callerIdentity = {},
1288
+ ) {
1289
+ const identityFailure = validateCallerIdentity(callerIdentity);
1290
+ if (identityFailure) return identityFailure;
1291
+ const updated = store.updateAgentStatus(
1292
+ agentId,
1293
+ status,
1294
+ presenceGeneration,
1295
+ );
1296
+ if (!updated) return staleOwnerFailure(agentId, { status });
1297
+ if (status === "offline") runtimeTopics.delete(agentId);
1298
+ refreshRoleCandidateForAgent(agentId);
1299
+ for (const roleName of ROLE_TOPICS) {
1300
+ ensureRoleLeader(roleName, {
1301
+ reason: `status:${status}`,
1302
+ transferBacklog: true,
1303
+ });
1304
+ }
1305
+ return {
1306
+ ok: true,
1307
+ data: {
1308
+ agent_id: agentId,
1309
+ status,
1310
+ presence_generation: presenceGeneration,
1311
+ ...hubIdentity(),
1312
+ },
1313
+ };
1314
+ },
1315
+
1188
1316
  route(msg) {
1189
1317
  const recipients = uniqueStrings(resolveRecipients(msg));
1190
1318
  if (!recipients.length) {
@@ -1369,7 +1497,14 @@ export function createRouter(store) {
1369
1497
  agent_id,
1370
1498
  reason = "manual",
1371
1499
  requested_by = "manual",
1500
+ hub_instance_id,
1501
+ store_fingerprint,
1372
1502
  } = {}) {
1503
+ const identityFailure = validateCallerIdentity({
1504
+ hub_instance_id,
1505
+ store_fingerprint,
1506
+ });
1507
+ if (identityFailure) return identityFailure;
1373
1508
  const roleName = normalizeRoleName(role);
1374
1509
  if (!roleName) {
1375
1510
  return {
@@ -1440,6 +1575,7 @@ export function createRouter(store) {
1440
1575
  leader_epoch: snapshot.leader_epoch,
1441
1576
  ...transfer,
1442
1577
  status: snapshot,
1578
+ ...hubIdentity(),
1443
1579
  },
1444
1580
  };
1445
1581
  },
@@ -1647,18 +1783,15 @@ export function createRouter(store) {
1647
1783
  return scheduleAssignRetry(job, reason, job.error, requested_by);
1648
1784
  },
1649
1785
 
1650
- sweepExpired() {
1651
- const now = Date.now();
1652
- let expired = 0;
1653
- const deadLettered = [];
1786
+ sweepExpired({ now_ms = Date.now(), source = "manual" } = {}) {
1787
+ const result = store.sweepExpired({ now_ms });
1654
1788
  for (const [messageId, record] of Array.from(liveMessages.entries())) {
1655
- if (record.message.expires_at_ms > now) continue;
1656
- store.moveToDeadLetter(messageId, "ttl_expired", null);
1657
- deadLettered.push(record.message);
1789
+ if (record.message.expires_at_ms > now_ms) continue;
1658
1790
  removeMessage(messageId);
1659
- expired += 1;
1660
1791
  }
1661
- for (const message of deadLettered) {
1792
+ for (const transition of result.transitions) {
1793
+ if (!transition.dlq_inserted) continue;
1794
+ const { message } = transition;
1662
1795
  if (
1663
1796
  !["handoff", "request"].includes(message.type) ||
1664
1797
  message.payload?.kind === "assign.job"
@@ -1683,12 +1816,15 @@ export function createRouter(store) {
1683
1816
  original_to: message.to_agent,
1684
1817
  reason: "ttl_expired",
1685
1818
  task: message.payload?.task ?? null,
1819
+ expired_at_ms: message.expires_at_ms,
1820
+ failed_at_ms: transition.failed_at_ms,
1821
+ recovery_source: source,
1686
1822
  },
1687
1823
  trace_id: message.trace_id,
1688
1824
  correlation_id: message.correlation_id,
1689
1825
  });
1690
1826
  }
1691
- return { messages: expired };
1827
+ return result;
1692
1828
  },
1693
1829
 
1694
1830
  sweepTimedOutAssigns() {
@@ -1709,19 +1845,49 @@ export function createRouter(store) {
1709
1845
  return { timed_out, retried };
1710
1846
  },
1711
1847
 
1712
- startSweeper() {
1848
+ startSweeper({
1849
+ run_immediately = false,
1850
+ interval_ms = 10000,
1851
+ onSweepError,
1852
+ } = {}) {
1713
1853
  if (sweepTimer) return;
1714
- sweepTimer = setInterval(() => {
1854
+ const reportSweepError = (source, error, failures) => {
1855
+ if (typeof onSweepError === "function") {
1856
+ onSweepError({ source, error, consecutive_failures: failures });
1857
+ }
1858
+ };
1859
+ let sweepFailures = 0;
1860
+ let assignFailures = 0;
1861
+ const runSweep = () => {
1862
+ try {
1863
+ router.sweepExpired({ source: "periodic" });
1864
+ sweepFailures = 0;
1865
+ } catch (error) {
1866
+ sweepFailures += 1;
1867
+ reportSweepError("periodic", error, sweepFailures);
1868
+ }
1715
1869
  try {
1716
- router.sweepExpired();
1717
1870
  router.sweepTimedOutAssigns();
1718
- } catch {}
1719
- }, 10000);
1871
+ assignFailures = 0;
1872
+ } catch (error) {
1873
+ assignFailures += 1;
1874
+ reportSweepError("assign_timeout", error, assignFailures);
1875
+ }
1876
+ };
1877
+ if (run_immediately) runSweep();
1878
+ sweepTimer = setInterval(() => {
1879
+ runSweep();
1880
+ }, interval_ms);
1881
+ let staleFailures = 0;
1720
1882
  staleTimer = setInterval(() => {
1721
1883
  try {
1722
1884
  store.sweepStaleAgents();
1723
1885
  router.reelectStaleRoles();
1724
- } catch {}
1886
+ staleFailures = 0;
1887
+ } catch (error) {
1888
+ staleFailures += 1;
1889
+ reportSweepError("stale_agents", error, staleFailures);
1890
+ }
1725
1891
  }, 120000);
1726
1892
  sweepTimer.unref();
1727
1893
  staleTimer.unref();
@@ -1777,8 +1943,19 @@ export function createRouter(store) {
1777
1943
 
1778
1944
  getStatus(
1779
1945
  scope = "hub",
1780
- { agent_id, trace_id, include_metrics = true } = {},
1946
+ {
1947
+ agent_id,
1948
+ trace_id,
1949
+ include_metrics = true,
1950
+ hub_instance_id,
1951
+ store_fingerprint,
1952
+ } = {},
1781
1953
  ) {
1954
+ const identityFailure = validateCallerIdentity({
1955
+ hub_instance_id,
1956
+ store_fingerprint,
1957
+ });
1958
+ if (identityFailure) return identityFailure;
1782
1959
  const data = {};
1783
1960
 
1784
1961
  if (scope === "hub" || scope === "queue") {
@@ -1787,6 +1964,7 @@ export function createRouter(store) {
1787
1964
  uptime_ms: (process.uptime() * 1000) | 0,
1788
1965
  realtime_transport: "named-pipe",
1789
1966
  audit_store: store.type || "sqlite",
1967
+ ...hubIdentity(),
1790
1968
  };
1791
1969
  data.roles = {
1792
1970
  cto: getRoleSnapshot("cto"),
@@ -1840,7 +2018,7 @@ export function createRouter(store) {
1840
2018
  data.trace = store.getMessagesByTrace(trace_id);
1841
2019
  }
1842
2020
 
1843
- return { ok: true, data };
2021
+ return { ok: true, data: { ...data, ...hubIdentity() } };
1844
2022
  },
1845
2023
  };
1846
2024
 
@@ -321,10 +321,12 @@ export async function probeClaudeDaemonCandidates({
321
321
  short,
322
322
  sessionId,
323
323
  timeoutMs = 6000,
324
+ tmpRoot = "/tmp",
324
325
  } = {}) {
325
326
  const candidates = await buildClaudeDaemonDiscoveryCandidates({
326
327
  configDir,
327
328
  env,
329
+ tmpRoot,
328
330
  });
329
331
  const callerProvenance = detectCallerProvenance(env);
330
332
  const targetRequested = Boolean(short || sessionId);
@@ -23,6 +23,8 @@ import {
23
23
  } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
 
26
+ import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
27
+
26
28
  export const STATES = Object.freeze({
27
29
  PLANNING: "PLANNING",
28
30
  EXECUTING: "EXECUTING",
@@ -39,21 +41,29 @@ export const MODES = Object.freeze({
39
41
  });
40
42
 
41
43
  const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
44
+ const DEFAULT_CLAUDE_MODEL = "fable";
42
45
 
43
- // Escalation chain (2026-05-27 정책):
44
- // 1. codex gpt-5.6-sol max — 최난도 단일 작업용 Codex escalation 단계
45
- // 2. claude opus-4-8 — 최종 수단
46
+ // Escalation chain (2026-09-22 정책):
47
+ // 1. codex gpt-6-astra max: 최난도 단일 작업용 Codex escalation 단계
48
+ // 2. claude fable alias: Claude CLI가 최신 Fable tier로 해석하는 최종 수단
46
49
  const DEFAULT_ESCALATION_CHAIN = Object.freeze([
47
50
  Object.freeze({
48
51
  cli: "codex",
49
- model: "gpt-5.6-sol",
50
- profile: "gpt56_sol_max",
52
+ model: "gpt-6-astra",
53
+ profile: "gpt6_astra_max",
51
54
  }),
52
- Object.freeze({ cli: "claude", model: "opus-4-8" }),
55
+ Object.freeze({ cli: "claude", model: DEFAULT_CLAUDE_MODEL }),
53
56
  ]);
54
57
 
55
58
  const STUCK_THRESHOLD = 3;
56
59
 
60
+ function normalizeEscalationProfile(profile) {
61
+ const normalized = normalizeCodexProfileName(profile);
62
+ return normalized === "ultra" || normalized === "gpt6_astra_ultra"
63
+ ? "gpt6_astra_max"
64
+ : normalized;
65
+ }
66
+
57
67
  export function createRetryStateMachine(options = {}) {
58
68
  const mode = options.mode || MODES.BOUNDED;
59
69
  const cliChain = resolveEscalationChain(options);
@@ -234,15 +244,25 @@ function resolveEscalationChain(options) {
234
244
  }
235
245
 
236
246
  const projectRoot = options.projectRoot || process.cwd();
237
- return (
238
- loadEscalationChainOverride(projectRoot) ||
239
- normalizeEscalationChain(
240
- DEFAULT_ESCALATION_CHAIN,
241
- "DEFAULT_ESCALATION_CHAIN",
242
- )
247
+ const projectOverride = loadEscalationChainOverride(projectRoot);
248
+ if (projectOverride) return projectOverride;
249
+
250
+ return normalizeEscalationChain(
251
+ resolveDefaultEscalationChain(options.env || process.env),
252
+ "DEFAULT_ESCALATION_CHAIN",
243
253
  );
244
254
  }
245
255
 
256
+ function resolveDefaultEscalationChain(env) {
257
+ const configuredModel = env?.TFX_ESCALATION_CLAUDE_MODEL?.trim();
258
+ if (!configuredModel) return DEFAULT_ESCALATION_CHAIN;
259
+
260
+ return [
261
+ DEFAULT_ESCALATION_CHAIN[0],
262
+ { cli: "claude", model: configuredModel },
263
+ ];
264
+ }
265
+
246
266
  export function loadEscalationChainOverride(projectRoot = process.cwd()) {
247
267
  const configPath = join(projectRoot, ESCALATION_CHAIN_CONFIG_PATH);
248
268
  if (!existsSync(configPath)) return null;
@@ -297,10 +317,7 @@ function normalizeEscalationChainEntry(entry, index, source) {
297
317
  if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
298
318
  const profile = entry.profile.trim();
299
319
  normalized.profile =
300
- entry.cli === "codex" &&
301
- (profile === "ultra" || profile === "gpt56_sol_ultra")
302
- ? "gpt56_sol_max"
303
- : profile;
320
+ entry.cli === "codex" ? normalizeEscalationProfile(profile) : profile;
304
321
  }
305
322
  return normalized;
306
323
  }
@@ -333,6 +350,12 @@ export function loadSnapshot(snapshotFile) {
333
350
  `unsupported snapshot version: ${parsed.version} (expected 1)`,
334
351
  );
335
352
  }
353
+ if (Array.isArray(parsed.cliChain) && parsed.cliChain.length > 0) {
354
+ return {
355
+ ...parsed,
356
+ cliChain: normalizeEscalationChain(parsed.cliChain, "snapshot.cliChain"),
357
+ };
358
+ }
336
359
  return parsed;
337
360
  }
338
361
 
@@ -0,0 +1,69 @@
1
+ // HUD CLI 표시 정책 판정 (순수 함수)
2
+ //
3
+ // machine profile 의 TFX_DISABLE_CODEX / TFX_DISABLE_ANTIGRAVITY 를 읽어서 HUD 가
4
+ // 어떤 프로바이더 행을 그릴지 정한다. 판독 자체는 scripts/lib/machine-profile.mjs
5
+ // 가 단일 정본이고, 이 모듈은 그 결과를 HUD 표시 결정으로 접기만 한다.
6
+ //
7
+ // 상대 경로 import 를 고정하는 이유: setup 이 hud/** 를 ~/.claude/hud/ 로,
8
+ // scripts/lib/* 를 ~/.claude/scripts/lib/ 로 각각 동기화한다. 그래서 설치본에서도
9
+ // ../scripts/lib/ 가 repo 와 같은 파일을 가리킨다. packages/core 와
10
+ // packages/triflux 미러도 같은 상대 레이아웃이다.
11
+ //
12
+ // fail-open 원칙: HUD 는 statusline 이라 어떤 예외에서도 죽으면 안 된다. 정책
13
+ // 판독이 실패하면 전부 표시하는 쪽으로 돌아간다.
14
+
15
+ import { resolveCliPolicy } from "../scripts/lib/machine-profile.mjs";
16
+
17
+ /**
18
+ * HUD 가 codex / antigravity 행을 그릴 수 있는지 판정한다.
19
+ *
20
+ * @param {NodeJS.ProcessEnv} [env]
21
+ * @param {{
22
+ * policy?: ReturnType<typeof resolveCliPolicy>|null,
23
+ * resolve?: (env: NodeJS.ProcessEnv, opts: object) => ReturnType<typeof resolveCliPolicy>,
24
+ * profile?: object,
25
+ * platform?: string,
26
+ * home?: string,
27
+ * }} [opts] policy 는 이미 계산한 정책 주입, resolve 는 테스트용 판독 시임이다.
28
+ * @returns {{ showCodex: boolean, antigravityAllowed: boolean, policy: object|null }}
29
+ */
30
+ export function resolveHudCliVisibility(env = process.env, opts = {}) {
31
+ try {
32
+ const resolver =
33
+ typeof opts.resolve === "function" ? opts.resolve : resolveCliPolicy;
34
+ const policy = opts.policy ?? resolver(env, opts);
35
+ return {
36
+ showCodex: policy?.codexDisabled !== true,
37
+ antigravityAllowed: policy?.antigravityDisabled !== true,
38
+ policy: policy ?? null,
39
+ };
40
+ } catch {
41
+ return { showCodex: true, antigravityAllowed: true, policy: null };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * antigravity / gemini 슬롯 행을 그릴지 판정한다.
47
+ *
48
+ * antigravity 가 허용이면 기존 동작 그대로 항상 그린다. 정책으로 꺼졌을 때만
49
+ * gemini 폴백 정보가 하나라도 있는지 보고, 전부 비어 있으면 행을 생략한다.
50
+ *
51
+ * @param {{
52
+ * antigravityAllowed?: boolean,
53
+ * antigravityReady?: boolean,
54
+ * geminiEmail?: unknown,
55
+ * geminiBucket?: unknown,
56
+ * geminiSession?: unknown,
57
+ * }} [args]
58
+ * @returns {boolean}
59
+ */
60
+ export function shouldRenderGeminiFallbackRow({
61
+ antigravityAllowed = true,
62
+ antigravityReady = false,
63
+ geminiEmail = null,
64
+ geminiBucket = null,
65
+ geminiSession = null,
66
+ } = {}) {
67
+ if (antigravityAllowed || antigravityReady) return true;
68
+ return Boolean(geminiEmail || geminiBucket || geminiSession);
69
+ }
package/hud/constants.mjs CHANGED
@@ -71,7 +71,7 @@ export const CONTEXT_MONITOR_LOG_DIR = join(
71
71
  "logs",
72
72
  );
73
73
 
74
- // 원격 프로브 캐시 (tfx-remote-spawn)
74
+ // 원격 프로브 캐시 (tfx-remote)
75
75
  export const REMOTE_ENV_CACHE_DIR = join(
76
76
  homedir(),
77
77
  ".claude",
@@ -89,12 +89,19 @@ export const CLAUDE_CREDENTIALS_PATH = join(
89
89
  );
90
90
  export const CLAUDE_CREDENTIALS_FILENAME = ".credentials.json";
91
91
  export const CLAUDE_CODE_USER_AGENT_FALLBACK = "claude-code/2.x";
92
+ // Claude Code 2.1.x 평문 폴백 파일 위치: CLAUDE_SECURESTORAGE_CONFIG_DIR 가 정의돼 있으면 그 디렉터리
93
+ // (빈 문자열은 기본 ~/.claude), 아니면 CLAUDE_CONFIG_DIR. 기본 경로는 항상 마지막 후보로 둔다.
92
94
  export function getClaudeCredentialPaths(env = process.env) {
93
95
  const paths = [];
94
- if (env.CLAUDE_CONFIG_DIR) {
96
+ const secureDir = env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
97
+ if (secureDir !== undefined) {
98
+ if (secureDir) {
99
+ paths.push(join(secureDir.normalize("NFC"), CLAUDE_CREDENTIALS_FILENAME));
100
+ }
101
+ } else if (env.CLAUDE_CONFIG_DIR) {
95
102
  paths.push(join(env.CLAUDE_CONFIG_DIR, CLAUDE_CREDENTIALS_FILENAME));
96
103
  }
97
- paths.push(CLAUDE_CREDENTIALS_PATH);
104
+ if (paths[0] !== CLAUDE_CREDENTIALS_PATH) paths.push(CLAUDE_CREDENTIALS_PATH);
98
105
  return paths;
99
106
  }
100
107
  export function getClaudeCodeUserAgent(env = process.env) {
@@ -198,7 +205,16 @@ export const ANTIGRAVITY_SETTINGS_PATH = join(
198
205
  // /usage 실측 (2026-05-22): Gemini 4개 모델이 동일 reset window 공유 (family-pool).
199
206
  // Claude/GPT-OSS family는 Gemini와 별도 quota 윈도우.
200
207
  export const ANTIGRAVITY_MODEL_ABBREV = {
201
- // Gemini family — 통합 quota
208
+ // Gemini family — 통합 quota. 3.6+ Flash 는 세대 숫자 + 등급으로 구분한다.
209
+ "Gemini 3.8 Flash (High)": "8h",
210
+ "Gemini 3.8 Flash (Medium)": "8m",
211
+ "Gemini 3.8 Flash (Low)": "8l",
212
+ "Gemini 3.7 Flash (High)": "7h",
213
+ "Gemini 3.7 Flash (Medium)": "7m",
214
+ "Gemini 3.7 Flash (Low)": "7l",
215
+ "Gemini 3.6 Flash (High)": "6h",
216
+ "Gemini 3.6 Flash (Medium)": "6m",
217
+ "Gemini 3.6 Flash (Low)": "6l",
202
218
  "Gemini 3.5 Flash (High)": "Fh",
203
219
  "Gemini 3.5 Flash (Medium)": "Fm",
204
220
  "Gemini 3.5 Flash (Low)": "Fl",
@@ -242,12 +258,6 @@ export const GEMINI_RPM_TRACKER_PATH = join(
242
258
  "cache",
243
259
  "gemini-rpm-tracker.json",
244
260
  );
245
- export const SV_ACCUMULATOR_PATH = join(
246
- homedir(),
247
- ".claude",
248
- "cache",
249
- "sv-accumulator.json",
250
- );
251
261
  // 이전 .omc/ 경로 fallback (기존 환경 호환)
252
262
  export const LEGACY_GEMINI_QUOTA_CACHE = join(
253
263
  homedir(),
@@ -273,12 +283,6 @@ export const LEGACY_GEMINI_RPM_TRACKER = join(
273
283
  "state",
274
284
  "gemini_rpm_tracker.json",
275
285
  );
276
- export const LEGACY_SV_ACCUMULATOR = join(
277
- homedir(),
278
- ".omc",
279
- "state",
280
- "sv-accumulator.json",
281
- );
282
286
 
283
287
  export const GEMINI_RPM_WINDOW_MS = 60 * 1000; // 60초 슬라이딩 윈도우
284
288
  export const GEMINI_QUOTA_STALE_MS = 5 * 60 * 1000; // 5분
@@ -289,7 +293,6 @@ export const ACCOUNT_LABEL_WIDTH = 10;
289
293
  export const PROVIDER_PREFIX_WIDTH = 2;
290
294
  export const PERCENT_CELL_WIDTH = 3;
291
295
  export const TIME_CELL_INNER_WIDTH = 6;
292
- export const SV_CELL_WIDTH = 5;
293
296
 
294
297
  export const CLAUDE_REFRESH_FLAG = "--refresh-claude-usage";
295
298
  export const CODEX_REFRESH_FLAG = "--refresh-codex-rate-limits";