@triflux/core 10.87.0 → 10.88.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.
package/hub/team/pane.mjs CHANGED
@@ -4,6 +4,7 @@ import { existsSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { judgeComposerState } from "../lib/jev-addon.mjs";
7
8
  import { privateTmpDir } from "../lib/private-tmp.mjs";
8
9
  import { psmuxExec } from "./psmux.mjs";
9
10
  import { detectMultiplexer, tmuxExec } from "./session.mjs";
@@ -251,6 +252,104 @@ export function submitPromptWithVerification({
251
252
  }
252
253
  }
253
254
 
255
+ /**
256
+ * 결정론적 컴포저 상태 판정이 모호할 때 Jev로 컴포저 상태를 판별해 Enter 재전송 여부를 결정한다.
257
+ */
258
+ export async function submitPromptWithJevVerification({
259
+ cli = "claude",
260
+ prompt = "",
261
+ capture,
262
+ sendEnter,
263
+ sleep = sleepMs,
264
+ settleMs = PASTE_SETTLE_MS,
265
+ retryLimit = SUBMIT_RETRY_LIMIT,
266
+ jevOptions = {},
267
+ } = {}) {
268
+ const strictVerification = isAntigravityCli(cli);
269
+ const promptPrefix = promptComposerNeedle(prompt);
270
+ const isConfirmedSubmitted = (text) => {
271
+ if (!promptPrefix) return true;
272
+ if (!text) return !strictVerification;
273
+ if (hasBusyMarkerOutsideComposer(text)) return true;
274
+ const state = composerPromptState(text, promptPrefix);
275
+ if (state === "absent") return true;
276
+ if (state === "unknown" && !strictVerification) return true;
277
+ return false;
278
+ };
279
+
280
+ try {
281
+ submitPromptWithVerification({
282
+ cli,
283
+ prompt,
284
+ capture,
285
+ sendEnter,
286
+ sleep,
287
+ settleMs,
288
+ retryLimit,
289
+ });
290
+ } catch (err) {
291
+ const capturedText =
292
+ typeof capture === "function" ? String(capture() ?? "") : "";
293
+ const verdict = await judgeComposerState(
294
+ { capturedText, promptPrefix },
295
+ jevOptions,
296
+ );
297
+ if (verdict?.state === "submitted_running") {
298
+ return { submitted: true, jevState: verdict.state };
299
+ }
300
+ if (
301
+ verdict?.state === "stuck_in_composer" &&
302
+ typeof sendEnter === "function"
303
+ ) {
304
+ sendEnter();
305
+ sleep(400);
306
+ const afterText =
307
+ typeof capture === "function" ? String(capture() ?? "") : "";
308
+ if (isConfirmedSubmitted(afterText)) {
309
+ return { submitted: true, jevState: "submitted_running" };
310
+ }
311
+ const postVerdict = await judgeComposerState(
312
+ { capturedText: afterText, promptPrefix },
313
+ jevOptions,
314
+ );
315
+ if (postVerdict?.state === "submitted_running") {
316
+ return { submitted: true, jevState: postVerdict.state };
317
+ }
318
+ }
319
+ throw err;
320
+ }
321
+ const capturedText =
322
+ typeof capture === "function" ? String(capture() ?? "") : "";
323
+ const verdict = await judgeComposerState(
324
+ { capturedText, promptPrefix },
325
+ jevOptions,
326
+ );
327
+ if (
328
+ verdict?.state === "stuck_in_composer" &&
329
+ typeof sendEnter === "function"
330
+ ) {
331
+ sendEnter();
332
+ sleep(400);
333
+ const afterText =
334
+ typeof capture === "function" ? String(capture() ?? "") : "";
335
+ if (isConfirmedSubmitted(afterText)) {
336
+ return { submitted: true, jevState: "submitted_running" };
337
+ }
338
+ const postVerdict = await judgeComposerState(
339
+ { capturedText: afterText, promptPrefix },
340
+ jevOptions,
341
+ );
342
+ return {
343
+ submitted: postVerdict?.state === "submitted_running",
344
+ jevState: postVerdict?.state || verdict.state,
345
+ };
346
+ }
347
+ return {
348
+ submitted: verdict?.state !== "modal_blocking",
349
+ jevState: verdict?.state || null,
350
+ };
351
+ }
352
+
254
353
  function confirmSubmit(target, prompt, cli, sendEnter) {
255
354
  submitPromptWithVerification({
256
355
  cli,
@@ -31,6 +31,7 @@ import {
31
31
  resolveCodexModelEffort,
32
32
  } from "../../scripts/lib/model-effort.mjs";
33
33
  import { AGY_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
34
+ import { judgeSameFailureCause } from "../lib/jev-addon.mjs";
34
35
 
35
36
  export const STATES = Object.freeze({
36
37
  PLANNING: "PLANNING",
@@ -126,10 +127,14 @@ export function createRetryStateMachine(options = {}) {
126
127
  return transition(STATES.DONE);
127
128
  }
128
129
 
129
- function reportVerifyFail(failureReason) {
130
+ function reportVerifyFail(failureReason, failOptions = {}) {
130
131
  const reason = String(failureReason || "unknown");
131
- if (reason === state.lastFailureReason) {
132
+ const isSameCause =
133
+ reason === state.lastFailureReason ||
134
+ (Boolean(state.lastFailureReason) && failOptions.sameCause === true);
135
+ if (isSameCause) {
132
136
  state.stuckCounter += 1;
137
+ state.lastFailureReason = reason;
133
138
  } else {
134
139
  state.stuckCounter = 1;
135
140
  state.lastFailureReason = reason;
@@ -155,6 +160,20 @@ export function createRetryStateMachine(options = {}) {
155
160
  return transition(STATES.DIAGNOSING, { reason });
156
161
  }
157
162
 
163
+ async function reportVerifyFailAsync(failureReason, jevOptions = {}) {
164
+ const reason = String(failureReason || "unknown");
165
+ let sameCause = false;
166
+ if (state.lastFailureReason && reason !== state.lastFailureReason) {
167
+ const verdict = await judgeSameFailureCause(
168
+ state.lastFailureReason,
169
+ reason,
170
+ jevOptions,
171
+ );
172
+ sameCause = verdict?.sameCause === true;
173
+ }
174
+ return reportVerifyFail(reason, { sameCause });
175
+ }
176
+
158
177
  function escalate() {
159
178
  if (state.cliIndex + 1 >= state.cliChain.length) {
160
179
  return transition(STATES.BUDGET_EXCEEDED, {
@@ -271,6 +290,7 @@ export function createRetryStateMachine(options = {}) {
271
290
  startIteration,
272
291
  reportVerifySuccess,
273
292
  reportVerifyFail,
293
+ reportVerifyFailAsync,
274
294
  escalate,
275
295
  on,
276
296
  serialize,
@@ -52,8 +52,14 @@ import {
52
52
  getProviderRow,
53
53
  renderAlignedRows,
54
54
  } from "./renderers.mjs";
55
+ import { sessionStatus } from "./session-status.mjs";
55
56
  import { selectTier } from "./terminal.mjs";
56
- import { formatTimeCell, formatTimeCellDH, readStdinJson } from "./utils.mjs";
57
+ import {
58
+ formatTimeCell,
59
+ formatTimeCellDH,
60
+ readStdinJson,
61
+ stripAnsi,
62
+ } from "./utils.mjs";
57
63
 
58
64
  async function main() {
59
65
  if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
@@ -74,7 +80,12 @@ async function main() {
74
80
  const stdin = await readStdinJson();
75
81
  // agy statusLine 은 같은 HUD 를 호출하되 product 로 자신을 밝히고 터미널 폭을 직접 넘긴다.
76
82
  const agySession = stdin?.product === "antigravity";
77
- if (agySession) {
83
+ const codexSession = stdin?.product === "codex";
84
+ const cacheOnly = process.argv.includes("--cache-only");
85
+ const status = await sessionStatus(stdin);
86
+ const currentMarker = agySession ? "a" : codexSession ? "x" : "c";
87
+ const jevMarker = codexSession ? "c" : "x";
88
+ if (agySession || codexSession) {
78
89
  if (stdin.terminal_width > 0)
79
90
  process.env.COLUMNS = String(stdin.terminal_width);
80
91
  } else {
@@ -94,16 +105,23 @@ async function main() {
94
105
  const antigravitySnapshot = antigravityAllowed
95
106
  ? readAntigravityQuotaSnapshot()
96
107
  : null;
97
- if (antigravitySnapshot?.shouldRefresh) scheduleAntigravityQuotaRefresh();
108
+ if (!cacheOnly && antigravitySnapshot?.shouldRefresh)
109
+ scheduleAntigravityQuotaRefresh();
98
110
  // 설정이 없는 홈에서는 갱신 프로세스를 시작하지 않는다.
99
111
  if (
112
+ !cacheOnly &&
100
113
  !stdinClaudeUsage &&
101
114
  claudeUsageSnapshot.shouldRefresh &&
102
115
  existsSync(join(homedir(), ".claude"))
103
116
  ) {
104
117
  scheduleClaudeUsageRefresh();
105
118
  }
106
- if (showCodex && codexSnapshot.shouldRefresh && existsSync(getCodexHome())) {
119
+ if (
120
+ !cacheOnly &&
121
+ showCodex &&
122
+ codexSnapshot.shouldRefresh &&
123
+ existsSync(getCodexHome())
124
+ ) {
107
125
  scheduleCodexRateLimitRefresh();
108
126
  }
109
127
 
@@ -127,6 +145,9 @@ async function main() {
127
145
  codexLoggedOut,
128
146
  showAntigravity: antigravityAllowed,
129
147
  antigravityQuota,
148
+ sessionStatus: status,
149
+ currentMarker,
150
+ jevMarker,
130
151
  });
131
152
  const header = bandMode ? `${BAND_HEADER}\n` : "";
132
153
  process.stdout.write(`${header}\x1b[0m${microLine}\n`);
@@ -136,15 +157,10 @@ async function main() {
136
157
  const rows =
137
158
  bandState === "claude"
138
159
  ? []
139
- : getClaudeRows(
140
- currentTier,
141
- agySession ? null : contextView,
142
- claudeUsage,
143
- {
144
- accountName: getClaudeEmail(),
145
- promptCache: stdin?.prompt_cache,
146
- },
147
- );
160
+ : getClaudeRows(currentTier, null, claudeUsage, {
161
+ accountName: getClaudeEmail(),
162
+ promptCache: stdin?.prompt_cache,
163
+ });
148
164
  let codexRowIndex = -1;
149
165
  if (showCodex) {
150
166
  codexRowIndex = rows.length;
@@ -169,11 +185,20 @@ async function main() {
169
185
  geminiBlue,
170
186
  antigravityQuota,
171
187
  getAntigravityAccountLabel(),
172
- { contextView: agySession ? contextView : null },
173
188
  ),
174
189
  );
175
190
  }
176
191
 
192
+ for (const row of rows) {
193
+ const marker = stripAnsi(row.prefix);
194
+ const extra =
195
+ marker === `${currentMarker}:`
196
+ ? status.context
197
+ : marker === `${jevMarker}:`
198
+ ? status.jev
199
+ : "";
200
+ if (extra) row.right = [row.right, extra].filter(Boolean).join(" ");
201
+ }
177
202
  const outputLines = renderAlignedRows(rows);
178
203
  if (outputLines[codexRowIndex] != null && codexLoggedOut) {
179
204
  outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
@@ -181,7 +206,11 @@ async function main() {
181
206
  // 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다. band 는 자기 자리에 그리니 띄우지 않는다.
182
207
  // agy 는 statusLine.padding 이 같은 역할을 한다.
183
208
  const leadingBreaks =
184
- bandMode || agySession ? "" : contextView.percent >= 85 ? "\n\n" : "\n";
209
+ bandMode || agySession || codexSession
210
+ ? ""
211
+ : contextView.percent >= 85
212
+ ? "\n\n"
213
+ : "\n";
185
214
  const resetLines = outputLines.map((line) => `\x1b[0m${line}`);
186
215
  // band 는 첫 줄 표지로 이 HUD 가 --band 를 아는지 확인한다.
187
216
  const header = bandMode ? `${BAND_HEADER}\n` : "";
@@ -190,7 +219,7 @@ async function main() {
190
219
 
191
220
  main().catch(() => {
192
221
  process.stdout.write(
193
- `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx:--%`)}\n`,
222
+ `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx:--`)}\n`,
194
223
  );
195
224
  });
196
225
 
package/hud/renderers.mjs CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  SEVEN_DAY_MS,
25
25
  TIME_CELL_INNER_WIDTH,
26
26
  } from "./constants.mjs";
27
- import { buildContextUsageView } from "./context-monitor.mjs";
28
27
  import { cacheGap, claudePromptCache } from "./prompt-cache.mjs";
29
28
  import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
30
29
  import {
@@ -91,7 +90,7 @@ export function getMicroLine(
91
90
  showAntigravity = true,
92
91
  codexLoggedOut = false,
93
92
  } = options;
94
- const ctxView = contextView || buildContextUsageView({});
93
+
95
94
  // Claude 5h/1w
96
95
  const cF =
97
96
  claudeUsage?.fiveHourPercent != null
@@ -144,7 +143,18 @@ export function getMicroLine(
144
143
  : agyText;
145
144
  segments.push(`${marker}${dim(":")}${agyValue}`);
146
145
  }
147
- segments.push(`${dim("ctx:")}${contextPercentText(ctxView)}`);
146
+ if (options.sessionStatus) {
147
+ for (let index = 0; index < segments.length; index += 1) {
148
+ const marker = stripAnsi(segments[index]).split(":")[0];
149
+ const extra =
150
+ marker === options.currentMarker
151
+ ? options.sessionStatus.context
152
+ : marker === options.jevMarker
153
+ ? options.sessionStatus.jev
154
+ : "";
155
+ if (extra) segments[index] += ` ${extra}`;
156
+ }
157
+ }
148
158
  return truncateAnsi(segments.join(" "), cols);
149
159
  }
150
160
 
@@ -0,0 +1,46 @@
1
+ import { buildContextUsageView } from "./context-monitor.mjs";
2
+ import { clampPercent } from "./utils.mjs";
3
+
4
+ export async function sessionStatus(stdin, env = process.env) {
5
+ let jev = "";
6
+ try {
7
+ const { isJevEnabled } = await import("../hub/lib/jev-addon.mjs");
8
+ jev = isJevEnabled({ env }) ? "j" : "";
9
+ } catch {
10
+ // 설정을 판정하지 못한 것은 런타임 호출 실패의 증거가 아니다.
11
+ }
12
+ const window = stdin?.context_window;
13
+ const view = buildContextUsageView(stdin);
14
+ const usageValues = Object.entries(window?.current_usage || {})
15
+ .filter(([key]) =>
16
+ [
17
+ "total_tokens",
18
+ "input_tokens",
19
+ "cache_creation_input_tokens",
20
+ "cache_read_input_tokens",
21
+ ].includes(key),
22
+ )
23
+ .map(([, value]) => value);
24
+ const hasUsage =
25
+ usageValues.length > 0 &&
26
+ usageValues.every((value) => Number.isFinite(value) && value >= 0);
27
+ const hasWindow =
28
+ Number.isFinite(window?.context_window_size) &&
29
+ window.context_window_size > 0;
30
+ const knownZero = hasUsage && usageValues.every((value) => value === 0);
31
+ let context = "ctx:--";
32
+ if (hasWindow && hasUsage && (knownZero || view.source === "stdin.tokens")) {
33
+ const used = knownZero ? 0 : view.usedTokens;
34
+ context = `ctx:${clampPercent((used / window.context_window_size) * 100)}%`;
35
+ } else if (
36
+ !(stdin?.product === "antigravity" && window?.context_window_size === 0)
37
+ ) {
38
+ const used = Number.isFinite(window?.used_percentage)
39
+ ? clampPercent(window.used_percentage)
40
+ : Number.isFinite(window?.remaining_percentage)
41
+ ? 100 - clampPercent(window.remaining_percentage)
42
+ : null;
43
+ if (used != null) context = `ctx:${used}%`;
44
+ }
45
+ return { context, jev };
46
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { promisify } from "node:util";
5
+ import { readCodexContext } from "../hub/team/session-context.mjs";
6
+ import { stripAnsi } from "./utils.mjs";
7
+
8
+ const exec = promisify(execFile);
9
+ const quote = (value) => `'${String(value).replace(/'/g, `'"'"'`)}'`;
10
+
11
+ export async function configureCodexHud(runTmux, target, env = process.env) {
12
+ // 기존 서버가 다른 홈에서 시작됐어도 신규 세션의 HUD는 실행자의 환경을 따른다.
13
+ for (const key of [
14
+ "HOME",
15
+ "CODEX_HOME",
16
+ "XDG_CONFIG_HOME",
17
+ "XDG_STATE_HOME",
18
+ "PATH",
19
+ "TFX_JEV",
20
+ "TFX_JEV_CLI",
21
+ "JEV_API_KEY",
22
+ "TYPESAFE_API_KEY",
23
+ "TFX_DISABLE_CODEX",
24
+ "TFX_DISABLE_ANTIGRAVITY",
25
+ "TFX_MACHINE_PROFILE_PATH",
26
+ ]) {
27
+ try {
28
+ await runTmux(null, [
29
+ "set-environment",
30
+ "-t",
31
+ target,
32
+ ...(env[key] == null ? ["-r", key] : [key, env[key]]),
33
+ ]);
34
+ } catch {
35
+ // execFile의 오류 문자열에 환경변수 값이 포함되지 않게 한다.
36
+ throw new Error(`Codex HUD environment setup failed: ${key}`);
37
+ }
38
+ }
39
+ const { stdout } = await runTmux(null, [
40
+ "display-message",
41
+ "-p",
42
+ "-t",
43
+ target,
44
+ "#{session_id}|#{pane_id}",
45
+ ]);
46
+ const [sessionId, paneId] = stdout.trim().split("|");
47
+ if (!/^\$\d+$/.test(sessionId) || !/^%\d+$/.test(paneId))
48
+ throw new Error("Codex HUD session/pane identity unavailable");
49
+ const command = [
50
+ process.execPath,
51
+ fileURLToPath(import.meta.url),
52
+ sessionId,
53
+ paneId,
54
+ ]
55
+ .map(quote)
56
+ .join(" ")
57
+ .replace(/#/g, "##");
58
+ for (const [key, value] of [
59
+ ["status", "3"],
60
+ ["status-interval", "5"],
61
+ ["status-format[0]", `#(${command} #{client_width})`],
62
+ ...[1, 2].map((n) => [`status-format[${n}]`, `#{@tfx_hud_${n}}`]),
63
+ ])
64
+ await runTmux(null, ["set-option", "-t", target, key, value]);
65
+ for (const key of ["status-format[3]", "@tfx_hud_3"])
66
+ await runTmux(null, ["set-option", "-u", "-t", target, key]);
67
+ }
68
+
69
+ export async function updateTmuxHud(session, pane, columns) {
70
+ // CLI 모듈은 triflux 설치본에 있다. core의 공유 HUD에서는 이 진입점을 실행하지 않는다.
71
+ const { resolveCodexTmuxThread } = await import(
72
+ new URL("../bin/tfx-live.mjs", import.meta.url)
73
+ );
74
+ const { threadId } = await resolveCodexTmuxThread(pane, {
75
+ requireRegistryAgreement: true,
76
+ runTmux: async (_remote, args) => {
77
+ // tmux 3.8은 format의 탭을 밑줄로 바꾸므로 출력 뒤에 탭으로 복원한다.
78
+ const safeArgs = args.map((arg) => arg.replace(/\t/g, "|"));
79
+ const result = await exec("tmux", safeArgs, { timeout: 2000 });
80
+ return { stdout: result.stdout.replace(/\|/g, "\t") };
81
+ },
82
+ });
83
+ const context = threadId ? await readCodexContext(null, threadId) : null;
84
+ const limit = context?.executionContextLimitTokens;
85
+ const used = context?.estimatedContextTokens;
86
+ const input = {
87
+ product: "codex",
88
+ terminal_width: Number(columns) || 120,
89
+ // 모델별 기본 분모는 쓰지 않고 현재 rollout의 실행 창만 쓴다.
90
+ ...(Number.isFinite(limit) &&
91
+ limit > 0 &&
92
+ Number.isFinite(used) &&
93
+ used >= 0
94
+ ? {
95
+ context_window: {
96
+ context_window_size: limit,
97
+ current_usage: { total_tokens: used },
98
+ },
99
+ }
100
+ : {}),
101
+ };
102
+ const rendering = exec(
103
+ process.execPath,
104
+ [
105
+ fileURLToPath(new URL("./hud-qos-status.mjs", import.meta.url)),
106
+ "--cache-only",
107
+ ],
108
+ {
109
+ timeout: 10000,
110
+ env: { ...process.env, COLUMNS: String(input.terminal_width) },
111
+ },
112
+ );
113
+ rendering.child.stdin.end(JSON.stringify(input));
114
+ const { stdout } = await rendering;
115
+ const lines = stripAnsi(stdout).trim().split("\n");
116
+ for (let n = 0; n < 3; n += 1) {
117
+ await exec(
118
+ "tmux",
119
+ [
120
+ "set-option",
121
+ "-t",
122
+ session,
123
+ `@tfx_hud_${n}`,
124
+ (lines[n] || "").replace(/#/g, "##"),
125
+ ],
126
+ { timeout: 2000 },
127
+ );
128
+ }
129
+ return lines[0] || "";
130
+ }
131
+
132
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
133
+ updateTmuxHud(...process.argv.slice(2))
134
+ .then((status) => {
135
+ process.stdout.write(`${status}\n`);
136
+ })
137
+ .catch(() => {
138
+ process.stdout.write("c: ctx:--\n");
139
+ });
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.87.0",
3
+ "version": "10.88.1",
4
4
  "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",