@triflux/core 10.50.1 → 10.51.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.
Files changed (90) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +1 -79
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -9
  8. package/hub/lib/cto-env.mjs +0 -23
  9. package/hub/lib/known-errors.json +2 -2
  10. package/hub/lib/process-utils.mjs +0 -123
  11. package/hub/lib/spawn-trace.mjs +3 -2
  12. package/hub/lib/timeout-defaults.mjs +0 -1
  13. package/hub/paths.mjs +0 -6
  14. package/hub/pipeline/index.mjs +0 -190
  15. package/hub/pipeline/transitions.mjs +0 -10
  16. package/hub/reflexion.mjs +1 -84
  17. package/hub/role-contract.mjs +0 -2
  18. package/hud/cli-policy.mjs +0 -26
  19. package/hud/colors.mjs +0 -13
  20. package/hud/constants.mjs +7 -153
  21. package/hud/context-monitor.mjs +3 -64
  22. package/hud/hud-qos-status.mjs +54 -264
  23. package/hud/providers/claude.mjs +4 -16
  24. package/hud/providers/gemini.mjs +83 -689
  25. package/hud/renderers.mjs +59 -367
  26. package/hud/terminal.mjs +1 -63
  27. package/hud/utils.mjs +2 -26
  28. package/package.json +1 -4
  29. package/scripts/lib/agent-route-policy.mjs +162 -14
  30. package/scripts/lib/claudemd-scanner.mjs +2 -199
  31. package/scripts/lib/cli-claude.mjs +0 -2
  32. package/scripts/lib/context.mjs +3 -19
  33. package/scripts/lib/env-probe.mjs +5 -64
  34. package/scripts/lib/gemini-profiles.mjs +2 -69
  35. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  36. package/scripts/lib/machine-profile.mjs +3 -17
  37. package/scripts/lib/mcp-filter.mjs +109 -53
  38. package/scripts/lib/mcp-health.mjs +2 -2
  39. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  40. package/scripts/lib/psmux-info.mjs +25 -104
  41. package/scripts/lib/session-presence.mjs +1 -1
  42. package/scripts/lib/skill-template.mjs +0 -271
  43. package/hub/codex-compat.mjs +0 -12
  44. package/hub/dynamic-routing-engine.mjs +0 -511
  45. package/hub/fullcycle.mjs +0 -113
  46. package/hub/gemini-adapter.mjs +0 -174
  47. package/hub/intent.mjs +0 -321
  48. package/hub/lib/mcp-response-cache.mjs +0 -239
  49. package/hub/lib/path-utils.mjs +0 -165
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/tfx-route-args.mjs +0 -353
  54. package/hub/lib/timeline-adapter.mjs +0 -190
  55. package/hub/pipeline/gates/confidence.mjs +0 -75
  56. package/hub/pipeline/gates/consensus.mjs +0 -106
  57. package/hub/pipeline/gates/index.mjs +0 -9
  58. package/hub/pipeline/gates/selfcheck.mjs +0 -120
  59. package/hub/quality/deslop.mjs +0 -274
  60. package/hub/research.mjs +0 -155
  61. package/hub/role-control-events.mjs +0 -113
  62. package/hub/routing/complexity.mjs +0 -269
  63. package/hub/routing/index.mjs +0 -122
  64. package/hub/routing/q-learning.mjs +0 -387
  65. package/hub/routing-snapshot.mjs +0 -261
  66. package/hub/token-mode.mjs +0 -228
  67. package/hud/mission-board.mjs +0 -56
  68. package/hud/providers/cto.mjs +0 -76
  69. package/mesh/index.mjs +0 -69
  70. package/mesh/mesh-budget.mjs +0 -137
  71. package/mesh/mesh-heartbeat.mjs +0 -100
  72. package/mesh/mesh-protocol.mjs +0 -96
  73. package/mesh/mesh-queue.mjs +0 -163
  74. package/mesh/mesh-registry.mjs +0 -78
  75. package/mesh/mesh-router.mjs +0 -77
  76. package/scripts/lib/agent-json.mjs +0 -27
  77. package/scripts/lib/async.mjs +0 -174
  78. package/scripts/lib/cli-agy.mjs +0 -85
  79. package/scripts/lib/cli-codex.mjs +0 -62
  80. package/scripts/lib/cli-gemini.mjs +0 -37
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/env.mjs +0 -14
  83. package/scripts/lib/hub.mjs +0 -48
  84. package/scripts/lib/pid.mjs +0 -63
  85. package/scripts/lib/quota.mjs +0 -47
  86. package/scripts/lib/skill-state.mjs +0 -220
  87. package/scripts/lib/team.mjs +0 -75
  88. package/scripts/lib/timeout.mjs +0 -66
  89. package/scripts/lib/tmp.mjs +0 -26
  90. package/scripts/lib/toml.mjs +0 -132
