@triflux/core 10.50.2 → 10.52.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/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +65 -1250
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -19
- package/hub/lib/process-utils.mjs +1 -156
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/nativeProxy.mjs +759 -0
- package/hub/team/session-context.mjs +1 -1
- package/hud/cli-policy.mjs +0 -22
- package/hud/constants.mjs +13 -26
- package/hud/hud-qos-status.mjs +42 -54
- package/hud/providers/gemini.mjs +125 -20
- package/hud/renderers.mjs +101 -122
- package/hud/utils.mjs +10 -6
- package/package.json +2 -6
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/session-presence.mjs +2 -100
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/adaptive-diagnostic.mjs +0 -350
- package/hub/adaptive-inject.mjs +0 -224
- package/hub/adaptive-memory.mjs +0 -277
- package/hub/adaptive.mjs +0 -172
- package/hub/assign-callbacks.mjs +0 -170
- package/hub/delegator/contracts.mjs +0 -38
- package/hub/delegator/index.mjs +0 -14
- package/hub/delegator/schema/delegator-tools.schema.json +0 -250
- package/hub/delegator/service.mjs +0 -336
- package/hub/delegator/tool-definitions.mjs +0 -35
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/hitl.mjs +0 -202
- package/hub/lib/known-errors.json +0 -72
- package/hub/lib/memory-store.mjs +0 -1001
- package/hub/lib/phase-manager.mjs +0 -493
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/state-snapshot.mjs +0 -293
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/trace-recorder.mjs +0 -153
- package/hub/lib/uuidv7.mjs +0 -45
- package/hub/memory-doctor.mjs +0 -640
- package/hub/middleware/quota-middleware.mjs +0 -70
- package/hub/middleware/request-logger.mjs +0 -191
- package/hub/paths.mjs +0 -24
- package/hub/pipeline/index.mjs +0 -163
- package/hub/pipeline/state.mjs +0 -191
- package/hub/pipeline/transitions.mjs +0 -131
- package/hub/reflexion.mjs +0 -290
- package/hub/role-contract.mjs +0 -314
- package/hub/router.mjs +0 -2026
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/session-fingerprint.mjs +0 -418
- package/hub/state.mjs +0 -300
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team-bridge.mjs +0 -27
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
// hub/lib/tfx-route-args.mjs
|
|
2
|
-
// Phase 3 Step B — tfx-auto / tfx-route 플래그 파서.
|
|
3
|
-
// 설계 문서: .triflux/plans/phase3-lead-codex-ralph-escalate.md
|
|
4
|
-
//
|
|
5
|
-
// 입력: ARGUMENTS 문자열 또는 토큰 배열.
|
|
6
|
-
// 출력: {cli, mode, parallel, retry, isolation, remote, lead, noClaudeNative,
|
|
7
|
-
// maxIterations, task, warnings}.
|
|
8
|
-
//
|
|
9
|
-
// 기존 플래그 (Phase 2 v10.9.33+):
|
|
10
|
-
// --cli {auto|codex|antigravity|claude}
|
|
11
|
-
// --mode {quick|deep|consensus|live}
|
|
12
|
-
// --parallel {1|N|swarm}
|
|
13
|
-
// --retry {0|1|ralph|auto-escalate} (Phase 3 에서 ralph/auto-escalate 신규)
|
|
14
|
-
// --isolation {none|worktree}
|
|
15
|
-
// --remote {none|<host>}
|
|
16
|
-
// --rounds <N> (--mode live 전용, 기본 4)
|
|
17
|
-
//
|
|
18
|
-
// Phase 3 신규:
|
|
19
|
-
// --lead {claude|codex} (tfx-auto-codex 의미 흡수)
|
|
20
|
-
// --no-claude-native (Claude native sub-agent 경로 disable)
|
|
21
|
-
// --max-iterations <N> (ralph / auto-escalate 상한, 0=unlimited)
|
|
22
|
-
|
|
23
|
-
import { hasCodeChange } from "./staged-file-detect.mjs";
|
|
24
|
-
|
|
25
|
-
export const DEFAULT_OPTIONS = Object.freeze({
|
|
26
|
-
cli: "auto",
|
|
27
|
-
mode: "quick",
|
|
28
|
-
parallel: "1",
|
|
29
|
-
retry: "1",
|
|
30
|
-
isolation: "none",
|
|
31
|
-
remote: "none",
|
|
32
|
-
lead: "claude",
|
|
33
|
-
noClaudeNative: false,
|
|
34
|
-
maxIterations: 0,
|
|
35
|
-
rounds: 4,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const VALID_VALUES = Object.freeze({
|
|
39
|
-
cli: ["auto", "codex", "antigravity", "claude"],
|
|
40
|
-
mode: ["quick", "deep", "consensus", "live"],
|
|
41
|
-
retry: ["0", "1", "ralph", "auto-escalate"],
|
|
42
|
-
isolation: ["none", "worktree"],
|
|
43
|
-
lead: ["claude", "codex"],
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const VALUE_FLAGS = new Set([
|
|
47
|
-
"--cli",
|
|
48
|
-
"--mode",
|
|
49
|
-
"--parallel",
|
|
50
|
-
"--retry",
|
|
51
|
-
"--isolation",
|
|
52
|
-
"--remote",
|
|
53
|
-
"--lead",
|
|
54
|
-
"--max-iterations",
|
|
55
|
-
"--rounds",
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
const BOOL_FLAGS = new Set(["--no-claude-native"]);
|
|
59
|
-
|
|
60
|
-
export function parseArgs(input) {
|
|
61
|
-
const tokens = Array.isArray(input)
|
|
62
|
-
? input.slice()
|
|
63
|
-
: tokenize(String(input || ""));
|
|
64
|
-
const opts = { ...DEFAULT_OPTIONS };
|
|
65
|
-
const warnings = [];
|
|
66
|
-
const taskTokens = [];
|
|
67
|
-
const providedFlags = new Set();
|
|
68
|
-
|
|
69
|
-
let i = 0;
|
|
70
|
-
while (i < tokens.length) {
|
|
71
|
-
const raw = tokens[i];
|
|
72
|
-
|
|
73
|
-
if (BOOL_FLAGS.has(raw)) {
|
|
74
|
-
providedFlags.add(raw);
|
|
75
|
-
applyBool(opts, raw);
|
|
76
|
-
i += 1;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// --flag=value 지원
|
|
81
|
-
const eqIdx = raw.indexOf("=");
|
|
82
|
-
let flag = raw;
|
|
83
|
-
let value = null;
|
|
84
|
-
if (eqIdx > 0 && raw.startsWith("--")) {
|
|
85
|
-
flag = raw.slice(0, eqIdx);
|
|
86
|
-
value = raw.slice(eqIdx + 1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (VALUE_FLAGS.has(flag)) {
|
|
90
|
-
providedFlags.add(flag);
|
|
91
|
-
if (value === null) {
|
|
92
|
-
const next = tokens[i + 1];
|
|
93
|
-
if (next === undefined || next.startsWith("--")) {
|
|
94
|
-
warnings.push(`${flag} needs a value`);
|
|
95
|
-
i += 1;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
value = next;
|
|
99
|
-
i += 2;
|
|
100
|
-
} else {
|
|
101
|
-
i += 1;
|
|
102
|
-
}
|
|
103
|
-
applyValue(opts, flag, value, warnings);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (raw.startsWith("--")) {
|
|
108
|
-
warnings.push(`unknown flag: ${raw}`);
|
|
109
|
-
i += 1;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
taskTokens.push(raw);
|
|
114
|
-
i += 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
validate(opts, warnings, providedFlags);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
...opts,
|
|
121
|
-
task: taskTokens.join(" ").trim(),
|
|
122
|
-
warnings,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function tokenize(str) {
|
|
127
|
-
const tokens = [];
|
|
128
|
-
let cur = "";
|
|
129
|
-
let quote = null;
|
|
130
|
-
for (const ch of str) {
|
|
131
|
-
if (quote) {
|
|
132
|
-
if (ch === quote) {
|
|
133
|
-
quote = null;
|
|
134
|
-
} else {
|
|
135
|
-
cur += ch;
|
|
136
|
-
}
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (ch === '"' || ch === "'") {
|
|
140
|
-
quote = ch;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (/\s/.test(ch)) {
|
|
144
|
-
if (cur) {
|
|
145
|
-
tokens.push(cur);
|
|
146
|
-
cur = "";
|
|
147
|
-
}
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
cur += ch;
|
|
151
|
-
}
|
|
152
|
-
if (cur) tokens.push(cur);
|
|
153
|
-
return tokens;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function applyBool(opts, flag) {
|
|
157
|
-
switch (flag) {
|
|
158
|
-
case "--no-claude-native":
|
|
159
|
-
opts.noClaudeNative = true;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function applyValue(opts, flag, value, warnings) {
|
|
165
|
-
switch (flag) {
|
|
166
|
-
case "--cli":
|
|
167
|
-
if (value === "gemini") {
|
|
168
|
-
opts.cli = "antigravity";
|
|
169
|
-
warnings.push(
|
|
170
|
-
"--cli gemini is deprecated; using --cli antigravity instead",
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
opts.cli = value;
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
case "--mode":
|
|
177
|
-
opts.mode = value;
|
|
178
|
-
break;
|
|
179
|
-
case "--parallel":
|
|
180
|
-
opts.parallel = value;
|
|
181
|
-
break;
|
|
182
|
-
case "--retry":
|
|
183
|
-
opts.retry = value;
|
|
184
|
-
break;
|
|
185
|
-
case "--isolation":
|
|
186
|
-
opts.isolation = value;
|
|
187
|
-
break;
|
|
188
|
-
case "--remote":
|
|
189
|
-
opts.remote = value;
|
|
190
|
-
break;
|
|
191
|
-
case "--lead":
|
|
192
|
-
opts.lead = value;
|
|
193
|
-
break;
|
|
194
|
-
case "--max-iterations": {
|
|
195
|
-
const n = Number.parseInt(value, 10);
|
|
196
|
-
if (Number.isNaN(n) || n < 0) {
|
|
197
|
-
warnings.push(
|
|
198
|
-
`invalid --max-iterations=${value}, expected non-negative integer (0=unlimited)`,
|
|
199
|
-
);
|
|
200
|
-
} else {
|
|
201
|
-
opts.maxIterations = n;
|
|
202
|
-
}
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
case "--rounds": {
|
|
206
|
-
const n = Number(value);
|
|
207
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
208
|
-
warnings.push(`invalid --rounds=${value}, expected positive integer`);
|
|
209
|
-
} else {
|
|
210
|
-
opts.rounds = n;
|
|
211
|
-
}
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function validate(opts, warnings, providedFlags = new Set()) {
|
|
218
|
-
for (const key of Object.keys(VALID_VALUES)) {
|
|
219
|
-
if (!VALID_VALUES[key].includes(opts[key])) {
|
|
220
|
-
warnings.push(
|
|
221
|
-
`invalid --${key}=${opts[key]}, expected one of ${VALID_VALUES[key].join("|")}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (
|
|
226
|
-
opts.parallel !== "1" &&
|
|
227
|
-
opts.parallel !== "swarm" &&
|
|
228
|
-
!/^\d+$/.test(opts.parallel)
|
|
229
|
-
) {
|
|
230
|
-
warnings.push(`invalid --parallel=${opts.parallel}, expected 1|N|swarm`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (opts.mode === "live") {
|
|
234
|
-
const ignored = [
|
|
235
|
-
["--parallel", "parallel", DEFAULT_OPTIONS.parallel],
|
|
236
|
-
["--isolation", "isolation", DEFAULT_OPTIONS.isolation],
|
|
237
|
-
["--remote", "remote", DEFAULT_OPTIONS.remote],
|
|
238
|
-
];
|
|
239
|
-
for (const [flag, key, fallback] of ignored) {
|
|
240
|
-
if (providedFlags.has(flag)) {
|
|
241
|
-
warnings.push(`${flag} ignored with --mode live`);
|
|
242
|
-
opts[key] = fallback;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (providedFlags.has("--retry") && opts.retry === "ralph") {
|
|
246
|
-
warnings.push("--retry ralph ignored with --mode live");
|
|
247
|
-
opts.retry = DEFAULT_OPTIONS.retry;
|
|
248
|
-
}
|
|
249
|
-
} else if (providedFlags.has("--rounds")) {
|
|
250
|
-
warnings.push("--rounds ignored unless --mode live");
|
|
251
|
-
opts.rounds = DEFAULT_OPTIONS.rounds;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const parallelOne = opts.parallel === "1" || opts.parallel === 1;
|
|
255
|
-
if (parallelOne && opts.isolation === "worktree") {
|
|
256
|
-
warnings.push(
|
|
257
|
-
"--isolation worktree requires --parallel >=2 or swarm; forcing isolation=none",
|
|
258
|
-
);
|
|
259
|
-
opts.isolation = "none";
|
|
260
|
-
}
|
|
261
|
-
if (opts.remote !== "none" && opts.parallel !== "swarm") {
|
|
262
|
-
warnings.push(
|
|
263
|
-
`--remote ${opts.remote} ignored (requires --parallel swarm)`,
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export { VALID_VALUES };
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Decide dispatch mode based on parsed args and current git state.
|
|
272
|
-
*
|
|
273
|
-
* Issue #281: auto router code changes should use isolated swarm dispatch for
|
|
274
|
-
* multi-task work unless the user explicitly selected local parallel mode.
|
|
275
|
-
*
|
|
276
|
-
* @param {object} args
|
|
277
|
-
* @param {object} [opts]
|
|
278
|
-
* @param {string} [opts.cwd]
|
|
279
|
-
* @param {Function} [opts.detector]
|
|
280
|
-
* @returns {{
|
|
281
|
-
* mode: "single" | "multi" | "swarm",
|
|
282
|
-
* escalated: boolean,
|
|
283
|
-
* warning: string | null,
|
|
284
|
-
* reason: string,
|
|
285
|
-
* }}
|
|
286
|
-
*/
|
|
287
|
-
export function decideDispatchMode(
|
|
288
|
-
args = {},
|
|
289
|
-
{ cwd = process.cwd(), detector = hasCodeChange } = {},
|
|
290
|
-
) {
|
|
291
|
-
const tasksCount = countTasks(args);
|
|
292
|
-
const parallel = normalizeParallel(args.parallel);
|
|
293
|
-
const hasUserParallel = parallel !== null && parallel !== "1";
|
|
294
|
-
const hasUserSwarm = parallel === "swarm" || args.isolation === "worktree";
|
|
295
|
-
const codeChanged = Boolean(detector({ cwd }));
|
|
296
|
-
|
|
297
|
-
if (hasUserSwarm) {
|
|
298
|
-
return {
|
|
299
|
-
mode: "swarm",
|
|
300
|
-
escalated: false,
|
|
301
|
-
warning: null,
|
|
302
|
-
reason: "user-explicit-swarm",
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (hasUserParallel && codeChanged) {
|
|
307
|
-
return {
|
|
308
|
-
mode: "multi",
|
|
309
|
-
escalated: false,
|
|
310
|
-
warning: `[tfx-auto] WARNING: 코드 변경 감지 (cwd race 위험). --parallel swarm --isolation worktree 권장. 사용자 명시 --parallel=${args.parallel} 존중하여 multi 로 진행.`,
|
|
311
|
-
reason: "user-explicit-multi-with-code-change",
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (tasksCount >= 2 && codeChanged) {
|
|
316
|
-
return {
|
|
317
|
-
mode: "swarm",
|
|
318
|
-
escalated: true,
|
|
319
|
-
warning: `[tfx-auto] 코드 변경 ${tasksCount}건 태스크 + dirty cwd 감지. swarm dispatch 자동 escalate (--parallel swarm --isolation worktree). Issue #281.`,
|
|
320
|
-
reason: "auto-escalate-code-change",
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (tasksCount >= 2 || hasUserParallel) {
|
|
325
|
-
return {
|
|
326
|
-
mode: "multi",
|
|
327
|
-
escalated: false,
|
|
328
|
-
warning: null,
|
|
329
|
-
reason: "multi-no-code-change",
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
mode: "single",
|
|
335
|
-
escalated: false,
|
|
336
|
-
warning: null,
|
|
337
|
-
reason: "single-task",
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function countTasks(args) {
|
|
342
|
-
if (Array.isArray(args.tasks)) return args.tasks.length;
|
|
343
|
-
if (typeof args.tasks === "number") return args.tasks;
|
|
344
|
-
if (typeof args.tasks === "string") return args.tasks.trim() ? 1 : 0;
|
|
345
|
-
if (typeof args.task === "string") return args.task.trim() ? 1 : 0;
|
|
346
|
-
return args.tasks ?? 0;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function normalizeParallel(value) {
|
|
350
|
-
if (value == null) return null;
|
|
351
|
-
if (value === "swarm") return value;
|
|
352
|
-
return String(value);
|
|
353
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
const VALID_CLIENTS = new Set(["codex", "claude", "gemini", "unknown"]);
|
|
2
|
-
const VALID_PHASES = new Set(["ingress", "handler", "store", "event-loop"]);
|
|
3
|
-
const SAMPLE_LIMIT = 1000;
|
|
4
|
-
|
|
5
|
-
const categories = new Map();
|
|
6
|
-
const workerDurations = [];
|
|
7
|
-
const workerCounts = {
|
|
8
|
-
spawned: 0,
|
|
9
|
-
exited: 0,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function debugInvalid(kind, details) {
|
|
13
|
-
if (process.env.TFX_TRACE_DEBUG !== "1") return;
|
|
14
|
-
console.debug(`[trace-recorder] ignored invalid ${kind}`, details);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeClient(client) {
|
|
18
|
-
const value = String(client || "unknown").toLowerCase();
|
|
19
|
-
return VALID_CLIENTS.has(value) ? value : "unknown";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizePhase(phase) {
|
|
23
|
-
const value = String(phase || "handler");
|
|
24
|
-
return VALID_PHASES.has(value) ? value : "handler";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function validDuration(durMs) {
|
|
28
|
-
return Number.isFinite(durMs) && durMs >= 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function emptyStats() {
|
|
32
|
-
return {
|
|
33
|
-
count: 0,
|
|
34
|
-
total: 0,
|
|
35
|
-
min: null,
|
|
36
|
-
max: null,
|
|
37
|
-
samples: [],
|
|
38
|
-
phases: {},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function addDuration(stats, durMs, phase) {
|
|
43
|
-
stats.count += 1;
|
|
44
|
-
stats.total += durMs;
|
|
45
|
-
stats.min = stats.min == null ? durMs : Math.min(stats.min, durMs);
|
|
46
|
-
stats.max = stats.max == null ? durMs : Math.max(stats.max, durMs);
|
|
47
|
-
stats.samples.push(durMs);
|
|
48
|
-
if (stats.samples.length > SAMPLE_LIMIT) {
|
|
49
|
-
stats.samples.splice(0, stats.samples.length - SAMPLE_LIMIT);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const phaseStats = stats.phases[phase] || emptyPhaseStats();
|
|
53
|
-
phaseStats.count += 1;
|
|
54
|
-
phaseStats.total += durMs;
|
|
55
|
-
phaseStats.min =
|
|
56
|
-
phaseStats.min == null ? durMs : Math.min(phaseStats.min, durMs);
|
|
57
|
-
phaseStats.max =
|
|
58
|
-
phaseStats.max == null ? durMs : Math.max(phaseStats.max, durMs);
|
|
59
|
-
stats.phases[phase] = phaseStats;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function emptyPhaseStats() {
|
|
63
|
-
return { count: 0, total: 0, min: null, max: null };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function percentile(samples, percentileValue) {
|
|
67
|
-
if (!samples.length) return null;
|
|
68
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
69
|
-
const index = Math.ceil((percentileValue / 100) * sorted.length) - 1;
|
|
70
|
-
return sorted[Math.max(0, Math.min(sorted.length - 1, index))];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function publicStats(stats) {
|
|
74
|
-
return {
|
|
75
|
-
count: stats.count,
|
|
76
|
-
total: stats.total,
|
|
77
|
-
min: stats.min,
|
|
78
|
-
max: stats.max,
|
|
79
|
-
p50: percentile(stats.samples, 50),
|
|
80
|
-
p95: percentile(stats.samples, 95),
|
|
81
|
-
p99: percentile(stats.samples, 99),
|
|
82
|
-
phases: stats.phases,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function recordRequest(
|
|
87
|
-
category,
|
|
88
|
-
durMs,
|
|
89
|
-
client = "unknown",
|
|
90
|
-
phase = "handler",
|
|
91
|
-
) {
|
|
92
|
-
if (!category || typeof category !== "string") return;
|
|
93
|
-
if (!validDuration(durMs)) {
|
|
94
|
-
debugInvalid("request duration", { category, durMs, client, phase });
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const clientKey = normalizeClient(client);
|
|
99
|
-
const phaseKey = normalizePhase(phase);
|
|
100
|
-
const bucket = categories.get(category) || new Map();
|
|
101
|
-
const stats = bucket.get(clientKey) || emptyStats();
|
|
102
|
-
addDuration(stats, durMs, phaseKey);
|
|
103
|
-
bucket.set(clientKey, stats);
|
|
104
|
-
categories.set(category, bucket);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function recordWorker(workerId, kind, command, durMs) {
|
|
108
|
-
if (!workerId || typeof workerId !== "string") return;
|
|
109
|
-
if (kind === "spawned") {
|
|
110
|
-
workerCounts.spawned += 1;
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (kind !== "exited") return;
|
|
114
|
-
|
|
115
|
-
workerCounts.exited += 1;
|
|
116
|
-
if (durMs == null) return;
|
|
117
|
-
if (!validDuration(durMs)) {
|
|
118
|
-
debugInvalid("worker duration", { workerId, kind, command, durMs });
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
workerDurations.push(durMs);
|
|
122
|
-
if (workerDurations.length > SAMPLE_LIMIT) {
|
|
123
|
-
workerDurations.splice(0, workerDurations.length - SAMPLE_LIMIT);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function snapshot() {
|
|
128
|
-
const categorySnapshot = {};
|
|
129
|
-
for (const [category, bucket] of categories.entries()) {
|
|
130
|
-
categorySnapshot[category] = {};
|
|
131
|
-
for (const [client, stats] of bucket.entries()) {
|
|
132
|
-
categorySnapshot[category][client] = publicStats(stats);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
categories: categorySnapshot,
|
|
138
|
-
workers: {
|
|
139
|
-
spawned: workerCounts.spawned,
|
|
140
|
-
exited: workerCounts.exited,
|
|
141
|
-
p50: percentile(workerDurations, 50),
|
|
142
|
-
p95: percentile(workerDurations, 95),
|
|
143
|
-
p99: percentile(workerDurations, 99),
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function reset() {
|
|
149
|
-
categories.clear();
|
|
150
|
-
workerDurations.length = 0;
|
|
151
|
-
workerCounts.spawned = 0;
|
|
152
|
-
workerCounts.exited = 0;
|
|
153
|
-
}
|
package/hub/lib/uuidv7.mjs
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// hub/lib/uuidv7.mjs — UUIDv7 generator (RFC 9562, monotonic)
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
let _rndPool = Buffer.alloc(0),
|
|
5
|
-
_rndOff = 0;
|
|
6
|
-
|
|
7
|
-
function pooledRandom(n) {
|
|
8
|
-
if (_rndOff + n > _rndPool.length) {
|
|
9
|
-
_rndPool = randomBytes(256);
|
|
10
|
-
_rndOff = 0;
|
|
11
|
-
}
|
|
12
|
-
const out = Buffer.from(_rndPool.subarray(_rndOff, _rndOff + n));
|
|
13
|
-
_rndOff += n;
|
|
14
|
-
return out;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let _lastMs = 0n;
|
|
18
|
-
let _seq = 0;
|
|
19
|
-
|
|
20
|
-
/** UUIDv7 생성 (RFC 9562, 단조 증가 보장) */
|
|
21
|
-
export function uuidv7() {
|
|
22
|
-
let now = BigInt(Date.now());
|
|
23
|
-
if (now <= _lastMs) {
|
|
24
|
-
_seq++;
|
|
25
|
-
if (_seq > 0xfff) {
|
|
26
|
-
now = _lastMs + 1n;
|
|
27
|
-
_seq = 0;
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
_seq = 0;
|
|
31
|
-
}
|
|
32
|
-
_lastMs = now;
|
|
33
|
-
const buf = pooledRandom(16);
|
|
34
|
-
buf[0] = Number((now >> 40n) & 0xffn);
|
|
35
|
-
buf[1] = Number((now >> 32n) & 0xffn);
|
|
36
|
-
buf[2] = Number((now >> 24n) & 0xffn);
|
|
37
|
-
buf[3] = Number((now >> 16n) & 0xffn);
|
|
38
|
-
buf[4] = Number((now >> 8n) & 0xffn);
|
|
39
|
-
buf[5] = Number(now & 0xffn);
|
|
40
|
-
buf[6] = ((_seq >> 8) & 0x0f) | 0x70;
|
|
41
|
-
buf[7] = _seq & 0xff;
|
|
42
|
-
buf[8] = (buf[8] & 0x3f) | 0x80;
|
|
43
|
-
const h = buf.toString("hex");
|
|
44
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
45
|
-
}
|