@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/hooks/codex-session-hook.mjs +67 -1
- package/hooks/cto-north-star-brief.mjs +3 -16
- package/hooks/error-context.mjs +3 -6
- package/hooks/hook-manager.mjs +1 -1
- package/hooks/hook-registry.json +0 -11
- package/hooks/keyword-rules.json +8 -4
- package/hooks/permission-safe-allow.mjs +36 -2
- package/hooks/session-start-fast.mjs +50 -1
- package/hooks/session-start-lake.mjs +3 -16
- package/hub/bridge.mjs +111 -5
- package/hub/gemini-adapter.mjs +29 -11
- package/hub/lib/memory-store.mjs +66 -30
- package/hub/lib/tfx-route-args.mjs +41 -4
- package/hub/reflexion.mjs +0 -57
- package/hub/router.mjs +202 -24
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/retry-state-machine.mjs +39 -16
- package/hud/cli-policy.mjs +69 -0
- package/hud/constants.mjs +20 -17
- package/hud/context-monitor.mjs +31 -21
- package/hud/hud-qos-status.mjs +81 -91
- package/hud/providers/claude.mjs +150 -84
- package/hud/providers/codex.mjs +10 -1
- package/hud/renderers.mjs +21 -59
- package/hud/utils.mjs +1 -15
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +29 -22
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/codex-profile-config.mjs +26 -8
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
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 {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: true,
|
|
1223
|
+
data: { ...result, hub_pid: process.pid, ...hubIdentity() },
|
|
1224
|
+
};
|
|
1143
1225
|
},
|
|
1144
1226
|
|
|
1145
|
-
refreshAgentLease(
|
|
1146
|
-
|
|
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(
|
|
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
|
|
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 >
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
44
|
-
// 1. codex gpt-
|
|
45
|
-
// 2. claude
|
|
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-
|
|
50
|
-
profile: "
|
|
52
|
+
model: "gpt-6-astra",
|
|
53
|
+
profile: "gpt6_astra_max",
|
|
51
54
|
}),
|
|
52
|
-
Object.freeze({ cli: "claude", model:
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
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";
|