@@ -1,228 +0,0 @@
1
- // hub/token-mode.mjs — Token Efficiency Mode
2
- // GAP 분석 P2 #7: 심볼 통신 + 약어로 30-50% 토큰 절감
3
-
4
- /**
5
- * @experimental 런타임 미연결 — 향후 통합 예정
6
- * @experimental
7
- * @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>}
8
- */
9
- export const COMPACT_RULES = [
10
- // ── 심볼 치환 (긴 매칭 우선 정렬) ──
11
- { from: ["greater than or equal"], to: "≥", type: "symbol" },
12
- { from: ["less than or equal"], to: "≤", type: "symbol" },
13
- { from: ["results in", "결과적으로"], to: "→", type: "symbol" },
14
- { from: ["therefore", "따라서"], to: "∴", type: "symbol" },
15
- { from: ["because", "왜냐하면"], to: "∵", type: "symbol" },
16
- { from: ["approximately", "대략"], to: "≈", type: "symbol" },
17
- { from: ["not equal", "같지 않"], to: "≠", type: "symbol" },
18
- { from: ["in progress", "진행 중"], to: "⏳", type: "symbol" },
19
- { from: ["completed", "완료"], to: "✓", type: "symbol" },
20
- { from: ["success", "성공"], to: "✓", type: "symbol" },
21
- { from: ["failure", "실패"], to: "✗", type: "symbol" },
22
- { from: ["warning", "경고"], to: "⚠", type: "symbol" },
23
- { from: ["error", "에러"], to: "✗", type: "symbol" },
24
- { from: ["pending", "대기"], to: "⏸", type: "symbol" },
25
-
26
- // ── 약어 (긴 매칭 우선 정렬) ──
27
- { from: ["configuration", "설정"], to: "cfg", type: "abbrev" },
28
- { from: ["implementation", "구현"], to: "impl", type: "abbrev" },
29
- { from: ["architecture", "아키텍처"], to: "arch", type: "abbrev" },
30
- { from: ["dependency", "의존성"], to: "dep", type: "abbrev" },
31
- { from: ["function", "함수"], to: "fn", type: "abbrev" },
32
- { from: ["parameter", "파라미터"], to: "param", type: "abbrev" },
33
- { from: ["repository", "저장소"], to: "repo", type: "abbrev" },
34
- { from: ["environment", "환경"], to: "env", type: "abbrev" },
35
- { from: ["variable", "변수"], to: "var", type: "abbrev" },
36
- { from: ["directory", "디렉토리"], to: "dir", type: "abbrev" },
37
-
38
- // ── 한국어 동사/명령형 약어 ──
39
- { from: ["구현해"], to: "impl", type: "abbrev" },
40
- { from: ["확인해"], to: "check", type: "abbrev" },
41
- { from: ["수정해"], to: "fix", type: "abbrev" },
42
- { from: ["테스트"], to: "test", type: "abbrev" },
43
- { from: ["리뷰"], to: "review", type: "abbrev" },
44
- { from: ["분석"], to: "analyze", type: "abbrev" },
45
- { from: ["설계"], to: "design", type: "abbrev" },
46
- { from: ["문서화"], to: "docs", type: "abbrev" },
47
- ];
48
-
49
- /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
50
- export const REVIEW_RULES = [
51
- { from: ["looks good to me", "lgtm"], to: "✓lgtm", type: "abbrev" },
52
- { from: ["needs changes", "수정 필요"], to: "✗chg", type: "abbrev" },
53
- { from: ["nitpick", "사소한"], to: "nit", type: "abbrev" },
54
- { from: ["blocking", "블로킹"], to: "blk", type: "abbrev" },
55
- { from: ["suggestion", "제안"], to: "sug", type: "abbrev" },
56
- { from: ["question", "질문"], to: "q", type: "abbrev" },
57
- { from: ["approved", "승인"], to: "✓apv", type: "abbrev" },
58
- { from: ["request changes", "변경 요청"], to: "✗req", type: "abbrev" },
59
- ];
60
-
61
- /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
62
- export const DESIGN_RULES = [
63
- { from: ["component", "컴포넌트"], to: "cmp", type: "abbrev" },
64
- { from: ["interface", "인터페이스"], to: "iface", type: "abbrev" },
65
- { from: ["abstraction", "추상화"], to: "abs", type: "abbrev" },
66
- { from: ["pattern", "패턴"], to: "ptn", type: "abbrev" },
67
- { from: ["dependency injection", "의존성 주입"], to: "di", type: "abbrev" },
68
- { from: ["single responsibility", "단일 책임"], to: "srp", type: "abbrev" },
69
- { from: ["open closed", "개방 폐쇄"], to: "ocp", type: "abbrev" },
70
- { from: ["inheritance", "상속"], to: "inh", type: "abbrev" },
71
- ];
72
-
73
- /** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
74
- export const DOCS_RULES = [
75
- { from: ["description", "설명"], to: "desc", type: "abbrev" },
76
- { from: ["example", "예시"], to: "ex", type: "abbrev" },
77
- { from: ["reference", "참조"], to: "ref", type: "abbrev" },
78
- { from: ["introduction", "소개"], to: "intro", type: "abbrev" },
79
- { from: ["deprecated", "사용 중단"], to: "dep", type: "abbrev" },
80
- { from: ["optional", "선택적"], to: "opt", type: "abbrev" },
81
- { from: ["required", "필수"], to: "req", type: "abbrev" },
82
- { from: ["returns", "반환"], to: "ret", type: "abbrev" },
83
- ];
84
-
85
- // ── 프로필 맵 ──
86
-
87
- /** @type {Record<string, Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>>} */
88
- const PROFILE_MAP = {
89
- default: COMPACT_RULES,
90
- review: [...COMPACT_RULES, ...REVIEW_RULES],
91
- design: [...COMPACT_RULES, ...DESIGN_RULES],
92
- docs: [...COMPACT_RULES, ...DOCS_RULES],
93
- };
94
-
95
- // ── 내부: 정렬된 치환 쌍 빌드 헬퍼 ──
96
-
97
- /**
98
- * 규칙 배열로부터 패턴 쌍을 빌드하고 긴 패턴 우선 정렬
99
- * @param {Array<{ from: string[], to: string }>} rules
100
- * @returns {Array<{ pattern: string, to: string, len: number }>}
101
- */
102
- function buildCompactPairs(rules) {
103
- const pairs = [];
104
- for (const rule of rules) {
105
- for (const keyword of rule.from) {
106
- pairs.push({ pattern: keyword, to: rule.to, len: keyword.length });
107
- }
108
- }
109
- pairs.sort((a, b) => b.len - a.len);
110
- return pairs;
111
- }
112
-
113
- /**
114
- * 규칙 배열로부터 확장 쌍을 빌드
115
- * @param {Array<{ from: string[], to: string }>} rules
116
- * @returns {Array<{ symbol: string, restore: string }>}
117
- */
118
- function buildExpandPairs(rules) {
119
- return rules.map((rule) => ({ symbol: rule.to, restore: rule.from[0] }));
120
- }
121
-
122
- // 기본 프로필 쌍 (기존 동작 유지)
123
- const _compactPairs = buildCompactPairs(COMPACT_RULES);
124
- const _expandPairs = buildExpandPairs(COMPACT_RULES);
125
-
126
- // ── 코드 블록 보호 유틸 ──
127
-
128
- const CODE_BLOCK_RE = /```[\s\S]*?```/g;
129
-
130
- /**
131
- * 코드 블록을 플레이스홀더로 치환, 변환 후 복원
132
- * @param {string} text
133
- * @param {(segment: string) => string} transform
134
- * @returns {string}
135
- */
136
- function withCodeProtection(text, transform) {
137
- const blocks = [];
138
- const placeholder = "\x00CB";
139
- let idx = 0;
140
- const masked = text.replace(CODE_BLOCK_RE, (match) => {
141
- blocks.push(match);
142
- return `${placeholder}${idx++}${placeholder}`;
143
- });
144
-
145
- const transformed = transform(masked);
146
-
147
- // 플레이스홀더 복원
148
- return transformed.replace(
149
- new RegExp(`${placeholder}(\\d+)${placeholder}`, "g"),
150
- (_, i) => blocks[Number(i)],
151
- );
152
- }
153
-
154
- // ── compact 모드 상태 ──
155
-
156
- let _compactMode = false;
157
-
158
- /**
159
- * compact 모드 활성 여부
160
- * @returns {boolean}
161
- */
162
- export function isCompactMode() {
163
- return _compactMode;
164
- }
165
-
166
- /**
167
- * 텍스트를 compact 모드로 변환
168
- * 심볼 치환 + 약어 적용, 코드 블록 내부는 보호
169
- * @param {string} text
170
- * @returns {string}
171
- */
172
- export function compactify(text) {
173
- if (!text || typeof text !== "string") return text ?? "";
174
- _compactMode = true;
175
-
176
- return withCodeProtection(text, (segment) => {
177
- let result = segment;
178
- for (const { pattern, to } of _compactPairs) {
179
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
- const re = new RegExp(escaped, "gi");
181
- result = result.replace(re, to);
182
- }
183
- return result;
184
- });
185
- }
186
-
187
- /**
188
- * 텍스트를 compact 모드로 변환 (도메인 프로필 선택 가능)
189
- * @param {string} text
190
- * @param {'default'|'review'|'design'|'docs'} [profile='default'] — 도메인 프로필
191
- * @returns {string}
192
- */
193
- export function applyCompactRules(text, profile = "default") {
194
- if (!text || typeof text !== "string") return text ?? "";
195
-
196
- const rules = PROFILE_MAP[profile] ?? COMPACT_RULES;
197
- const pairs = buildCompactPairs(rules);
198
-
199
- return withCodeProtection(text, (segment) => {
200
- let result = segment;
201
- for (const { pattern, to } of pairs) {
202
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
203
- const re = new RegExp(escaped, "gi");
204
- result = result.replace(re, to);
205
- }
206
- return result;
207
- });
208
- }
209
-
210
- /**
211
- * compact 텍스트를 원래 형태로 복원 (best-effort)
212
- * @param {string} text
213
- * @returns {string}
214
- */
215
- export function expand(text) {
216
- if (!text || typeof text !== "string") return text ?? "";
217
- _compactMode = false;
218
-
219
- return withCodeProtection(text, (segment) => {
220
- let result = segment;
221
- for (const { symbol, restore } of _expandPairs) {
222
- const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
223
- const re = new RegExp(escaped, "g");
224
- result = result.replace(re, restore);
225
- }
226
- return result;
227
- });
228
- }
@@ -1,56 +0,0 @@
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 =
44
- agents.length > 0
45
- ? Math.round(
46
- agents.reduce((sum, a) => sum + a.progress, 0) / agents.length,
47
- )
48
- : 0;
49
-
50
- return {
51
- agents,
52
- // TODO: derive dagLevel from real mission dependency metadata instead of hardcoding 0.
53
- dagLevel: 0,
54
- totalProgress,
55
- };
56
- }
@@ -1,76 +0,0 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { resolveRoleControlSnapshot } from "../../hub/lib/cto-env.mjs";
4
- import {
5
- CTO_LAKE_CURRENT_RELATIVE_PATH,
6
- CTO_STATUS_LINE_MAX_CHARS,
7
- CTO_STATUS_STALE_MS,
8
- } from "../constants.mjs";
9
-
10
- function oneLine(value) {
11
- return String(value || "")
12
- .replace(/\x1b\[[0-9;]*m/g, "")
13
- .replace(/\s+/g, " ")
14
- .trim();
15
- }
16
-
17
- function capLine(line) {
18
- const text = oneLine(line);
19
- if (text.length <= CTO_STATUS_LINE_MAX_CHARS) return text;
20
- return `${text.slice(0, CTO_STATUS_LINE_MAX_CHARS - 3)}...`;
21
- }
22
-
23
- function resolveCurrentPath({ currentPath, rootDir } = {}) {
24
- if (currentPath) return currentPath;
25
- return join(rootDir || process.cwd(), CTO_LAKE_CURRENT_RELATIVE_PATH);
26
- }
27
-
28
- function extractBriefVersion(lines) {
29
- const versionLine = lines.find((line) => /^brief_version:\s*/i.test(line));
30
- return oneLine(versionLine?.replace(/^brief_version:\s*/i, "")) || null;
31
- }
32
-
33
- function extractStatusLine(lines) {
34
- const summaryLine = lines.find((line) => /^summary:\s*/i.test(line));
35
- const summary = oneLine(summaryLine?.replace(/^summary:\s*/i, ""));
36
- if (summary) return capLine(summary);
37
-
38
- const sectionHeaders = new Set([
39
- "repo_state",
40
- "active_goals",
41
- "swarm_shards",
42
- "hub_status",
43
- "recent_events",
44
- "generated_at",
45
- "injection_note",
46
- ]);
47
- const fallback = lines.find((line) => {
48
- const clean = oneLine(line);
49
- return (
50
- clean && !/^brief_version:\s*/i.test(clean) && !sectionHeaders.has(clean)
51
- );
52
- });
53
- return fallback ? capLine(fallback) : null;
54
- }
55
-
56
- export function readCtoStatus(options = {}) {
57
- if (!resolveRoleControlSnapshot(options.env).auto_collect_enabled)
58
- return null;
59
- try {
60
- const currentPath = resolveCurrentPath(options);
61
- const stat = statSync(currentPath);
62
- const raw = readFileSync(currentPath, "utf8");
63
- const lines = raw.split(/\r?\n/u).map(oneLine).filter(Boolean);
64
- const line = extractStatusLine(lines);
65
- if (!line) return null;
66
-
67
- const version = extractBriefVersion(lines);
68
- const isStale = Date.now() - stat.mtimeMs > CTO_STATUS_STALE_MS;
69
- return {
70
- line,
71
- rightTag: isStale ? "stale" : version || "cto",
72
- };
73
- } catch {
74
- return null;
75
- }
76
- }
package/mesh/index.mjs DELETED
@@ -1,69 +0,0 @@
1
- import { readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- export { createMeshBudget } from "./mesh-budget.mjs";
5
- export { createHeartbeatMonitor } from "./mesh-heartbeat.mjs";
6
- export {
7
- createMessage,
8
- deserialize,
9
- MSG_TYPES,
10
- serialize,
11
- validate,
12
- } from "./mesh-protocol.mjs";
13
- export { createMessageQueue } from "./mesh-queue.mjs";
14
- export { createRegistry } from "./mesh-registry.mjs";
15
- export { routeMessage, routeOrDeadLetter } from "./mesh-router.mjs";
16
-
17
- /**
18
- * Loads skills assigned to a specific agent from a skills directory.
19
- * Reuses the same directory-scan approach as generateSkillDocs().
20
- *
21
- * @param {string} agentId - The agent identifier
22
- * @param {string} skillsDir - Path to the skills directory
23
- * @returns {Promise<string[]>} Array of skill names available to this agent
24
- */
25
- export async function loadSkillsForAgent(agentId, skillsDir) {
26
- if (!agentId || typeof agentId !== "string") {
27
- throw new TypeError("agentId must be a non-empty string");
28
- }
29
- if (!skillsDir || typeof skillsDir !== "string") {
30
- throw new TypeError("skillsDir must be a non-empty string");
31
- }
32
-
33
- let entries;
34
- try {
35
- entries = readdirSync(skillsDir, { withFileTypes: true });
36
- } catch {
37
- return [];
38
- }
39
-
40
- const skills = [];
41
- for (const entry of entries) {
42
- if (!entry.isDirectory()) continue;
43
- const skillName = entry.name;
44
- const skillPath = join(skillsDir, skillName, "SKILL.md");
45
- let skillContent = null;
46
- try {
47
- const { readFileSync } = await import("node:fs");
48
- skillContent = readFileSync(skillPath, "utf8");
49
- } catch {
50
- // Skill has no SKILL.md — include it anyway
51
- }
52
-
53
- // If SKILL.md mentions the agentId or no agent restriction, include it
54
- const isRestricted = skillContent
55
- ? /^agents?\s*:/im.test(skillContent)
56
- : false;
57
-
58
- if (!isRestricted) {
59
- skills.push(skillName);
60
- continue;
61
- }
62
-
63
- if (skillContent?.includes(agentId)) {
64
- skills.push(skillName);
65
- }
66
- }
67
-
68
- return skills;
69
- }
@@ -1,137 +0,0 @@
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)
32
- return { level: "critical", message: "에이전트 분할 또는 세션 교체 권장" };
33
- if (p >= WARNING_LEVELS.warn) return { level: "warn", message: "압축 권장" };
34
- if (p >= WARNING_LEVELS.info)
35
- return { level: "info", message: "컨텍스트 절반 이상 사용" };
36
- return { level: "ok", message: "" };
37
- }
38
-
39
- /**
40
- * Creates a per-agent token budget manager.
41
- * @returns {object} Budget API
42
- */
43
- export function createMeshBudget() {
44
- // Map<agentId, { allocated: number, consumed: number }>
45
- const budgets = new Map();
46
-
47
- /**
48
- * Allocates a token budget to an agent.
49
- * @param {string} agentId
50
- * @param {number} tokenLimit
51
- */
52
- function allocate(agentId, tokenLimit) {
53
- if (!agentId || typeof agentId !== "string") {
54
- throw new TypeError("agentId must be a non-empty string");
55
- }
56
- const limit = Math.max(0, Math.round(Number(tokenLimit) || 0));
57
- const existing = budgets.get(agentId);
58
- budgets.set(agentId, {
59
- allocated: limit,
60
- consumed: existing?.consumed ?? 0,
61
- });
62
- }
63
-
64
- /**
65
- * Records token consumption for an agent.
66
- * @param {string} agentId
67
- * @param {number} tokens
68
- * @returns {{ remaining: number, percent: number, level: string }}
69
- */
70
- function consume(agentId, tokens) {
71
- const budget = budgets.get(agentId);
72
- if (!budget) {
73
- throw new Error(`No budget allocated for agent: ${agentId}`);
74
- }
75
- const amount = Math.max(0, Math.round(Number(tokens) || 0));
76
- const updated = {
77
- allocated: budget.allocated,
78
- consumed: budget.consumed + amount,
79
- };
80
- budgets.set(agentId, updated);
81
-
82
- const remaining = Math.max(0, updated.allocated - updated.consumed);
83
- const percent =
84
- updated.allocated > 0
85
- ? clampPercent((updated.consumed / updated.allocated) * 100)
86
- : 100;
87
- const { level } = classifyLevel(percent);
88
- return { remaining, percent, level };
89
- }
90
-
91
- /**
92
- * Returns the budget status for an agent.
93
- * @param {string} agentId
94
- * @returns {{ allocated: number, consumed: number, remaining: number, level: string }}
95
- */
96
- function getStatus(agentId) {
97
- const budget = budgets.get(agentId);
98
- if (!budget) {
99
- return { allocated: 0, consumed: 0, remaining: 0, level: "ok" };
100
- }
101
- const remaining = Math.max(0, budget.allocated - budget.consumed);
102
- const percent =
103
- budget.allocated > 0
104
- ? clampPercent((budget.consumed / budget.allocated) * 100)
105
- : 0;
106
- const { level } = classifyLevel(percent);
107
- return {
108
- allocated: budget.allocated,
109
- consumed: budget.consumed,
110
- remaining,
111
- level,
112
- };
113
- }
114
-
115
- /**
116
- * Resets consumed tokens for all agents (keeps allocations).
117
- */
118
- function resetAll() {
119
- for (const [agentId, budget] of budgets) {
120
- budgets.set(agentId, { allocated: budget.allocated, consumed: 0 });
121
- }
122
- }
123
-
124
- /**
125
- * Returns a snapshot of all current allocations.
126
- * @returns {Map<string, { allocated: number, consumed: number }>}
127
- */
128
- function listAllocations() {
129
- const snap = new Map();
130
- for (const [id, b] of budgets) {
131
- snap.set(id, Object.freeze({ ...b }));
132
- }
133
- return snap;
134
- }
135
-
136
- return { allocate, consume, getStatus, resetAll, listAllocations };
137
- }
@@ -1,100 +0,0 @@
1
- const DEFAULT_INTERVAL_MS = 30_000;
2
- const DEFAULT_THRESHOLD_MS = 60_000;
3
-
4
- /**
5
- * Creates a heartbeat monitor that tracks agent liveness.
6
- *
7
- * @param {object} registry - A mesh-registry instance
8
- * @param {object} [opts]
9
- * @param {number} [opts.intervalMs=30000] - Scan interval
10
- * @param {number} [opts.thresholdMs=60000] - Stale threshold
11
- * @param {function} [opts.onStale] - Called with agentId when stale detected
12
- * @returns {object} HeartbeatMonitor API
13
- */
14
- export function createHeartbeatMonitor(registry, opts = {}) {
15
- const {
16
- intervalMs = DEFAULT_INTERVAL_MS,
17
- thresholdMs = DEFAULT_THRESHOLD_MS,
18
- onStale,
19
- } = opts;
20
-
21
- /** @type {Map<string, number>} agentId → last heartbeat timestamp */
22
- const heartbeats = new Map();
23
- let timer = null;
24
-
25
- /**
26
- * Records a heartbeat for an agent.
27
- * @param {string} agentId
28
- */
29
- function recordHeartbeat(agentId) {
30
- if (!agentId || typeof agentId !== "string") {
31
- throw new TypeError("agentId must be a non-empty string");
32
- }
33
- heartbeats.set(agentId, Date.now());
34
- }
35
-
36
- /**
37
- * Returns agent IDs whose last heartbeat exceeds the threshold.
38
- * Only considers agents currently registered in the registry.
39
- *
40
- * @param {number} [customThresholdMs] - Override default threshold
41
- * @returns {string[]}
42
- */
43
- function getStaleAgents(customThresholdMs) {
44
- const threshold = customThresholdMs ?? thresholdMs;
45
- const now = Date.now();
46
- const registered = registry.listAll();
47
- const stale = [];
48
-
49
- for (const agent of registered) {
50
- const lastBeat = heartbeats.get(agent.agentId);
51
- if (lastBeat === undefined || now - lastBeat >= threshold) {
52
- stale.push(agent.agentId);
53
- }
54
- }
55
- return stale;
56
- }
57
-
58
- /**
59
- * Runs a single scan: finds stale agents and invokes onStale callback.
60
- */
61
- function scan() {
62
- const stale = getStaleAgents();
63
- if (typeof onStale === "function") {
64
- for (const agentId of stale) {
65
- onStale(agentId);
66
- }
67
- }
68
- }
69
-
70
- /**
71
- * Starts periodic heartbeat scanning.
72
- * @param {number} [customIntervalMs]
73
- */
74
- function start(customIntervalMs) {
75
- stop();
76
- const interval = customIntervalMs ?? intervalMs;
77
- timer = setInterval(scan, interval);
78
- timer.unref?.();
79
- }
80
-
81
- /**
82
- * Stops periodic scanning.
83
- */
84
- function stop() {
85
- if (timer !== null) {
86
- clearInterval(timer);
87
- timer = null;
88
- }
89
- }
90
-
91
- /**
92
- * Removes heartbeat record for an agent.
93
- * @param {string} agentId
94
- */
95
- function remove(agentId) {
96
- heartbeats.delete(agentId);
97
- }
98
-
99
- return { recordHeartbeat, getStaleAgents, start, stop, scan, remove };
100
- }