@triflux/core 10.52.0 → 10.53.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/bridge.mjs CHANGED
@@ -465,9 +465,8 @@ async function cmdDaemonWait(args) {
465
465
  if (!payload.short && !payload.sessionId)
466
466
  throw new Error("short or sessionId is required");
467
467
  const { probeClaudeDaemonCandidates } = await loadDaemonControl();
468
- const { findClaudeTranscript, readClaudeTranscript } = await import(
469
- "./team/claude-transcript.mjs"
470
- );
468
+ const { claudeWaitVerdict, findClaudeTranscript, readClaudeTranscript } =
469
+ await import("./team/claude-transcript.mjs");
471
470
  const timeoutMs = numericOption(payload.timeoutMs, 30_000);
472
471
  const pollIntervalMs = Math.max(
473
472
  10,
@@ -535,28 +534,8 @@ async function cmdDaemonWait(args) {
535
534
  target: probe.target,
536
535
  ...daemonProbeMetadata(probe),
537
536
  };
538
- if (transcript?.error) {
539
- return emitJson({
540
- ...last,
541
- ok: false,
542
- status: "failed",
543
- error: transcript.error,
544
- });
545
- }
546
- if (
547
- (idle || transcript?.sectionClosed) &&
548
- transcript?.userSeen &&
549
- transcript.turnEnded
550
- ) {
551
- if (!transcript.response.trim())
552
- return emitJson({
553
- ...last,
554
- ok: false,
555
- status: "failed",
556
- error: "turn ended without assistant text",
557
- });
558
- return emitJson({ ...last, status: "completed", done: true });
559
- }
537
+ const verdict = claudeWaitVerdict(transcript, idle);
538
+ if (verdict) return emitJson({ ...last, ...verdict });
560
539
  if (Date.now() >= deadline) break;
561
540
  await new Promise((resolve) =>
562
541
  setTimeout(resolve, Math.min(pollIntervalMs, deadline - Date.now())),
@@ -15,61 +15,6 @@ import {
15
15
  import { IS_WINDOWS, killProcess } from "./platform.mjs";
16
16
  import { createInterventionLadder } from "./team/intervention.mjs";
17
17
 
18
- // ── Quota retry-after 파싱 ──────────────────────────────────────
19
-
20
- const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
21
-
22
- /**
23
- * 에러 텍스트에서 retry-after 시간을 동적 파싱.
24
- * "try again at 2026-04-11T10:00:00Z", "retry after 3600 seconds",
25
- * "resets in 7 days", "wait 5 hours" 등을 감지.
26
- */
27
- export function parseRetryAfterMs(text, provider) {
28
- // 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
29
- const isoMatch = text.match(
30
- /(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
31
- );
32
- if (isoMatch) {
33
- const target = new Date(isoMatch[1]).getTime();
34
- if (target > Date.now()) return target - Date.now();
35
- }
36
-
37
- // 2. Duration: "N seconds/minutes/hours/days"
38
- const durMatch = text.match(
39
- /(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
40
- );
41
- if (durMatch) {
42
- const n = Number(durMatch[1]);
43
- const unit = durMatch[2].toLowerCase();
44
- const multipliers = {
45
- second: 1000,
46
- minute: 60_000,
47
- hour: 3600_000,
48
- day: 86400_000,
49
- week: 604800_000,
50
- };
51
- return n * (multipliers[unit] || 3600_000);
52
- }
53
-
54
- // 3. "7 days" / "24 hours" 단독 (context 없이)
55
- const standaloneMatch = text.match(/(\d+)\s*(day|hour|week|minute)/i);
56
- if (standaloneMatch) {
57
- const n = Number(standaloneMatch[1]);
58
- const unit = standaloneMatch[2].toLowerCase();
59
- const multipliers = {
60
- minute: 60_000,
61
- hour: 3600_000,
62
- day: 86400_000,
63
- week: 604800_000,
64
- };
65
- const parsed = n * (multipliers[unit] || 3600_000);
66
- if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
67
- }
68
-
69
- // 4. fallback: provider 기본값
70
- return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
71
- }
72
-
73
18
  // ── Shell utilities ─────────────────────────────────────────────
74
19
 
75
20
  export function normalizePathForShell(value) {
@@ -195,47 +140,28 @@ export function appendWarnings(stderr, warnings = []) {
195
140
  return [stderr, text].filter(Boolean).join("\n");
196
141
  }
197
142
 
198
- // ── Broker-integrated execution ─────────────────────────────────
143
+ // ── Attempt execution ───────────────────────────────────────────
199
144
 
200
145
  /**
201
- * Shared execute() logic that uses account-broker for per-account circuit
202
- * breaking instead of a global breaker.
146
+ * preflight 뒤 buildAttemptsFn 이 만든 시도를 순서대로 실행한다.
203
147
  *
204
148
  * @param {object} params
205
- * @param {string} params.provider — 'codex' | 'gemini'
206
149
  * @param {(prompt: string, workdir: string, preflight: object, attempt: object) => Promise<object>} params.runFn
207
150
  * @param {(opts: object) => Promise<object>} params.preflightFn
208
151
  * @param {(opts: object, preflight: object) => object[]} params.buildAttemptsFn
209
152
  * @param {object} params.opts — caller-supplied execute options
210
153
  * @returns {Promise<object>} createResult-shaped result
211
154
  */
212
- export async function executeWithCircuitBroker({
213
- provider,
155
+ export async function executeWithAttempts({
214
156
  runFn,
215
157
  preflightFn,
216
158
  buildAttemptsFn,
217
159
  opts = {},
218
160
  }) {
219
- // late-import to avoid circular dependency at module load time
220
- const brokerMod = await import("./account-broker.mjs");
221
161
  const { withRetry } = await import("./workers/worker-utils.mjs");
222
162
 
223
- // access broker as live binding property (not destructured) so reloadBroker() propagates
224
- const hasBroker = brokerMod.broker != null;
225
- // Empty broker (TFX_DISABLE_ACCOUNT_BROKER=1 or zero accounts) must not
226
- // gate execution. lease() is null because there are no accounts to lease,
227
- // not because all accounts are busy/cooldown/circuit. Treat it like
228
- // hasBroker=false and fall through to the default auth path.
229
- const brokerDisabled = hasBroker && brokerMod.broker.isDisabled === true;
230
- const effectiveBroker = hasBroker && !brokerDisabled;
231
- const lease = effectiveBroker ? brokerMod.broker.lease({ provider }) : null;
232
- if (effectiveBroker && !lease) {
233
- return createResult(false, { fellBack: true, failureMode: "circuit_open" });
234
- }
235
-
236
163
  const preflight = await preflightFn(opts);
237
164
  if (!preflight.ok) {
238
- if (lease) brokerMod.broker.release(lease.id, { ok: false });
239
165
  return createResult(false, {
240
166
  stderr: appendWarnings("", preflight.warnings),
241
167
  fellBack: opts.fallbackToClaude !== false,
@@ -250,15 +176,11 @@ export async function executeWithCircuitBroker({
250
176
  try {
251
177
  lastResult = await withRetry(
252
178
  async () => {
253
- // PRD A1: lease 메타데이터를 runFn 에 전달해서 adapter 가 실제 spawn 시
254
- // 해당 account 의 authFile/env/profile 을 적용할 수 있게 한다. lease=null
255
- // 이면 adapter 는 default 경로 (단일 ~/.codex/auth.json) 로 동작한다.
256
179
  const result = await runFn(
257
180
  opts.prompt || "",
258
181
  opts.workdir || process.cwd(),
259
182
  preflight,
260
183
  attempts[attemptIndex],
261
- lease,
262
184
  );
263
185
  const current = {
264
186
  ...result,
@@ -286,26 +208,7 @@ export async function executeWithCircuitBroker({
286
208
  createResult(false, { stderr: String(error?.message || error) });
287
209
  }
288
210
 
289
- if (lastResult.ok) {
290
- if (lease) brokerMod.broker.release(lease.id, { ok: true });
291
- return lastResult;
292
- }
293
-
294
- if (lease) {
295
- if (lastResult.failureMode === "rate_limited") {
296
- const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
297
- const coolMs = parseRetryAfterMs(text, provider);
298
- brokerMod.broker.markRateLimited(lease.id, coolMs);
299
- brokerMod.broker.emit("cooldown", {
300
- id: lease.id,
301
- provider,
302
- coolMs,
303
- reason: "quota_exhausted",
304
- });
305
- } else {
306
- brokerMod.broker.release(lease.id, { ok: false });
307
- }
308
- }
211
+ if (lastResult.ok) return lastResult;
309
212
  return {
310
213
  ...lastResult,
311
214
  retried: attempts.length > 1,
@@ -374,13 +277,10 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
374
277
  let child;
375
278
 
376
279
  try {
377
- // PRD A1: opts.spawnEnv 가 있으면 그 env 로 spawn (lease 의 authFile/env 적용).
378
- // undefined 면 spawn 의 default 동작 (부모 process env inherit) 유지.
379
280
  child = spawnProcess(command, {
380
281
  cwd: workdir,
381
282
  shell: true,
382
283
  windowsHide: true,
383
- env: opts.spawnEnv,
384
284
  });
385
285
  } catch (error) {
386
286
  return createResult(false, {
@@ -1,11 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { basename, join } from "node:path";
3
+ import { join } from "node:path";
4
4
  import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
5
5
  import {
6
6
  buildExecCommand,
7
- createResult,
8
- executeWithCircuitBroker,
7
+ executeWithAttempts,
9
8
  normalizePathForShell,
10
9
  runProcess,
11
10
  shellQuote,
@@ -113,7 +112,7 @@ export function buildLaunchScript(opts = {}) {
113
112
 
114
113
  // CTO boundary: this adapter assembles Codex prompts outside
115
114
  // scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
116
- // launcher/circuit-broker workdir contracts have focused coverage.
115
+ // launcher workdir contracts have focused coverage.
117
116
  export function buildExecArgs(opts = {}) {
118
117
  const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
119
118
  const command = buildExecCommand(prompt, opts.resultFile || null, {
@@ -153,7 +152,7 @@ export function buildExecArgs(opts = {}) {
153
152
 
154
153
  // ── Codex execution ─────────────────────────────────────────────
155
154
 
156
- async function runCodex(prompt, workdir, preflight, attempt, lease) {
155
+ async function runCodex(prompt, workdir, preflight, attempt) {
157
156
  const dir = join(tmpdir(), "triflux-codex-exec");
158
157
  mkdirSync(dir, { recursive: true });
159
158
  const resultFile = join(
@@ -162,7 +161,7 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
162
161
  );
163
162
  const command = commandWithOverrides(
164
163
  buildExecCommand(prompt, resultFile, {
165
- profile: lease?.profile ?? attempt.profile,
164
+ profile: attempt.profile,
166
165
  skipGitRepoCheck: true,
167
166
  sandboxBypass: attempt.forceBypass,
168
167
  }),
@@ -170,125 +169,17 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
170
169
  preflight.codexPath,
171
170
  buildOverrides(attempt.requested, attempt.excluded),
172
171
  );
173
- // PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
174
- // 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
175
- // account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
176
- // 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
177
- const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
178
- const authCheck = validateLeaseAuth(lease, spawnEnv);
179
- if (!authCheck.ok) {
180
- return createResult(false, {
181
- stderr: authCheck.error,
182
- failureMode: "auth_sync",
183
- skipCircuit: true,
184
- });
185
- }
186
172
  return runProcess(command, workdir, attempt.timeout, {
187
173
  resultFile,
188
174
  inferStallMode,
189
- spawnEnv,
190
175
  cli: "codex",
191
- codexHome: spawnEnv?.CODEX_HOME,
192
176
  });
193
177
  }
194
178
 
195
- function buildLeaseSpawnEnv(lease) {
196
- const extra = {};
197
- if (lease.authFile) {
198
- // lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
199
- // codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
200
- // 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
201
- // 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
202
- // 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
203
- // 실제로 있을 때만 적용한다 (false-positive 회피).
204
- try {
205
- const dir = dirnameOf(lease.authFile);
206
- if (dir) extra.CODEX_HOME = dir;
207
- } catch {}
208
- }
209
- if (lease.env && typeof lease.env === "object") {
210
- Object.assign(extra, lease.env);
211
- }
212
- return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
213
- }
214
-
215
- function validateLeaseAuth(lease, spawnEnv) {
216
- if (!lease?.authFile) return { ok: true };
217
- if (basename(lease.authFile) !== "auth.json") {
218
- return {
219
- ok: false,
220
- error: `lease authFile must resolve to auth.json for account ${lease.id}`,
221
- };
222
- }
223
- if (!existsSync(lease.authFile)) {
224
- return {
225
- ok: false,
226
- error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
227
- };
228
- }
229
- if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
230
- return {
231
- ok: false,
232
- // This is currently expected to match because buildLeaseSpawnEnv derives
233
- // CODEX_HOME from authFile. Keep the guard so future lease.env overrides
234
- // cannot silently point Codex at a different account home.
235
- error: `CODEX_HOME mismatch for account ${lease.id}`,
236
- };
237
- }
238
- try {
239
- const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
240
- const accountId =
241
- parsed?.tokens?.account_id ??
242
- parsed?.account_id ??
243
- parsed?.accountId ??
244
- null;
245
- if (!accountId) {
246
- return {
247
- ok: false,
248
- error: `lease authFile has no account_id for account ${lease.id}`,
249
- };
250
- }
251
- if (accountId !== lease.id) {
252
- return {
253
- ok: false,
254
- error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
255
- };
256
- }
257
- } catch (error) {
258
- return {
259
- ok: false,
260
- error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
261
- };
262
- }
263
- return { ok: true };
264
- }
265
-
266
- function dirnameOf(filePath) {
267
- if (typeof filePath !== "string" || !filePath) return null;
268
- const lastSep = Math.max(
269
- filePath.lastIndexOf("/"),
270
- filePath.lastIndexOf("\\"),
271
- );
272
- if (lastSep < 0) return null;
273
- return filePath.slice(0, lastSep);
274
- }
275
-
276
179
  // ── Public API ──────────────────────────────────────────────────
277
180
 
278
- export async function getCircuitState() {
279
- const brokerMod = await import("./account-broker.mjs");
280
- if (!brokerMod.broker) return { state: "closed", failures: [] };
281
- const snap = brokerMod.broker
282
- .snapshot()
283
- .filter((a) => a.provider === "codex");
284
- return snap.length
285
- ? { state: snap[0].circuitState, accounts: snap }
286
- : { state: "closed", failures: [] };
287
- }
288
-
289
181
  export function execute(opts = {}) {
290
- return executeWithCircuitBroker({
291
- provider: "codex",
182
+ return executeWithAttempts({
292
183
  runFn: runCodex,
293
184
  preflightFn: (o) =>
294
185
  runPreflight({
package/hub/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // @triflux/core — barrel export (auto-generated by pack.mjs)
2
2
  // Adapters
3
3
  export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
4
- export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } from './codex-adapter.mjs';
4
+ export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript } from './codex-adapter.mjs';
5
5
  export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
6
6
 
7
7
  // Platform
@@ -9,4 +9,3 @@ export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, ki
9
9
 
10
10
  // Core services
11
11
  export { parseArgs, parseJsonSafe } from './bridge.mjs';
12
- export { broker as accountBroker } from './account-broker.mjs';
@@ -157,6 +157,25 @@ function acceptLine(state, line, marker) {
157
157
  }
158
158
  }
159
159
 
160
+ // daemon 과 tmux wait 가 같은 기준으로 끝을 판정한다. 아직이면 null.
161
+ export function claudeWaitVerdict(transcript, idle) {
162
+ if (transcript?.error)
163
+ return { ok: false, status: "failed", error: transcript.error };
164
+ if (
165
+ !transcript?.userSeen ||
166
+ !transcript.turnEnded ||
167
+ !(idle || transcript.sectionClosed)
168
+ )
169
+ return null;
170
+ if (!transcript.response.trim())
171
+ return {
172
+ ok: false,
173
+ status: "failed",
174
+ error: "turn ended without assistant text",
175
+ };
176
+ return { status: "completed", done: true };
177
+ }
178
+
160
179
  export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
161
180
  if (!transcriptPath) return null;
162
181
  const marker = requestId ? `[tfx-live req=${requestId}]` : null;
package/hud/constants.mjs CHANGED
@@ -6,21 +6,6 @@ import { join } from "node:path";
6
6
 
7
7
  export const VERSION = "2.0";
8
8
 
9
- export const ACCOUNTS_CONFIG_PATH = join(
10
- homedir(),
11
- ".claude",
12
- "cache",
13
- "tfx-hub",
14
- "accounts.json",
15
- );
16
- export const ACCOUNTS_STATE_PATH = join(
17
- homedir(),
18
- ".claude",
19
- "cache",
20
- "tfx-hub",
21
- "cli_accounts_state.json",
22
- );
23
-
24
9
  // triflux-mods 의 Claude band 가 세션마다 갱신하는 표식. 있으면 Claude 행을 뺀다.
25
10
  export const CLAUDE_BAND_MARKER_DIR = join(
26
11
  homedir(),
@@ -107,19 +92,19 @@ export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
107
92
  export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
108
93
  export const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
109
94
 
110
- export const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
95
+ // Codex CLI 는 CODEX_HOME 이 있으면 그 디렉터리를 쓴다. 호출 시점에 읽어 환경 변화를 따른다.
96
+ export function getCodexHome() {
97
+ return process.env.CODEX_HOME || join(homedir(), ".codex");
98
+ }
99
+ export function getCodexAuthPath() {
100
+ return join(getCodexHome(), "auth.json");
101
+ }
111
102
  export const CODEX_PROBE_STATE_PATH = join(
112
103
  homedir(),
113
104
  ".claude",
114
105
  "cache",
115
106
  "codex-probe-state.json",
116
107
  );
117
- export const CODEX_BROKER_AUTH_CACHE_DIR = join(
118
- homedir(),
119
- ".claude",
120
- "cache",
121
- "tfx-hub",
122
- );
123
108
  export const CODEX_PROBE_TTL_MS = 5 * 60 * 1000;
124
109
  export const CODEX_PROBE_TIMEOUT_MS = 10_000;
125
110
  export const CODEX_QUOTA_CACHE_PATH = join(
@@ -14,14 +14,13 @@ import {
14
14
  RESET,
15
15
  } from "./colors.mjs";
16
16
  import {
17
- ACCOUNTS_CONFIG_PATH,
18
- ACCOUNTS_STATE_PATH,
19
17
  ANTIGRAVITY_REFRESH_FLAG,
20
18
  CLAUDE_BAND_MARKER_DIR,
21
19
  CLAUDE_BAND_MARKER_TTL_MS,
22
20
  CLAUDE_REFRESH_FLAG,
23
- CODEX_AUTH_PATH,
24
21
  CODEX_REFRESH_FLAG,
22
+ getCodexAuthPath,
23
+ getCodexHome,
25
24
  } from "./constants.mjs";
26
25
  import { buildContextUsageView } from "./context-monitor.mjs";
27
26
  import {
@@ -31,7 +30,6 @@ import {
31
30
  } from "./providers/claude.mjs";
32
31
  import {
33
32
  getCodexEmail,
34
- hasBrokerCodexAccounts,
35
33
  readCodexRateLimitSnapshot,
36
34
  refreshCodexRateLimitsCache,
37
35
  scheduleCodexRateLimitRefresh,
@@ -50,12 +48,7 @@ import {
50
48
  renderAlignedRows,
51
49
  } from "./renderers.mjs";
52
50
  import { selectTier } from "./terminal.mjs";
53
- import {
54
- formatTimeCell,
55
- formatTimeCellDH,
56
- readJson,
57
- readStdinJson,
58
- } from "./utils.mjs";
51
+ import { formatTimeCell, formatTimeCellDH, readStdinJson } from "./utils.mjs";
59
52
 
60
53
  async function main() {
61
54
  if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
@@ -73,8 +66,6 @@ async function main() {
73
66
 
74
67
  const stdinPromise = readStdinJson();
75
68
  const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
76
- const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
77
- const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
78
69
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
79
70
  const codexSnapshot = readCodexRateLimitSnapshot();
80
71
  const antigravitySnapshot = antigravityAllowed
@@ -88,11 +79,7 @@ async function main() {
88
79
  ) {
89
80
  scheduleClaudeUsageRefresh();
90
81
  }
91
- if (
92
- showCodex &&
93
- codexSnapshot.shouldRefresh &&
94
- (existsSync(join(homedir(), ".codex")) || hasBrokerCodexAccounts())
95
- ) {
82
+ if (showCodex && codexSnapshot.shouldRefresh && existsSync(getCodexHome())) {
96
83
  scheduleCodexRateLimitRefresh();
97
84
  }
98
85
 
@@ -106,9 +93,13 @@ async function main() {
106
93
  ? { ...antigravitySnapshot?.data, auth: getAntigravityAuthKind() }
107
94
  : null;
108
95
  const currentTier = selectTier();
96
+ // 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
97
+ const codexLoggedOut =
98
+ showCodex && !codexBuckets && !existsSync(getCodexAuthPath());
109
99
  if (currentTier === "nano") {
110
100
  const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
111
101
  showCodex,
102
+ codexLoggedOut,
112
103
  showAntigravity: antigravityAllowed,
113
104
  antigravityQuota,
114
105
  });
@@ -128,8 +119,6 @@ async function main() {
128
119
  "codex",
129
120
  "x",
130
121
  codexWhite,
131
- accountsConfig,
132
- accountsState,
133
122
  codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
134
123
  getCodexEmail(),
135
124
  ),
@@ -142,8 +131,6 @@ async function main() {
142
131
  "antigravity",
143
132
  "a",
144
133
  geminiBlue,
145
- accountsConfig,
146
- accountsState,
147
134
  antigravityQuota,
148
135
  getAntigravityAccountLabel(),
149
136
  ),
@@ -151,13 +138,7 @@ async function main() {
151
138
  }
152
139
 
153
140
  const outputLines = renderAlignedRows(rows);
154
- // 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
155
- if (
156
- outputLines[codexRowIndex] != null &&
157
- !codexBuckets &&
158
- !existsSync(CODEX_AUTH_PATH) &&
159
- !hasBrokerCodexAccounts()
160
- ) {
141
+ if (outputLines[codexRowIndex] != null && codexLoggedOut) {
161
142
  outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
162
143
  }
163
144
  // 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
@@ -2,16 +2,8 @@
2
2
  // 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
3
3
 
4
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";
5
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
15
7
 
16
8
  function readJwtExpSec(token) {
17
9
  try {
@@ -54,7 +46,6 @@ export function recordProbe(stateFilePath, key, nowMs) {
54
46
 
55
47
  export function listProbeTargets({
56
48
  codexAuthPath,
57
- brokerCacheDir,
58
49
  stateFilePath,
59
50
  ttlMs,
60
51
  nowMs,
@@ -68,26 +59,6 @@ export function listProbeTargets({
68
59
  if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
69
60
  targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
70
61
  }
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
62
  return targets;
92
63
  }
93
64