triflux 10.3.2 → 10.3.4
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/.claude-plugin/plugin.json +22 -22
- package/LICENSE +21 -21
- package/README.ko.md +16 -0
- package/README.md +8 -0
- package/hooks/hook-registry.json +256 -256
- package/hub/adaptive-inject.mjs +1 -1
- package/hub/assign-callbacks.mjs +120 -120
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/hitl.mjs +143 -143
- package/hub/lib/path-utils.mjs +167 -0
- package/hub/router.mjs +791 -791
- package/hub/session-fingerprint.mjs +1 -1
- package/hub/team/cli/commands/attach.mjs +37 -37
- package/hub/team/cli/commands/debug.mjs +74 -74
- package/hub/team/cli/commands/focus.mjs +53 -53
- package/hub/team/cli/commands/list.mjs +24 -24
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
- package/hub/team/cli/commands/start/start-mux.mjs +73 -73
- package/hub/team/cli/commands/start/start-wt.mjs +69 -69
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/render.mjs +30 -30
- package/hub/team/cli/services/attach-fallback.mjs +54 -54
- package/hub/team/cli/services/member-selector.mjs +30 -30
- package/hub/team/cli/services/native-control.mjs +116 -116
- package/hub/team/cli/services/task-model.mjs +30 -30
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +161 -161
- package/hub/team/runtime-strategy.mjs +74 -0
- package/hub/team/session.mjs +611 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/worktree-lifecycle.mjs +61 -2
- package/hub/tray.mjs +368 -368
- package/hub/workers/codex-mcp.mjs +507 -507
- package/hub/workers/factory.mjs +21 -21
- package/hud/hud-qos-status.mjs +17 -3
- package/hud/mission-board.mjs +53 -0
- package/hud/providers/claude.mjs +95 -22
- package/hud/renderers.mjs +39 -5
- package/mesh/index.mjs +63 -0
- package/mesh/mesh-budget.mjs +128 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +165 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +76 -0
- package/package.json +2 -1
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/demo.mjs +169 -0
- package/scripts/headless-guard.mjs +16 -4
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/notion-read.mjs +553 -553
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-batch-stats.mjs +96 -96
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
- package/skills/tfx-remote-spawn/references/hosts.json +0 -16
package/hub/workers/factory.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
-
|
|
3
|
-
import { GeminiWorker } from './gemini-worker.mjs';
|
|
4
|
-
import { ClaudeWorker } from './claude-worker.mjs';
|
|
5
|
-
import { CodexMcpWorker } from './codex-mcp.mjs';
|
|
6
|
-
import { DelegatorMcpWorker } from './delegator-mcp.mjs';
|
|
7
|
-
|
|
8
|
-
export function createWorker(type, opts = {}) {
|
|
9
|
-
switch (type) {
|
|
10
|
-
case 'gemini':
|
|
11
|
-
return new GeminiWorker(opts);
|
|
12
|
-
case 'claude':
|
|
13
|
-
return new ClaudeWorker(opts);
|
|
14
|
-
case 'codex':
|
|
15
|
-
return new CodexMcpWorker(opts);
|
|
16
|
-
case 'delegator':
|
|
17
|
-
return new DelegatorMcpWorker(opts);
|
|
18
|
-
default:
|
|
19
|
-
throw new Error(`Unknown worker type: ${type}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
+
|
|
3
|
+
import { GeminiWorker } from './gemini-worker.mjs';
|
|
4
|
+
import { ClaudeWorker } from './claude-worker.mjs';
|
|
5
|
+
import { CodexMcpWorker } from './codex-mcp.mjs';
|
|
6
|
+
import { DelegatorMcpWorker } from './delegator-mcp.mjs';
|
|
7
|
+
|
|
8
|
+
export function createWorker(type, opts = {}) {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'gemini':
|
|
11
|
+
return new GeminiWorker(opts);
|
|
12
|
+
case 'claude':
|
|
13
|
+
return new ClaudeWorker(opts);
|
|
14
|
+
case 'codex':
|
|
15
|
+
return new CodexMcpWorker(opts);
|
|
16
|
+
case 'delegator':
|
|
17
|
+
return new DelegatorMcpWorker(opts);
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unknown worker type: ${type}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readJson, readStdinJson, getProviderAccountId, getCliArgValue,
|
|
17
17
|
} from "./utils.mjs";
|
|
18
18
|
import { selectTier } from "./terminal.mjs";
|
|
19
|
+
import { getMissionBoardState } from "./mission-board.mjs";
|
|
19
20
|
|
|
20
21
|
// Claude provider
|
|
21
22
|
import {
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
// Renderers
|
|
42
43
|
import {
|
|
43
44
|
getClaudeRows, getProviderRow, getTeamRow,
|
|
44
|
-
renderAlignedRows, getMicroLine,
|
|
45
|
+
renderAlignedRows, getMicroLine, renderMissionBoard,
|
|
45
46
|
readLatestBenchmarkDiff, formatTokenSummary,
|
|
46
47
|
readTokenSavings, readSvAccumulator,
|
|
47
48
|
} from "./renderers.mjs";
|
|
@@ -103,11 +104,15 @@ async function main() {
|
|
|
103
104
|
// 실측 데이터 추출
|
|
104
105
|
const stdin = await stdinPromise;
|
|
105
106
|
const contextView = buildContextUsageView(stdin, contextSnapshot);
|
|
107
|
+
const claudeUsage = claudeUsageSnapshot.isStale
|
|
108
|
+
? { ...(claudeUsageSnapshot.data || {}), stale: true }
|
|
109
|
+
: claudeUsageSnapshot.data;
|
|
106
110
|
const codexEmail = getCodexEmail();
|
|
107
111
|
const geminiEmail = getGeminiEmail();
|
|
108
112
|
const codexBuckets = codexSnapshot.buckets;
|
|
109
113
|
const geminiSession = geminiSessionSnapshot.session;
|
|
110
114
|
const geminiQuota = geminiQuotaSnapshot.quota;
|
|
115
|
+
const missionBoardState = await getMissionBoardState();
|
|
111
116
|
|
|
112
117
|
// 누적 절약 데이터 읽기
|
|
113
118
|
const svSavings = readTokenSavings();
|
|
@@ -149,7 +154,7 @@ async function main() {
|
|
|
149
154
|
|
|
150
155
|
// nano tier: 1줄 모드 (극소 폭 또는 알림 배너 대응)
|
|
151
156
|
if (CURRENT_TIER === "nano") {
|
|
152
|
-
const microLine = getMicroLine(contextView,
|
|
157
|
+
const microLine = getMicroLine(contextView, claudeUsage, codexBuckets,
|
|
153
158
|
geminiSession, geminiBucket, combinedSvPct);
|
|
154
159
|
process.stdout.write(`\x1b[0m${microLine}\n`);
|
|
155
160
|
return;
|
|
@@ -164,7 +169,7 @@ async function main() {
|
|
|
164
169
|
};
|
|
165
170
|
|
|
166
171
|
const rows = [
|
|
167
|
-
...getClaudeRows(CURRENT_TIER, contextView,
|
|
172
|
+
...getClaudeRows(CURRENT_TIER, contextView, claudeUsage, combinedSvPct),
|
|
168
173
|
getProviderRow(CURRENT_TIER, "codex", "x", codexWhite, qosProfile, accountsConfig, accountsState,
|
|
169
174
|
codexQuotaData, codexEmail, codexSv, null),
|
|
170
175
|
getProviderRow(CURRENT_TIER, "gemini", "g", geminiBlue, qosProfile, accountsConfig, accountsState,
|
|
@@ -175,6 +180,15 @@ async function main() {
|
|
|
175
180
|
const teamRow = getTeamRow(CURRENT_TIER);
|
|
176
181
|
if (teamRow) rows.push(teamRow);
|
|
177
182
|
|
|
183
|
+
const missionBoard = renderMissionBoard(missionBoardState);
|
|
184
|
+
if (missionBoard) {
|
|
185
|
+
rows.push({
|
|
186
|
+
prefix: bold(claudeOrange("\u25B2")),
|
|
187
|
+
left: missionBoard,
|
|
188
|
+
right: "",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
// 최근 벤치마크 diff → 토큰 요약 행 추가
|
|
179
193
|
const latestDiff = readLatestBenchmarkDiff();
|
|
180
194
|
if (latestDiff) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// HUD Mission Board — .omc/state/sessions/ 기반 에이전트 진행률 집계
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const SESSIONS_DIR = join(homedir(), ".omc", "state", "sessions");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* .omc/state/sessions/ 디렉토리를 읽어 팀 상태를 반환한다.
|
|
12
|
+
* @returns {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null}
|
|
13
|
+
*/
|
|
14
|
+
export async function getMissionBoardState(sessionsDir = SESSIONS_DIR) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(sessionsDir);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const jsonFiles = entries.filter((f) => f.endsWith(".json"));
|
|
23
|
+
if (jsonFiles.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
const agents = [];
|
|
26
|
+
for (const file of jsonFiles) {
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(join(sessionsDir, file), "utf8");
|
|
30
|
+
data = JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const name = data.name ?? file.replace(/\.json$/, "");
|
|
36
|
+
const status = data.status ?? "idle";
|
|
37
|
+
const progress = typeof data.progress === "number" ? data.progress : 0;
|
|
38
|
+
agents.push({ name, status, progress });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (agents.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
const totalProgress = agents.length > 0
|
|
44
|
+
? Math.round(agents.reduce((sum, a) => sum + a.progress, 0) / agents.length)
|
|
45
|
+
: 0;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
agents,
|
|
49
|
+
// TODO: derive dagLevel from real mission dependency metadata instead of hardcoding 0.
|
|
50
|
+
dagLevel: 0,
|
|
51
|
+
totalProgress,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/hud/providers/claude.mjs
CHANGED
|
@@ -16,6 +16,16 @@ import {
|
|
|
16
16
|
import { readJson, writeJsonSafe, clampPercent, advanceToNextCycle } from "../utils.mjs";
|
|
17
17
|
import { readContextMonitorSnapshot } from "../context-monitor.mjs";
|
|
18
18
|
|
|
19
|
+
export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
|
|
20
|
+
export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
|
|
21
|
+
export const CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS = [
|
|
22
|
+
CLAUDE_USAGE_POLL_BASE_MS,
|
|
23
|
+
10_000,
|
|
24
|
+
30_000,
|
|
25
|
+
60_000,
|
|
26
|
+
120_000,
|
|
27
|
+
];
|
|
28
|
+
|
|
19
29
|
// OMC 활성 여부에 따라 캐시 TTL 동적 결정
|
|
20
30
|
function getClaudeUsageStaleMs() {
|
|
21
31
|
return existsSync(OMC_PLUGIN_USAGE_CACHE_PATH)
|
|
@@ -23,6 +33,49 @@ function getClaudeUsageStaleMs() {
|
|
|
23
33
|
: CLAUDE_USAGE_STALE_MS_SOLO;
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
export function computeClaudeUsagePollState({
|
|
37
|
+
consecutive429s = 0,
|
|
38
|
+
outcome = "success",
|
|
39
|
+
random = Math.random,
|
|
40
|
+
jitterRatio = CLAUDE_USAGE_POLL_JITTER_RATIO,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const current429s = Number.isFinite(consecutive429s) ? Math.max(0, consecutive429s) : 0;
|
|
43
|
+
const next429s = outcome === "rate_limit" ? current429s + 1 : 0;
|
|
44
|
+
const stepIndex = outcome === "rate_limit"
|
|
45
|
+
? Math.min(next429s, CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS.length - 1)
|
|
46
|
+
: 0;
|
|
47
|
+
const baseDelayMs = CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS[stepIndex];
|
|
48
|
+
const sample = Number(random?.());
|
|
49
|
+
const normalized = Number.isFinite(sample) ? Math.min(1, Math.max(0, sample)) : 0.5;
|
|
50
|
+
const jitterFactor = 1 + ((normalized * 2) - 1) * jitterRatio;
|
|
51
|
+
return {
|
|
52
|
+
consecutive429s: next429s,
|
|
53
|
+
baseDelayMs,
|
|
54
|
+
delayMs: Math.max(1, Math.round(baseDelayMs * jitterFactor)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSnapshotSchedule(cache) {
|
|
59
|
+
const timestamp = Number(cache?.timestamp);
|
|
60
|
+
const nextRefreshAt = Number(cache?.nextRefreshAt);
|
|
61
|
+
if (Number.isFinite(nextRefreshAt)) {
|
|
62
|
+
return {
|
|
63
|
+
nextRefreshAt,
|
|
64
|
+
shouldRefresh: Date.now() >= nextRefreshAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ageMs = Number.isFinite(timestamp) ? Date.now() - timestamp : Number.MAX_SAFE_INTEGER;
|
|
69
|
+
const fallbackMs = cache?.error
|
|
70
|
+
? (cache.errorType === "rate_limit" ? CLAUDE_USAGE_429_BACKOFF_MS : CLAUDE_USAGE_ERROR_BACKOFF_MS)
|
|
71
|
+
: getClaudeUsageStaleMs();
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
nextRefreshAt: Number.isFinite(timestamp) ? timestamp + fallbackMs : null,
|
|
75
|
+
shouldRefresh: ageMs >= fallbackMs,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
26
79
|
export function readClaudeCredentials() {
|
|
27
80
|
const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
|
|
28
81
|
if (!data) return null;
|
|
@@ -154,18 +207,21 @@ export function stripStaleResets(data) {
|
|
|
154
207
|
export function readClaudeUsageSnapshot() {
|
|
155
208
|
const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
|
|
156
209
|
const ts = Number(cache?.timestamp);
|
|
157
|
-
const
|
|
210
|
+
const schedule = getSnapshotSchedule(cache);
|
|
211
|
+
const staleBackoffActive = cache?.errorType === "rate_limit"
|
|
212
|
+
&& Number.isFinite(schedule.nextRefreshAt)
|
|
213
|
+
&& Date.now() < schedule.nextRefreshAt;
|
|
158
214
|
|
|
159
215
|
// 1차: 자체 캐시에 유효 데이터가 있는 경우
|
|
160
216
|
if (cache?.data) {
|
|
161
217
|
// 에러 상태에서 보존된 stale 데이터 → backoff 존중하되 표시용 데이터 반환
|
|
162
218
|
if (cache.error) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
:
|
|
166
|
-
|
|
219
|
+
return {
|
|
220
|
+
data: stripStaleResets(cache.data),
|
|
221
|
+
shouldRefresh: schedule.shouldRefresh,
|
|
222
|
+
isStale: staleBackoffActive,
|
|
223
|
+
};
|
|
167
224
|
}
|
|
168
|
-
const isFresh = ageMs < getClaudeUsageStaleMs();
|
|
169
225
|
// resets_at이 지난 윈도우의 percent를 0으로 보정 (stale 캐시 방지)
|
|
170
226
|
const data = { ...cache.data };
|
|
171
227
|
const now = Date.now();
|
|
@@ -175,24 +231,21 @@ export function readClaudeUsageSnapshot() {
|
|
|
175
231
|
if (data.weeklyResetsAt && new Date(data.weeklyResetsAt).getTime() <= now) {
|
|
176
232
|
data.weeklyPercent = 0;
|
|
177
233
|
}
|
|
178
|
-
return { data, shouldRefresh:
|
|
234
|
+
return { data, shouldRefresh: schedule.shouldRefresh, isStale: false };
|
|
179
235
|
}
|
|
180
236
|
|
|
181
237
|
// 2차: 에러 backoff — 최근 에러 시 재시도 억제 (무한 spawn 방지)
|
|
182
238
|
if (cache?.error && Number.isFinite(ts)) {
|
|
183
|
-
|
|
184
|
-
? CLAUDE_USAGE_429_BACKOFF_MS
|
|
185
|
-
: CLAUDE_USAGE_ERROR_BACKOFF_MS;
|
|
186
|
-
if (ageMs < backoffMs) {
|
|
239
|
+
if (!schedule.shouldRefresh) {
|
|
187
240
|
const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
|
|
188
241
|
// OMC 캐시가 에러 이후 갱신되었으면 → 에러 캐시 덮어쓰고 그 데이터 사용
|
|
189
242
|
if (omcCache?.data?.fiveHourPercent != null && omcCache.timestamp > ts) {
|
|
190
243
|
writeClaudeUsageCache(omcCache.data);
|
|
191
|
-
return { data: omcCache.data, shouldRefresh: false };
|
|
244
|
+
return { data: omcCache.data, shouldRefresh: false, isStale: false };
|
|
192
245
|
}
|
|
193
246
|
// stale OMC fallback 또는 null (--% 플레이스홀더 표시, 가짜 0% 방지)
|
|
194
247
|
const staleData = omcCache?.data?.fiveHourPercent != null ? stripStaleResets(omcCache.data) : null;
|
|
195
|
-
return { data: staleData, shouldRefresh: false };
|
|
248
|
+
return { data: staleData, shouldRefresh: false, isStale: staleBackoffActive };
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
|
|
@@ -203,23 +256,37 @@ export function readClaudeUsageSnapshot() {
|
|
|
203
256
|
const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Number.MAX_SAFE_INTEGER;
|
|
204
257
|
if (omcAge < OMC_CACHE_MAX_AGE_MS) {
|
|
205
258
|
writeClaudeUsageCache(omcCache.data);
|
|
206
|
-
return { data: omcCache.data, shouldRefresh: omcAge > getClaudeUsageStaleMs() };
|
|
259
|
+
return { data: omcCache.data, shouldRefresh: omcAge > getClaudeUsageStaleMs(), isStale: false };
|
|
207
260
|
}
|
|
208
261
|
// stale이어도 data: null보다는 오래된 데이터를 fallback으로 표시
|
|
209
|
-
return { data: stripStaleResets(omcCache.data), shouldRefresh: true };
|
|
262
|
+
return { data: stripStaleResets(omcCache.data), shouldRefresh: true, isStale: false };
|
|
210
263
|
}
|
|
211
264
|
|
|
212
265
|
// 캐시/fallback 모두 없음: null 반환 → --% 플레이스홀더 + 리프레시 시도
|
|
213
|
-
return { data: null, shouldRefresh: true };
|
|
266
|
+
return { data: null, shouldRefresh: true, isStale: false };
|
|
214
267
|
}
|
|
215
268
|
|
|
216
|
-
export function writeClaudeUsageCache(data, errorInfo = null) {
|
|
269
|
+
export function writeClaudeUsageCache(data, errorInfo = null, pollState = null) {
|
|
270
|
+
const state = pollState || (errorInfo
|
|
271
|
+
? {
|
|
272
|
+
consecutive429s: errorInfo.type === "rate_limit" ? 1 : 0,
|
|
273
|
+
baseDelayMs: errorInfo.type === "rate_limit"
|
|
274
|
+
? CLAUDE_USAGE_429_BACKOFF_MS
|
|
275
|
+
: CLAUDE_USAGE_ERROR_BACKOFF_MS,
|
|
276
|
+
delayMs: errorInfo.type === "rate_limit"
|
|
277
|
+
? CLAUDE_USAGE_429_BACKOFF_MS
|
|
278
|
+
: CLAUDE_USAGE_ERROR_BACKOFF_MS,
|
|
279
|
+
}
|
|
280
|
+
: computeClaudeUsagePollState({ outcome: "success" }));
|
|
217
281
|
const entry = {
|
|
218
282
|
timestamp: Date.now(),
|
|
219
283
|
data,
|
|
220
284
|
error: !!errorInfo,
|
|
221
285
|
errorType: errorInfo?.type || null, // "rate_limit" | "auth" | "network" | "unknown"
|
|
222
286
|
errorStatus: errorInfo?.status || null, // HTTP 상태 코드
|
|
287
|
+
consecutive429s: state.consecutive429s,
|
|
288
|
+
nextRefreshBaseMs: state.baseDelayMs,
|
|
289
|
+
nextRefreshAt: Date.now() + state.delayMs,
|
|
223
290
|
};
|
|
224
291
|
// 에러 시 기존 유효 데이터 보존 (--% n/a 방지)
|
|
225
292
|
if (errorInfo && data == null) {
|
|
@@ -234,9 +301,11 @@ export function writeClaudeUsageCache(data, errorInfo = null) {
|
|
|
234
301
|
|
|
235
302
|
export async function fetchClaudeUsage(forceRefresh = false) {
|
|
236
303
|
const existingSnapshot = readClaudeUsageSnapshot();
|
|
237
|
-
if (!forceRefresh && !existingSnapshot.shouldRefresh
|
|
238
|
-
return existingSnapshot.data;
|
|
304
|
+
if (!forceRefresh && !existingSnapshot.shouldRefresh) {
|
|
305
|
+
return existingSnapshot.data || null;
|
|
239
306
|
}
|
|
307
|
+
const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
|
|
308
|
+
const consecutive429s = Number.isFinite(cache?.consecutive429s) ? cache.consecutive429s : 0;
|
|
240
309
|
let creds = readClaudeCredentials();
|
|
241
310
|
if (!creds) {
|
|
242
311
|
writeClaudeUsageCache(null, { type: "auth", status: 0 });
|
|
@@ -262,11 +331,15 @@ export async function fetchClaudeUsage(forceRefresh = false) {
|
|
|
262
331
|
: result.status === 401 || result.status === 403 ? "auth"
|
|
263
332
|
: result.error === "timeout" || result.error === "network" ? "network"
|
|
264
333
|
: "unknown";
|
|
265
|
-
|
|
334
|
+
const pollState = errorType === "rate_limit"
|
|
335
|
+
? computeClaudeUsagePollState({ consecutive429s, outcome: "rate_limit" })
|
|
336
|
+
: null;
|
|
337
|
+
writeClaudeUsageCache(existingSnapshot.data, { type: errorType, status: result.status }, pollState);
|
|
266
338
|
return existingSnapshot.data || null;
|
|
267
339
|
}
|
|
268
340
|
const usage = parseClaudeUsageResponse(result.data);
|
|
269
|
-
|
|
341
|
+
const pollState = usage ? computeClaudeUsagePollState({ outcome: "success" }) : null;
|
|
342
|
+
writeClaudeUsageCache(usage, usage ? null : { type: "unknown", status: 0 }, pollState);
|
|
270
343
|
return usage;
|
|
271
344
|
}
|
|
272
345
|
|
|
@@ -291,7 +364,7 @@ export function scheduleClaudeUsageRefresh() {
|
|
|
291
364
|
try {
|
|
292
365
|
if (existsSync(lockPath)) {
|
|
293
366
|
const lockAge = Date.now() - readJson(lockPath, {}).t;
|
|
294
|
-
if (lockAge <
|
|
367
|
+
if (lockAge < 1000) return; // 짧은 중복 스폰만 억제, 실제 폴링 주기는 nextRefreshAt가 제어
|
|
295
368
|
}
|
|
296
369
|
writeJsonSafe(lockPath, { t: Date.now() });
|
|
297
370
|
} catch { /* 락 실패 무시 — 스폰 진행 */ }
|
package/hud/renderers.mjs
CHANGED
|
@@ -122,6 +122,38 @@ export function getTeamRow(currentTier) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Mission Board 렌더러
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
const STATUS_ICON = {
|
|
130
|
+
active: "*",
|
|
131
|
+
idle: ".",
|
|
132
|
+
done: "+",
|
|
133
|
+
failed: "!",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Mission Board 상태를 1줄 compact 포맷으로 렌더링한다.
|
|
138
|
+
* 예: "MB: exec:+ ui:* perf:. [2/3 67%]"
|
|
139
|
+
* @param {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null} state
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function renderMissionBoard(state) {
|
|
143
|
+
if (!state) return "";
|
|
144
|
+
|
|
145
|
+
const { agents, totalProgress } = state;
|
|
146
|
+
const done = agents.filter((a) => a.status === "done").length;
|
|
147
|
+
const total = agents.length;
|
|
148
|
+
|
|
149
|
+
const parts = agents.map((a) => {
|
|
150
|
+
const icon = STATUS_ICON[a.status] ?? "?";
|
|
151
|
+
return `${a.name}:${icon}`;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return `MB: ${parts.join(" ")} [${done}/${total} ${totalProgress}%]`;
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
// ============================================================================
|
|
126
158
|
// 행 정렬 렌더링
|
|
127
159
|
// ============================================================================
|
|
@@ -159,6 +191,7 @@ export function renderAlignedRows(rows) {
|
|
|
159
191
|
// ============================================================================
|
|
160
192
|
export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSession, geminiBucket, combinedSvPct) {
|
|
161
193
|
const ctxView = contextView || buildContextUsageView({}, null);
|
|
194
|
+
const staleMarker = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
|
|
162
195
|
|
|
163
196
|
// Claude 5h/1w
|
|
164
197
|
const cF = claudeUsage?.fiveHourPercent != null ? clampPercent(claudeUsage.fiveHourPercent) : null;
|
|
@@ -198,7 +231,7 @@ export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSessi
|
|
|
198
231
|
`${bold(codexWhite("x"))}${dim(":")}${xVal} ` +
|
|
199
232
|
`${bold(geminiBlue("g"))}${dim(":")}${gVal} ` +
|
|
200
233
|
`${dim("sv:")}${sv} ` +
|
|
201
|
-
`${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
|
|
234
|
+
`${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${staleMarker}`;
|
|
202
235
|
return truncateAnsi(line, cols);
|
|
203
236
|
}
|
|
204
237
|
|
|
@@ -208,6 +241,7 @@ export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSessi
|
|
|
208
241
|
export function getClaudeRows(currentTier, contextView, claudeUsage, combinedSvPct) {
|
|
209
242
|
const ctxView = contextView || buildContextUsageView({}, null);
|
|
210
243
|
const prefix = `${bold(claudeOrange("c"))}:`;
|
|
244
|
+
const staleMarker = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
|
|
211
245
|
|
|
212
246
|
// 절약 퍼센트
|
|
213
247
|
const svStr = formatSvPct(combinedSvPct || 0);
|
|
@@ -235,25 +269,25 @@ export function getClaudeRows(currentTier, contextView, claudeUsage, combinedSvP
|
|
|
235
269
|
if (currentTier === "nano" || currentTier === "micro") {
|
|
236
270
|
const fShort = hasData && fiveHourPercent != null ? colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange) : dim("--");
|
|
237
271
|
const wShort = hasData && weeklyPercent != null ? colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange) : dim("--");
|
|
238
|
-
const quotaSection = `${fShort}${dim("/")}${wShort}`;
|
|
272
|
+
const quotaSection = `${fShort}${dim("/")}${wShort}${staleMarker}`;
|
|
239
273
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
240
274
|
}
|
|
241
275
|
|
|
242
276
|
if (currentTier === "minimal") {
|
|
243
|
-
const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}`;
|
|
277
|
+
const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleMarker}`;
|
|
244
278
|
const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
|
|
245
279
|
return [{ prefix, left: quotaSection, right }];
|
|
246
280
|
}
|
|
247
281
|
|
|
248
282
|
if (currentTier === "compact") {
|
|
249
|
-
const quotaSection = `${dim("5h:")}${fStr} ${dim(fTime)} ${dim("1w:")}${wStr} ${dim(wTime)}`;
|
|
283
|
+
const quotaSection = `${dim("5h:")}${fStr} ${dim(fTime)} ${dim("1w:")}${wStr} ${dim(wTime)}${staleMarker}`;
|
|
250
284
|
const warning = ctxView.warningTag ? ` ${dim("|")} ${yellow(ctxView.warningTag)}` : "";
|
|
251
285
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
|
|
252
286
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
253
287
|
}
|
|
254
288
|
|
|
255
289
|
// full tier (>= 120 cols)
|
|
256
|
-
const quotaSection = `${dim("5h:")}${fBar}${fStr} ${dim(fTime)} ${dim("1w:")}${wBar}${wStr} ${dim(wTime)}`;
|
|
290
|
+
const quotaSection = `${dim("5h:")}${fBar}${fStr} ${dim(fTime)} ${dim("1w:")}${wBar}${wStr} ${dim(wTime)}${staleMarker}`;
|
|
257
291
|
const warning = ctxView.warningTag ? ` ${dim("|")} ${yellow(ctxView.warningTag)}` : "";
|
|
258
292
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
|
|
259
293
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
package/mesh/index.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export { MSG_TYPES, createMessage, serialize, deserialize, validate } from "./mesh-protocol.mjs";
|
|
5
|
+
export { createRegistry } from "./mesh-registry.mjs";
|
|
6
|
+
export { createMeshBudget } from "./mesh-budget.mjs";
|
|
7
|
+
export { routeMessage, routeOrDeadLetter } from "./mesh-router.mjs";
|
|
8
|
+
export { createMessageQueue } from "./mesh-queue.mjs";
|
|
9
|
+
export { createHeartbeatMonitor } from "./mesh-heartbeat.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads skills assigned to a specific agent from a skills directory.
|
|
13
|
+
* Reuses the same directory-scan approach as generateSkillDocs().
|
|
14
|
+
*
|
|
15
|
+
* @param {string} agentId - The agent identifier
|
|
16
|
+
* @param {string} skillsDir - Path to the skills directory
|
|
17
|
+
* @returns {Promise<string[]>} Array of skill names available to this agent
|
|
18
|
+
*/
|
|
19
|
+
export async function loadSkillsForAgent(agentId, skillsDir) {
|
|
20
|
+
if (!agentId || typeof agentId !== "string") {
|
|
21
|
+
throw new TypeError("agentId must be a non-empty string");
|
|
22
|
+
}
|
|
23
|
+
if (!skillsDir || typeof skillsDir !== "string") {
|
|
24
|
+
throw new TypeError("skillsDir must be a non-empty string");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const skills = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
const skillName = entry.name;
|
|
38
|
+
const skillPath = join(skillsDir, skillName, "SKILL.md");
|
|
39
|
+
let skillContent = null;
|
|
40
|
+
try {
|
|
41
|
+
const { readFileSync } = await import("node:fs");
|
|
42
|
+
skillContent = readFileSync(skillPath, "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
// Skill has no SKILL.md — include it anyway
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If SKILL.md mentions the agentId or no agent restriction, include it
|
|
48
|
+
const isRestricted = skillContent
|
|
49
|
+
? /^agents?\s*:/im.test(skillContent)
|
|
50
|
+
: false;
|
|
51
|
+
|
|
52
|
+
if (!isRestricted) {
|
|
53
|
+
skills.push(skillName);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (skillContent && skillContent.includes(agentId)) {
|
|
58
|
+
skills.push(skillName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return skills;
|
|
63
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warning level thresholds mirroring context-monitor.mjs classifyContextThreshold().
|
|
3
|
+
* Reproduced here to avoid cross-module dependency.
|
|
4
|
+
*/
|
|
5
|
+
const WARNING_LEVELS = Object.freeze({
|
|
6
|
+
critical: 90,
|
|
7
|
+
warn: 80,
|
|
8
|
+
info: 60,
|
|
9
|
+
ok: 0,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clamps a percentage value to [0, 100].
|
|
14
|
+
* @param {number} value
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
function clampPercent(value) {
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
if (!Number.isFinite(n)) return 0;
|
|
20
|
+
return Math.min(100, Math.max(0, n));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Classifies a usage percentage into a warning level.
|
|
25
|
+
* Mirrors context-monitor.mjs classifyContextThreshold().
|
|
26
|
+
* @param {number} percent
|
|
27
|
+
* @returns {{ level: string, message: string }}
|
|
28
|
+
*/
|
|
29
|
+
function classifyLevel(percent) {
|
|
30
|
+
const p = clampPercent(percent);
|
|
31
|
+
if (p >= WARNING_LEVELS.critical) return { level: "critical", message: "에이전트 분할 또는 세션 교체 권장" };
|
|
32
|
+
if (p >= WARNING_LEVELS.warn) return { level: "warn", message: "압축 권장" };
|
|
33
|
+
if (p >= WARNING_LEVELS.info) return { level: "info", message: "컨텍스트 절반 이상 사용" };
|
|
34
|
+
return { level: "ok", message: "" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a per-agent token budget manager.
|
|
39
|
+
* @returns {object} Budget API
|
|
40
|
+
*/
|
|
41
|
+
export function createMeshBudget() {
|
|
42
|
+
// Map<agentId, { allocated: number, consumed: number }>
|
|
43
|
+
const budgets = new Map();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Allocates a token budget to an agent.
|
|
47
|
+
* @param {string} agentId
|
|
48
|
+
* @param {number} tokenLimit
|
|
49
|
+
*/
|
|
50
|
+
function allocate(agentId, tokenLimit) {
|
|
51
|
+
if (!agentId || typeof agentId !== "string") {
|
|
52
|
+
throw new TypeError("agentId must be a non-empty string");
|
|
53
|
+
}
|
|
54
|
+
const limit = Math.max(0, Math.round(Number(tokenLimit) || 0));
|
|
55
|
+
const existing = budgets.get(agentId);
|
|
56
|
+
budgets.set(agentId, {
|
|
57
|
+
allocated: limit,
|
|
58
|
+
consumed: existing?.consumed ?? 0,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Records token consumption for an agent.
|
|
64
|
+
* @param {string} agentId
|
|
65
|
+
* @param {number} tokens
|
|
66
|
+
* @returns {{ remaining: number, percent: number, level: string }}
|
|
67
|
+
*/
|
|
68
|
+
function consume(agentId, tokens) {
|
|
69
|
+
const budget = budgets.get(agentId);
|
|
70
|
+
if (!budget) {
|
|
71
|
+
throw new Error(`No budget allocated for agent: ${agentId}`);
|
|
72
|
+
}
|
|
73
|
+
const amount = Math.max(0, Math.round(Number(tokens) || 0));
|
|
74
|
+
const updated = {
|
|
75
|
+
allocated: budget.allocated,
|
|
76
|
+
consumed: budget.consumed + amount,
|
|
77
|
+
};
|
|
78
|
+
budgets.set(agentId, updated);
|
|
79
|
+
|
|
80
|
+
const remaining = Math.max(0, updated.allocated - updated.consumed);
|
|
81
|
+
const percent = updated.allocated > 0
|
|
82
|
+
? clampPercent((updated.consumed / updated.allocated) * 100)
|
|
83
|
+
: 100;
|
|
84
|
+
const { level } = classifyLevel(percent);
|
|
85
|
+
return { remaining, percent, level };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns the budget status for an agent.
|
|
90
|
+
* @param {string} agentId
|
|
91
|
+
* @returns {{ allocated: number, consumed: number, remaining: number, level: string }}
|
|
92
|
+
*/
|
|
93
|
+
function getStatus(agentId) {
|
|
94
|
+
const budget = budgets.get(agentId);
|
|
95
|
+
if (!budget) {
|
|
96
|
+
return { allocated: 0, consumed: 0, remaining: 0, level: "ok" };
|
|
97
|
+
}
|
|
98
|
+
const remaining = Math.max(0, budget.allocated - budget.consumed);
|
|
99
|
+
const percent = budget.allocated > 0
|
|
100
|
+
? clampPercent((budget.consumed / budget.allocated) * 100)
|
|
101
|
+
: 0;
|
|
102
|
+
const { level } = classifyLevel(percent);
|
|
103
|
+
return { allocated: budget.allocated, consumed: budget.consumed, remaining, level };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resets consumed tokens for all agents (keeps allocations).
|
|
108
|
+
*/
|
|
109
|
+
function resetAll() {
|
|
110
|
+
for (const [agentId, budget] of budgets) {
|
|
111
|
+
budgets.set(agentId, { allocated: budget.allocated, consumed: 0 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns a snapshot of all current allocations.
|
|
117
|
+
* @returns {Map<string, { allocated: number, consumed: number }>}
|
|
118
|
+
*/
|
|
119
|
+
function listAllocations() {
|
|
120
|
+
const snap = new Map();
|
|
121
|
+
for (const [id, b] of budgets) {
|
|
122
|
+
snap.set(id, Object.freeze({ ...b }));
|
|
123
|
+
}
|
|
124
|
+
return snap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { allocate, consume, getStatus, resetAll, listAllocations };
|
|
128
|
+
}
|