@triflux/core 10.42.0 → 10.44.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/hooks/keyword-rules.json +139 -19
- package/hooks/pipeline-stop.mjs +1 -1
- package/hub/bridge.mjs +80 -1
- package/hub/cli-adapter-base.mjs +111 -21
- package/hub/codex-adapter.mjs +8 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +3 -1
- package/hub/intent.mjs +24 -28
- package/hub/lib/cto-env.mjs +46 -0
- package/hub/lib/memory-store.mjs +25 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/middleware/request-logger.mjs +4 -1
- package/hub/role-contract.mjs +314 -0
- package/hub/role-control-events.mjs +113 -0
- package/hub/router.mjs +383 -29
- package/hub/team/claude-daemon-control.mjs +28 -15
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/retry-state-machine.mjs +12 -3
- package/hub/team/synapse-http.mjs +149 -4
- package/hud/constants.mjs +21 -0
- package/hud/context-monitor.mjs +18 -21
- package/hud/hud-qos-status.mjs +1 -1
- package/hud/providers/codex-probe.mjs +216 -0
- package/hud/providers/codex.mjs +206 -29
- package/hud/renderers.mjs +6 -9
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +23 -160
- package/scripts/lib/codex-profile-config.mjs +31 -5
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/hub/log-retention.mjs +0 -726
package/hud/providers/codex.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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 && !
|
|
259
|
+
if (rl?.limit_id && !found[rl.limit_id]) {
|
|
122
260
|
// window_minutes 기준으로 5h/1w 슬롯 정규화
|
|
123
261
|
const { primary, secondary } = normalizeBuckets(rl);
|
|
124
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
330
|
+
return { buckets, shouldRefresh: !isFresh };
|
|
186
331
|
}
|
|
187
332
|
|
|
188
|
-
export function
|
|
189
|
-
|
|
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
|
|
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
|
|
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
|
|