@triflux/core 10.42.0 → 10.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/keyword-rules.json +139 -19
- package/hooks/pipeline-stop.mjs +1 -1
- package/hub/bridge.mjs +80 -1
- package/hub/cli-adapter-base.mjs +111 -21
- package/hub/codex-adapter.mjs +8 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +3 -1
- package/hub/intent.mjs +24 -28
- package/hub/lib/cto-env.mjs +46 -0
- package/hub/lib/memory-store.mjs +25 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/middleware/request-logger.mjs +4 -1
- package/hub/role-contract.mjs +314 -0
- package/hub/role-control-events.mjs +113 -0
- package/hub/router.mjs +383 -29
- package/hub/team/claude-daemon-control.mjs +28 -15
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/retry-state-machine.mjs +12 -3
- package/hub/team/synapse-http.mjs +149 -4
- package/hud/constants.mjs +21 -0
- package/hud/context-monitor.mjs +18 -21
- package/hud/hud-qos-status.mjs +1 -1
- package/hud/providers/codex-probe.mjs +216 -0
- package/hud/providers/codex.mjs +206 -29
- package/hud/renderers.mjs +6 -9
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +23 -160
- package/scripts/lib/codex-profile-config.mjs +31 -5
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/hub/log-retention.mjs +0 -726
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { normalizeRoleKey } from "../role-contract.mjs";
|
|
4
7
|
|
|
5
8
|
const HUB_DEFAULT_PORT = 27888;
|
|
6
9
|
// Same token file as hub/bridge.mjs (HUB_TOKEN_FILE). Kept inline rather than
|
|
@@ -21,6 +24,144 @@ function normalizeToken(raw) {
|
|
|
21
24
|
return token || null;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function opaqueId(prefix, value) {
|
|
28
|
+
return `${prefix}_${createHash("sha256")
|
|
29
|
+
.update(String(value || ""))
|
|
30
|
+
.digest("base64url")
|
|
31
|
+
.slice(0, 22)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedProjectId(value) {
|
|
35
|
+
try {
|
|
36
|
+
return normalizeRoleKey({ project_id: value, role_kind: "cto" }).project_id;
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveProjectId(cwd, meta, opts = {}) {
|
|
43
|
+
const explicit = meta?.project_id || process.env.TFX_PROJECT_ID;
|
|
44
|
+
if (explicit) return normalizedProjectId(explicit);
|
|
45
|
+
if (typeof opts.resolveProjectId === "function") {
|
|
46
|
+
return normalizedProjectId(opts.resolveProjectId(cwd));
|
|
47
|
+
}
|
|
48
|
+
let current = cwd;
|
|
49
|
+
while (current) {
|
|
50
|
+
const manifest = join(current, ".triflux", "project.json");
|
|
51
|
+
if (existsSync(manifest)) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
54
|
+
if (parsed?.schema_version !== "tfx.project.v1") return "";
|
|
55
|
+
return normalizedProjectId(parsed.project_id);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current) break;
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferSessionCli(meta, opts = {}) {
|
|
68
|
+
const explicit = String(meta?.cli || meta?.actor_cli || "")
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
if (explicit === "codex" || explicit === "claude") return explicit;
|
|
72
|
+
const entrypoint = String(opts.entrypoint ?? process.argv[1] ?? "");
|
|
73
|
+
if (entrypoint.includes("codex-session-hook")) return "codex";
|
|
74
|
+
if (entrypoint.includes("agy-session-hook")) return "other";
|
|
75
|
+
return "claude";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveTmuxSession(meta, opts = {}) {
|
|
79
|
+
const explicit =
|
|
80
|
+
meta?.tmux_session || meta?.session_name || process.env.TFX_TMUX_SESSION;
|
|
81
|
+
if (explicit) return String(explicit).trim();
|
|
82
|
+
if (!process.env.TMUX) return "";
|
|
83
|
+
try {
|
|
84
|
+
const run =
|
|
85
|
+
opts.tmuxSessionName ||
|
|
86
|
+
(() =>
|
|
87
|
+
execFileSync("tmux", ["display-message", "-p", "#{session_name}"], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
timeout: 200,
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
}));
|
|
92
|
+
return String(run() || "").trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildSynapseRegistrationMeta(meta, opts = {}) {
|
|
99
|
+
if (!meta || meta.sessionKind !== "interactive") return meta;
|
|
100
|
+
const sessionId = String(meta.sessionId || "").trim();
|
|
101
|
+
if (!sessionId) return meta;
|
|
102
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : process.cwd();
|
|
103
|
+
const cli = inferSessionCli(meta, opts);
|
|
104
|
+
const hostId = String(
|
|
105
|
+
meta.host_id ||
|
|
106
|
+
process.env.TFX_HOST_ID ||
|
|
107
|
+
opaqueId("hst", opts.hostname?.() || hostname()),
|
|
108
|
+
).trim();
|
|
109
|
+
const expiresAtMs = Number(opts.nowMs?.() ?? Date.now()) + 299000;
|
|
110
|
+
const transportLocators = [];
|
|
111
|
+
if (cli === "claude") {
|
|
112
|
+
transportLocators.push({
|
|
113
|
+
schema_version: "tfx.transport-locator.v1",
|
|
114
|
+
transport_id: opaqueId("trn", `claude-uds:${hostId}:${sessionId}`),
|
|
115
|
+
kind: "claude-uds",
|
|
116
|
+
host_id: hostId,
|
|
117
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
118
|
+
priority: 100,
|
|
119
|
+
expires_at_ms: expiresAtMs,
|
|
120
|
+
locator: {
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
bridge_id: String(
|
|
123
|
+
meta.bridge_id || process.env.TFX_BRIDGE_ID || "local-hub",
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const tmuxSession = resolveTmuxSession(meta, opts);
|
|
129
|
+
if (tmuxSession) {
|
|
130
|
+
const muxServerSource =
|
|
131
|
+
meta.mux_server_id ||
|
|
132
|
+
process.env.TFX_MUX_SERVER_ID ||
|
|
133
|
+
String(process.env.TMUX || "default").split(",", 1)[0];
|
|
134
|
+
transportLocators.push({
|
|
135
|
+
schema_version: "tfx.transport-locator.v1",
|
|
136
|
+
transport_id: opaqueId("trn", `tmux:${hostId}:${tmuxSession}`),
|
|
137
|
+
kind: "tmux",
|
|
138
|
+
host_id: hostId,
|
|
139
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
140
|
+
priority: cli === "codex" ? 100 : 50,
|
|
141
|
+
expires_at_ms: expiresAtMs,
|
|
142
|
+
locator: {
|
|
143
|
+
session_name: tmuxSession,
|
|
144
|
+
mux_server_id: opaqueId("mux", muxServerSource),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const directAgentId = `session:${sessionId}`;
|
|
149
|
+
return {
|
|
150
|
+
...meta,
|
|
151
|
+
agent_id:
|
|
152
|
+
meta.agent_id ||
|
|
153
|
+
(directAgentId.length <= 64 && /^[a-zA-Z0-9._:-]+$/u.test(directAgentId)
|
|
154
|
+
? directAgentId
|
|
155
|
+
: opaqueId("session", sessionId)),
|
|
156
|
+
cli,
|
|
157
|
+
project_id: resolveProjectId(cwd, meta, opts) || undefined,
|
|
158
|
+
session_id: meta.session_id || sessionId,
|
|
159
|
+
host_id: hostId,
|
|
160
|
+
transport_locators_json:
|
|
161
|
+
meta.transport_locators_json ?? JSON.stringify(transportLocators),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
24
165
|
// Mirrors hub/bridge.mjs:readHubToken — env override first, then the token file.
|
|
25
166
|
// Missing token → null (backward-compatible with token-less hubs).
|
|
26
167
|
function readHubToken() {
|
|
@@ -139,7 +280,11 @@ export function drainPendingSynapse(timeoutMs = 1000) {
|
|
|
139
280
|
}
|
|
140
281
|
|
|
141
282
|
export function registerSynapseSession(meta, opts = {}) {
|
|
142
|
-
return fireAndForgetSynapse(
|
|
283
|
+
return fireAndForgetSynapse(
|
|
284
|
+
"/synapse/register",
|
|
285
|
+
buildSynapseRegistrationMeta(meta, opts),
|
|
286
|
+
opts,
|
|
287
|
+
);
|
|
143
288
|
}
|
|
144
289
|
|
|
145
290
|
export function heartbeatSynapseSession(
|
package/hud/constants.mjs
CHANGED
|
@@ -50,6 +50,13 @@ export const CONTEXT_MONITOR_CACHE_PATH = join(
|
|
|
50
50
|
"tfx-hub",
|
|
51
51
|
"context-monitor.json",
|
|
52
52
|
);
|
|
53
|
+
export const HUB_REQUEST_MONITOR_CACHE_PATH = join(
|
|
54
|
+
homedir(),
|
|
55
|
+
".claude",
|
|
56
|
+
"cache",
|
|
57
|
+
"tfx-hub",
|
|
58
|
+
"hub-request-context-monitor.json",
|
|
59
|
+
);
|
|
53
60
|
export const CONTEXT_MONITOR_LEGACY_PATH = join(
|
|
54
61
|
homedir(),
|
|
55
62
|
".omc",
|
|
@@ -122,6 +129,20 @@ export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
122
129
|
export const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
123
130
|
|
|
124
131
|
export const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
|
|
132
|
+
export const CODEX_PROBE_STATE_PATH = join(
|
|
133
|
+
homedir(),
|
|
134
|
+
".claude",
|
|
135
|
+
"cache",
|
|
136
|
+
"codex-probe-state.json",
|
|
137
|
+
);
|
|
138
|
+
export const CODEX_BROKER_AUTH_CACHE_DIR = join(
|
|
139
|
+
homedir(),
|
|
140
|
+
".claude",
|
|
141
|
+
"cache",
|
|
142
|
+
"tfx-hub",
|
|
143
|
+
);
|
|
144
|
+
export const CODEX_PROBE_TTL_MS = 5 * 60 * 1000;
|
|
145
|
+
export const CODEX_PROBE_TIMEOUT_MS = 10_000;
|
|
125
146
|
export const CODEX_QUOTA_CACHE_PATH = join(
|
|
126
147
|
homedir(),
|
|
127
148
|
".claude",
|
package/hud/context-monitor.mjs
CHANGED
|
@@ -283,26 +283,23 @@ export function deriveContextLimit(stdin) {
|
|
|
283
283
|
|
|
284
284
|
export function buildContextUsageView(stdin, snapshot = null) {
|
|
285
285
|
const stdinUsage = getStdinContextUsage(stdin);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
? Math.max(1, stdinLimit)
|
|
299
|
-
: modelId
|
|
300
|
-
? Math.max(1, modelHintLimit)
|
|
301
|
-
: Math.max(1, monitorLimit || modelHintLimit);
|
|
286
|
+
if (!stdinUsage) {
|
|
287
|
+
return {
|
|
288
|
+
usedTokens: 0,
|
|
289
|
+
limitTokens: 0,
|
|
290
|
+
percent: 0,
|
|
291
|
+
display: "--",
|
|
292
|
+
warningLevel: "ok",
|
|
293
|
+
warningMessage: "",
|
|
294
|
+
warningTag: "",
|
|
295
|
+
source: "none",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
302
298
|
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
299
|
+
const modelId = stdin?.model?.id ?? stdin?.model;
|
|
300
|
+
const limitTokens = Math.max(1, stdinUsage.limitTokens);
|
|
301
|
+
const usedTokens = stdinUsage.usedTokens;
|
|
302
|
+
const percent = clampPercent((usedTokens / limitTokens) * 100);
|
|
306
303
|
|
|
307
304
|
const warning = classifyContextThreshold(percent);
|
|
308
305
|
const showInfoOnlyStatus = !(
|
|
@@ -324,12 +321,12 @@ export function buildContextUsageView(stdin, snapshot = null) {
|
|
|
324
321
|
: warning.level === "info"
|
|
325
322
|
? "ℹ 절반 이상 사용"
|
|
326
323
|
: "",
|
|
327
|
-
source: stdinUsage
|
|
324
|
+
source: stdinUsage.source,
|
|
328
325
|
};
|
|
329
326
|
}
|
|
330
327
|
|
|
331
328
|
export function createContextMonitor(options = {}) {
|
|
332
|
-
// Default to a 1M context window (Opus 4.x [1M] / Codex
|
|
329
|
+
// Default to a 1M context window (Opus 4.x [1M] / Codex GPT-5.6 family runs on
|
|
333
330
|
// a 1M window) when no explicit limit is supplied, so the cached percent the
|
|
334
331
|
// Stop hook reads is not inflated against a 200K assumption — assuming 200K
|
|
335
332
|
// there false-positives at ~15% real usage and blocks legitimate stops.
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// codex app-server account/rateLimits/read 능동 프로브 + 정규화.
|
|
2
|
+
// 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
|
|
3
|
+
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
function readJwtExpSec(token) {
|
|
17
|
+
try {
|
|
18
|
+
const payload = JSON.parse(
|
|
19
|
+
Buffer.from(token.split(".")[1], "base64url").toString("utf-8"),
|
|
20
|
+
);
|
|
21
|
+
return Number(payload.exp) || 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJsonFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EXP_MARGIN_SEC = 600;
|
|
36
|
+
|
|
37
|
+
function accessTokenUsable(auth, nowMs) {
|
|
38
|
+
const token = auth?.tokens?.access_token;
|
|
39
|
+
if (!token) return false;
|
|
40
|
+
const exp = readJwtExpSec(token);
|
|
41
|
+
return exp > Math.floor(nowMs / 1000) + EXP_MARGIN_SEC;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recordProbe(stateFilePath, key, nowMs) {
|
|
45
|
+
const state = readJsonFile(stateFilePath) || {};
|
|
46
|
+
state[key] = nowMs;
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(stateFilePath), { recursive: true });
|
|
49
|
+
writeFileSync(stateFilePath, JSON.stringify(state));
|
|
50
|
+
} catch {
|
|
51
|
+
// 상태 기록 실패는 무시 — 다음 사이클에 재시도
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listProbeTargets({
|
|
56
|
+
codexAuthPath,
|
|
57
|
+
brokerCacheDir,
|
|
58
|
+
stateFilePath,
|
|
59
|
+
ttlMs,
|
|
60
|
+
nowMs,
|
|
61
|
+
}) {
|
|
62
|
+
const state = readJsonFile(stateFilePath) || {};
|
|
63
|
+
const due = (key) =>
|
|
64
|
+
!(Number(state[key]) > 0 && nowMs - Number(state[key]) < ttlMs);
|
|
65
|
+
const targets = [];
|
|
66
|
+
|
|
67
|
+
const currentAuth = readJsonFile(codexAuthPath);
|
|
68
|
+
if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
|
|
69
|
+
targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (brokerCacheDir && existsSync(brokerCacheDir)) {
|
|
73
|
+
let files = [];
|
|
74
|
+
try {
|
|
75
|
+
files = readdirSync(brokerCacheDir).filter((file) =>
|
|
76
|
+
/^codex-auth-.+\.json$/.test(file),
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
files = [];
|
|
80
|
+
}
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const key = file.replace(/^codex-auth-/, "").replace(/\.json$/, "");
|
|
83
|
+
if (!due(key)) continue;
|
|
84
|
+
const auth = readJsonFile(join(brokerCacheDir, file));
|
|
85
|
+
if (!auth || !accessTokenUsable(auth, nowMs)) continue;
|
|
86
|
+
const home = mkdtempSync(join(tmpdir(), `tfx-probe-home-${key}-`));
|
|
87
|
+
writeFileSync(join(home, "auth.json"), JSON.stringify(auth));
|
|
88
|
+
targets.push({ key, codexHome: home });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return targets;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toSnakeBucket(bucket) {
|
|
95
|
+
if (!bucket) return null;
|
|
96
|
+
return {
|
|
97
|
+
used_percent: bucket.usedPercent ?? null,
|
|
98
|
+
window_minutes: bucket.windowDurationMins ?? null,
|
|
99
|
+
resets_at: bucket.resetsAt ?? null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toSnapshot(entry, nowIso) {
|
|
104
|
+
return {
|
|
105
|
+
limitId: entry.limitId,
|
|
106
|
+
limitName: entry.limitName ?? null,
|
|
107
|
+
primary: toSnakeBucket(entry.primary),
|
|
108
|
+
secondary: toSnakeBucket(entry.secondary),
|
|
109
|
+
credits: entry.credits ?? null,
|
|
110
|
+
tokens: null,
|
|
111
|
+
contextWindow: null,
|
|
112
|
+
timestamp: nowIso,
|
|
113
|
+
probe: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizeAppServerRateLimits(result, nowIso) {
|
|
118
|
+
if (!result) return [];
|
|
119
|
+
const byId = result.rateLimitsByLimitId;
|
|
120
|
+
if (byId && typeof byId === "object" && Object.keys(byId).length > 0) {
|
|
121
|
+
return Object.values(byId)
|
|
122
|
+
.filter((e) => e?.limitId)
|
|
123
|
+
.map((e) => toSnapshot(e, nowIso));
|
|
124
|
+
}
|
|
125
|
+
if (result.rateLimits?.limitId) {
|
|
126
|
+
return [toSnapshot(result.rateLimits, nowIso)];
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function probeRateLimitsViaAppServer({
|
|
132
|
+
codexHome = null,
|
|
133
|
+
spawnBin = "codex",
|
|
134
|
+
spawnArgs = ["app-server"],
|
|
135
|
+
timeoutMs = 10000,
|
|
136
|
+
now = new Date(),
|
|
137
|
+
} = {}) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
let child;
|
|
140
|
+
try {
|
|
141
|
+
child = spawn(spawnBin, spawnArgs, {
|
|
142
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
143
|
+
env: codexHome
|
|
144
|
+
? { ...process.env, CODEX_HOME: codexHome }
|
|
145
|
+
: process.env,
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
resolve(null);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let settled = false;
|
|
152
|
+
let buf = "";
|
|
153
|
+
const finish = (value) => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
try {
|
|
158
|
+
child.kill();
|
|
159
|
+
} catch {
|
|
160
|
+
// 이미 종료
|
|
161
|
+
}
|
|
162
|
+
resolve(value);
|
|
163
|
+
};
|
|
164
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
165
|
+
const send = (obj) => {
|
|
166
|
+
try {
|
|
167
|
+
child.stdin.write(`${JSON.stringify(obj)}\n`);
|
|
168
|
+
} catch {
|
|
169
|
+
finish(null);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
child.on("error", () => finish(null));
|
|
173
|
+
child.on("exit", () => finish(null));
|
|
174
|
+
child.stdout.on("data", (data) => {
|
|
175
|
+
buf += data.toString();
|
|
176
|
+
let idx = buf.indexOf("\n");
|
|
177
|
+
while (idx >= 0) {
|
|
178
|
+
const line = buf.slice(0, idx);
|
|
179
|
+
buf = buf.slice(idx + 1);
|
|
180
|
+
idx = buf.indexOf("\n");
|
|
181
|
+
if (!line.trim()) continue;
|
|
182
|
+
let msg;
|
|
183
|
+
try {
|
|
184
|
+
msg = JSON.parse(line);
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (msg.id === 1 && msg.result) {
|
|
189
|
+
send({
|
|
190
|
+
jsonrpc: "2.0",
|
|
191
|
+
id: 2,
|
|
192
|
+
method: "account/rateLimits/read",
|
|
193
|
+
params: {},
|
|
194
|
+
});
|
|
195
|
+
} else if (msg.id === 2) {
|
|
196
|
+
if (msg.error) finish(null);
|
|
197
|
+
else {
|
|
198
|
+
finish(normalizeAppServerRateLimits(msg.result, now.toISOString()));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
send({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
id: 1,
|
|
206
|
+
method: "initialize",
|
|
207
|
+
params: {
|
|
208
|
+
clientInfo: {
|
|
209
|
+
name: "triflux-hud-probe",
|
|
210
|
+
title: "triflux HUD quota probe",
|
|
211
|
+
version: "1.0.0",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|