@triflux/core 10.51.0 → 10.53.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/hub/bridge.mjs +69 -1197
- package/hub/cli-adapter-base.mjs +4 -104
- package/hub/codex-adapter.mjs +7 -116
- package/hub/index.mjs +3 -20
- package/hub/lib/process-utils.mjs +1 -33
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/nativeProxy.mjs +759 -0
- package/hub/team/session-context.mjs +1 -1
- package/hud/constants.mjs +9 -22
- package/hud/hud-qos-status.mjs +16 -19
- package/hud/providers/codex-probe.mjs +2 -31
- package/hud/providers/codex.mjs +16 -58
- package/hud/providers/gemini.mjs +20 -0
- package/hud/renderers.mjs +101 -77
- package/hud/utils.mjs +9 -13
- package/package.json +3 -6
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +42 -101
- package/scripts/lib/session-presence.mjs +1 -99
- package/hub/account-broker.mjs +0 -1221
- 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/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/state-snapshot.mjs +0 -293
- 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 -312
- package/hub/router.mjs +0 -2026
- 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/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -15,61 +15,6 @@ import {
|
|
|
15
15
|
import { IS_WINDOWS, killProcess } from "./platform.mjs";
|
|
16
16
|
import { createInterventionLadder } from "./team/intervention.mjs";
|
|
17
17
|
|
|
18
|
-
// ── Quota retry-after 파싱 ──────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 에러 텍스트에서 retry-after 시간을 동적 파싱.
|
|
24
|
-
* "try again at 2026-04-11T10:00:00Z", "retry after 3600 seconds",
|
|
25
|
-
* "resets in 7 days", "wait 5 hours" 등을 감지.
|
|
26
|
-
*/
|
|
27
|
-
export function parseRetryAfterMs(text, provider) {
|
|
28
|
-
// 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
|
|
29
|
-
const isoMatch = text.match(
|
|
30
|
-
/(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
|
|
31
|
-
);
|
|
32
|
-
if (isoMatch) {
|
|
33
|
-
const target = new Date(isoMatch[1]).getTime();
|
|
34
|
-
if (target > Date.now()) return target - Date.now();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2. Duration: "N seconds/minutes/hours/days"
|
|
38
|
-
const durMatch = text.match(
|
|
39
|
-
/(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
|
|
40
|
-
);
|
|
41
|
-
if (durMatch) {
|
|
42
|
-
const n = Number(durMatch[1]);
|
|
43
|
-
const unit = durMatch[2].toLowerCase();
|
|
44
|
-
const multipliers = {
|
|
45
|
-
second: 1000,
|
|
46
|
-
minute: 60_000,
|
|
47
|
-
hour: 3600_000,
|
|
48
|
-
day: 86400_000,
|
|
49
|
-
week: 604800_000,
|
|
50
|
-
};
|
|
51
|
-
return n * (multipliers[unit] || 3600_000);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 3. "7 days" / "24 hours" 단독 (context 없이)
|
|
55
|
-
const standaloneMatch = text.match(/(\d+)\s*(day|hour|week|minute)/i);
|
|
56
|
-
if (standaloneMatch) {
|
|
57
|
-
const n = Number(standaloneMatch[1]);
|
|
58
|
-
const unit = standaloneMatch[2].toLowerCase();
|
|
59
|
-
const multipliers = {
|
|
60
|
-
minute: 60_000,
|
|
61
|
-
hour: 3600_000,
|
|
62
|
-
day: 86400_000,
|
|
63
|
-
week: 604800_000,
|
|
64
|
-
};
|
|
65
|
-
const parsed = n * (multipliers[unit] || 3600_000);
|
|
66
|
-
if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 4. fallback: provider 기본값
|
|
70
|
-
return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
18
|
// ── Shell utilities ─────────────────────────────────────────────
|
|
74
19
|
|
|
75
20
|
export function normalizePathForShell(value) {
|
|
@@ -195,47 +140,28 @@ export function appendWarnings(stderr, warnings = []) {
|
|
|
195
140
|
return [stderr, text].filter(Boolean).join("\n");
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
// ──
|
|
143
|
+
// ── Attempt execution ───────────────────────────────────────────
|
|
199
144
|
|
|
200
145
|
/**
|
|
201
|
-
*
|
|
202
|
-
* breaking instead of a global breaker.
|
|
146
|
+
* preflight 뒤 buildAttemptsFn 이 만든 시도를 순서대로 실행한다.
|
|
203
147
|
*
|
|
204
148
|
* @param {object} params
|
|
205
|
-
* @param {string} params.provider — 'codex' | 'gemini'
|
|
206
149
|
* @param {(prompt: string, workdir: string, preflight: object, attempt: object) => Promise<object>} params.runFn
|
|
207
150
|
* @param {(opts: object) => Promise<object>} params.preflightFn
|
|
208
151
|
* @param {(opts: object, preflight: object) => object[]} params.buildAttemptsFn
|
|
209
152
|
* @param {object} params.opts — caller-supplied execute options
|
|
210
153
|
* @returns {Promise<object>} createResult-shaped result
|
|
211
154
|
*/
|
|
212
|
-
export async function
|
|
213
|
-
provider,
|
|
155
|
+
export async function executeWithAttempts({
|
|
214
156
|
runFn,
|
|
215
157
|
preflightFn,
|
|
216
158
|
buildAttemptsFn,
|
|
217
159
|
opts = {},
|
|
218
160
|
}) {
|
|
219
|
-
// late-import to avoid circular dependency at module load time
|
|
220
|
-
const brokerMod = await import("./account-broker.mjs");
|
|
221
161
|
const { withRetry } = await import("./workers/worker-utils.mjs");
|
|
222
162
|
|
|
223
|
-
// access broker as live binding property (not destructured) so reloadBroker() propagates
|
|
224
|
-
const hasBroker = brokerMod.broker != null;
|
|
225
|
-
// Empty broker (TFX_DISABLE_ACCOUNT_BROKER=1 or zero accounts) must not
|
|
226
|
-
// gate execution. lease() is null because there are no accounts to lease,
|
|
227
|
-
// not because all accounts are busy/cooldown/circuit. Treat it like
|
|
228
|
-
// hasBroker=false and fall through to the default auth path.
|
|
229
|
-
const brokerDisabled = hasBroker && brokerMod.broker.isDisabled === true;
|
|
230
|
-
const effectiveBroker = hasBroker && !brokerDisabled;
|
|
231
|
-
const lease = effectiveBroker ? brokerMod.broker.lease({ provider }) : null;
|
|
232
|
-
if (effectiveBroker && !lease) {
|
|
233
|
-
return createResult(false, { fellBack: true, failureMode: "circuit_open" });
|
|
234
|
-
}
|
|
235
|
-
|
|
236
163
|
const preflight = await preflightFn(opts);
|
|
237
164
|
if (!preflight.ok) {
|
|
238
|
-
if (lease) brokerMod.broker.release(lease.id, { ok: false });
|
|
239
165
|
return createResult(false, {
|
|
240
166
|
stderr: appendWarnings("", preflight.warnings),
|
|
241
167
|
fellBack: opts.fallbackToClaude !== false,
|
|
@@ -250,15 +176,11 @@ export async function executeWithCircuitBroker({
|
|
|
250
176
|
try {
|
|
251
177
|
lastResult = await withRetry(
|
|
252
178
|
async () => {
|
|
253
|
-
// PRD A1: lease 메타데이터를 runFn 에 전달해서 adapter 가 실제 spawn 시
|
|
254
|
-
// 해당 account 의 authFile/env/profile 을 적용할 수 있게 한다. lease=null
|
|
255
|
-
// 이면 adapter 는 default 경로 (단일 ~/.codex/auth.json) 로 동작한다.
|
|
256
179
|
const result = await runFn(
|
|
257
180
|
opts.prompt || "",
|
|
258
181
|
opts.workdir || process.cwd(),
|
|
259
182
|
preflight,
|
|
260
183
|
attempts[attemptIndex],
|
|
261
|
-
lease,
|
|
262
184
|
);
|
|
263
185
|
const current = {
|
|
264
186
|
...result,
|
|
@@ -286,26 +208,7 @@ export async function executeWithCircuitBroker({
|
|
|
286
208
|
createResult(false, { stderr: String(error?.message || error) });
|
|
287
209
|
}
|
|
288
210
|
|
|
289
|
-
if (lastResult.ok)
|
|
290
|
-
if (lease) brokerMod.broker.release(lease.id, { ok: true });
|
|
291
|
-
return lastResult;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (lease) {
|
|
295
|
-
if (lastResult.failureMode === "rate_limited") {
|
|
296
|
-
const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
|
|
297
|
-
const coolMs = parseRetryAfterMs(text, provider);
|
|
298
|
-
brokerMod.broker.markRateLimited(lease.id, coolMs);
|
|
299
|
-
brokerMod.broker.emit("cooldown", {
|
|
300
|
-
id: lease.id,
|
|
301
|
-
provider,
|
|
302
|
-
coolMs,
|
|
303
|
-
reason: "quota_exhausted",
|
|
304
|
-
});
|
|
305
|
-
} else {
|
|
306
|
-
brokerMod.broker.release(lease.id, { ok: false });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
211
|
+
if (lastResult.ok) return lastResult;
|
|
309
212
|
return {
|
|
310
213
|
...lastResult,
|
|
311
214
|
retried: attempts.length > 1,
|
|
@@ -374,13 +277,10 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
|
|
|
374
277
|
let child;
|
|
375
278
|
|
|
376
279
|
try {
|
|
377
|
-
// PRD A1: opts.spawnEnv 가 있으면 그 env 로 spawn (lease 의 authFile/env 적용).
|
|
378
|
-
// undefined 면 spawn 의 default 동작 (부모 process env inherit) 유지.
|
|
379
280
|
child = spawnProcess(command, {
|
|
380
281
|
cwd: workdir,
|
|
381
282
|
shell: true,
|
|
382
283
|
windowsHide: true,
|
|
383
|
-
env: opts.spawnEnv,
|
|
384
284
|
});
|
|
385
285
|
} catch (error) {
|
|
386
286
|
return createResult(false, {
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
5
5
|
import {
|
|
6
6
|
buildExecCommand,
|
|
7
|
-
|
|
8
|
-
executeWithCircuitBroker,
|
|
7
|
+
executeWithAttempts,
|
|
9
8
|
normalizePathForShell,
|
|
10
9
|
runProcess,
|
|
11
10
|
shellQuote,
|
|
@@ -113,7 +112,7 @@ export function buildLaunchScript(opts = {}) {
|
|
|
113
112
|
|
|
114
113
|
// CTO boundary: this adapter assembles Codex prompts outside
|
|
115
114
|
// scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
|
|
116
|
-
// launcher
|
|
115
|
+
// launcher workdir contracts have focused coverage.
|
|
117
116
|
export function buildExecArgs(opts = {}) {
|
|
118
117
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
119
118
|
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
@@ -153,7 +152,7 @@ export function buildExecArgs(opts = {}) {
|
|
|
153
152
|
|
|
154
153
|
// ── Codex execution ─────────────────────────────────────────────
|
|
155
154
|
|
|
156
|
-
async function runCodex(prompt, workdir, preflight, attempt
|
|
155
|
+
async function runCodex(prompt, workdir, preflight, attempt) {
|
|
157
156
|
const dir = join(tmpdir(), "triflux-codex-exec");
|
|
158
157
|
mkdirSync(dir, { recursive: true });
|
|
159
158
|
const resultFile = join(
|
|
@@ -162,7 +161,7 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
|
162
161
|
);
|
|
163
162
|
const command = commandWithOverrides(
|
|
164
163
|
buildExecCommand(prompt, resultFile, {
|
|
165
|
-
profile:
|
|
164
|
+
profile: attempt.profile,
|
|
166
165
|
skipGitRepoCheck: true,
|
|
167
166
|
sandboxBypass: attempt.forceBypass,
|
|
168
167
|
}),
|
|
@@ -170,125 +169,17 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
|
170
169
|
preflight.codexPath,
|
|
171
170
|
buildOverrides(attempt.requested, attempt.excluded),
|
|
172
171
|
);
|
|
173
|
-
// PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
|
|
174
|
-
// 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
|
|
175
|
-
// account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
|
|
176
|
-
// 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
|
|
177
|
-
const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
|
|
178
|
-
const authCheck = validateLeaseAuth(lease, spawnEnv);
|
|
179
|
-
if (!authCheck.ok) {
|
|
180
|
-
return createResult(false, {
|
|
181
|
-
stderr: authCheck.error,
|
|
182
|
-
failureMode: "auth_sync",
|
|
183
|
-
skipCircuit: true,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
172
|
return runProcess(command, workdir, attempt.timeout, {
|
|
187
173
|
resultFile,
|
|
188
174
|
inferStallMode,
|
|
189
|
-
spawnEnv,
|
|
190
175
|
cli: "codex",
|
|
191
|
-
codexHome: spawnEnv?.CODEX_HOME,
|
|
192
176
|
});
|
|
193
177
|
}
|
|
194
178
|
|
|
195
|
-
function buildLeaseSpawnEnv(lease) {
|
|
196
|
-
const extra = {};
|
|
197
|
-
if (lease.authFile) {
|
|
198
|
-
// lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
|
|
199
|
-
// codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
|
|
200
|
-
// 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
|
|
201
|
-
// 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
|
|
202
|
-
// 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
|
|
203
|
-
// 실제로 있을 때만 적용한다 (false-positive 회피).
|
|
204
|
-
try {
|
|
205
|
-
const dir = dirnameOf(lease.authFile);
|
|
206
|
-
if (dir) extra.CODEX_HOME = dir;
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
if (lease.env && typeof lease.env === "object") {
|
|
210
|
-
Object.assign(extra, lease.env);
|
|
211
|
-
}
|
|
212
|
-
return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function validateLeaseAuth(lease, spawnEnv) {
|
|
216
|
-
if (!lease?.authFile) return { ok: true };
|
|
217
|
-
if (basename(lease.authFile) !== "auth.json") {
|
|
218
|
-
return {
|
|
219
|
-
ok: false,
|
|
220
|
-
error: `lease authFile must resolve to auth.json for account ${lease.id}`,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (!existsSync(lease.authFile)) {
|
|
224
|
-
return {
|
|
225
|
-
ok: false,
|
|
226
|
-
error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
|
|
230
|
-
return {
|
|
231
|
-
ok: false,
|
|
232
|
-
// This is currently expected to match because buildLeaseSpawnEnv derives
|
|
233
|
-
// CODEX_HOME from authFile. Keep the guard so future lease.env overrides
|
|
234
|
-
// cannot silently point Codex at a different account home.
|
|
235
|
-
error: `CODEX_HOME mismatch for account ${lease.id}`,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
|
|
240
|
-
const accountId =
|
|
241
|
-
parsed?.tokens?.account_id ??
|
|
242
|
-
parsed?.account_id ??
|
|
243
|
-
parsed?.accountId ??
|
|
244
|
-
null;
|
|
245
|
-
if (!accountId) {
|
|
246
|
-
return {
|
|
247
|
-
ok: false,
|
|
248
|
-
error: `lease authFile has no account_id for account ${lease.id}`,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
if (accountId !== lease.id) {
|
|
252
|
-
return {
|
|
253
|
-
ok: false,
|
|
254
|
-
error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
return {
|
|
259
|
-
ok: false,
|
|
260
|
-
error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return { ok: true };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function dirnameOf(filePath) {
|
|
267
|
-
if (typeof filePath !== "string" || !filePath) return null;
|
|
268
|
-
const lastSep = Math.max(
|
|
269
|
-
filePath.lastIndexOf("/"),
|
|
270
|
-
filePath.lastIndexOf("\\"),
|
|
271
|
-
);
|
|
272
|
-
if (lastSep < 0) return null;
|
|
273
|
-
return filePath.slice(0, lastSep);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
179
|
// ── Public API ──────────────────────────────────────────────────
|
|
277
180
|
|
|
278
|
-
export async function getCircuitState() {
|
|
279
|
-
const brokerMod = await import("./account-broker.mjs");
|
|
280
|
-
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
281
|
-
const snap = brokerMod.broker
|
|
282
|
-
.snapshot()
|
|
283
|
-
.filter((a) => a.provider === "codex");
|
|
284
|
-
return snap.length
|
|
285
|
-
? { state: snap[0].circuitState, accounts: snap }
|
|
286
|
-
: { state: "closed", failures: [] };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
181
|
export function execute(opts = {}) {
|
|
290
|
-
return
|
|
291
|
-
provider: "codex",
|
|
182
|
+
return executeWithAttempts({
|
|
292
183
|
runFn: runCodex,
|
|
293
184
|
preflightFn: (o) =>
|
|
294
185
|
runPreflight({
|
package/hub/index.mjs
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
// @triflux/core — barrel export (auto-generated by pack.mjs)
|
|
2
2
|
// Adapters
|
|
3
3
|
export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
|
|
4
|
-
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript
|
|
4
|
+
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript } from './codex-adapter.mjs';
|
|
5
5
|
export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
export { createRouter } from './router.mjs';
|
|
9
|
-
|
|
10
|
-
// State & Paths & Platform
|
|
11
|
-
export * from './paths.mjs';
|
|
7
|
+
// Platform
|
|
12
8
|
export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, killProcess, pipePath } from './platform.mjs';
|
|
13
9
|
|
|
14
10
|
// Core services
|
|
15
|
-
export {
|
|
16
|
-
export { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
17
|
-
export { getHubUrl, getHubPipePath, requestJson, post, connectPipe, parseArgs, parseJsonSafe } from './bridge.mjs';
|
|
18
|
-
|
|
19
|
-
// Reflexion (adaptive error learning)
|
|
20
|
-
export { normalizeError, adaptiveRuleFromError } from './reflexion.mjs';
|
|
21
|
-
|
|
22
|
-
// Neural Memory (Lake 1.5)
|
|
23
|
-
export { createAdaptiveEngine } from './adaptive.mjs';
|
|
24
|
-
export { createAdaptiveMemory } from './adaptive-memory.mjs';
|
|
25
|
-
export { createDiagnosticPipeline } from './adaptive-diagnostic.mjs';
|
|
26
|
-
export { createAdaptiveInjector } from './adaptive-inject.mjs';
|
|
27
|
-
export { buildAdaptiveFingerprint, createAdaptiveFingerprintService } from './session-fingerprint.mjs';
|
|
28
|
-
export { broker as accountBroker } from './account-broker.mjs';
|
|
11
|
+
export { parseArgs, parseJsonSafe } from './bridge.mjs';
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
unlinkSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import {
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { IS_WINDOWS, killProcess } from "../platform.mjs";
|
|
15
15
|
|
|
@@ -387,23 +387,6 @@ function getProcessSnapshot({
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function addHubPid(protectedPids) {
|
|
391
|
-
try {
|
|
392
|
-
const hubPidPath = join(
|
|
393
|
-
homedir(),
|
|
394
|
-
".claude",
|
|
395
|
-
"cache",
|
|
396
|
-
"tfx-hub",
|
|
397
|
-
"hub.pid",
|
|
398
|
-
);
|
|
399
|
-
if (existsSync(hubPidPath)) {
|
|
400
|
-
const hubInfo = JSON.parse(readFileSync(hubPidPath, "utf8"));
|
|
401
|
-
const hubPid = normalizePid(hubInfo?.pid);
|
|
402
|
-
if (hubPid) protectedPids.add(hubPid);
|
|
403
|
-
}
|
|
404
|
-
} catch {}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
390
|
function getProtectedPids({
|
|
408
391
|
protectedPids,
|
|
409
392
|
isWindows = IS_WINDOWS,
|
|
@@ -415,7 +398,6 @@ function getProtectedPids({
|
|
|
415
398
|
if (Number.isInteger(process.ppid) && process.ppid > 0) {
|
|
416
399
|
result.add(process.ppid);
|
|
417
400
|
}
|
|
418
|
-
addHubPid(result);
|
|
419
401
|
if (!includeAncestorScan) return result;
|
|
420
402
|
|
|
421
403
|
const snapshot = getProcessSnapshot({ isWindows, spawnSyncFn });
|
|
@@ -845,22 +827,8 @@ export function cleanupStaleFsmonitorDaemons({
|
|
|
845
827
|
function cleanupOrphansUnix() {
|
|
846
828
|
const myPid = process.pid;
|
|
847
829
|
|
|
848
|
-
// Hub PID 보호
|
|
849
830
|
const protectedPids = new Set();
|
|
850
831
|
protectedPids.add(myPid);
|
|
851
|
-
try {
|
|
852
|
-
const hubPidPath = join(
|
|
853
|
-
homedir(),
|
|
854
|
-
".claude",
|
|
855
|
-
"cache",
|
|
856
|
-
"tfx-hub",
|
|
857
|
-
"hub.pid",
|
|
858
|
-
);
|
|
859
|
-
if (existsSync(hubPidPath)) {
|
|
860
|
-
const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
|
|
861
|
-
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
862
|
-
}
|
|
863
|
-
} catch {}
|
|
864
832
|
|
|
865
833
|
// 현재 프로세스의 조상 트리 보호
|
|
866
834
|
try {
|
|
@@ -1775,8 +1775,7 @@ export async function dispatchClaudeDaemonJob({
|
|
|
1775
1775
|
/**
|
|
1776
1776
|
* dispatchClaudeDaemonJob 의 대칭 정리 경로.
|
|
1777
1777
|
* removeProjection + killDaemonJob (+ optional removeClaudeJobState) 를
|
|
1778
|
-
* 모두 시도하며, 각 스텝 오류는
|
|
1779
|
-
* 동일하게 .catch 로 삼킨다.
|
|
1778
|
+
* 모두 시도하며, 각 스텝 오류는 .catch 로 삼킨다.
|
|
1780
1779
|
*/
|
|
1781
1780
|
export async function teardownClaudeDaemonJob({
|
|
1782
1781
|
controlSock,
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// 실제 bridge ID 가 없으면 만들어 내지 않는다. 추정 ID 는 붙을 수 있는 세션처럼 보이게 한다.
|
|
5
|
+
export function normalizeClaudeBridgeSessionId(bridgeSessionId) {
|
|
5
6
|
const value = String(bridgeSessionId || "").trim();
|
|
6
|
-
if (value.startsWith("session_")) return value;
|
|
7
7
|
if (value.startsWith("cse_")) return `session_${value.slice(4)}`;
|
|
8
|
-
|
|
9
|
-
return `session_${short}`;
|
|
8
|
+
return value;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export function buildClaudeSessionProjection({
|
|
@@ -30,6 +29,7 @@ export function buildClaudeSessionProjection({
|
|
|
30
29
|
if (!cwd) throw new Error("cwd is required");
|
|
31
30
|
if (!name) throw new Error("name is required");
|
|
32
31
|
if (!agent) throw new Error("agent is required");
|
|
32
|
+
const bridge = normalizeClaudeBridgeSessionId(bridgeSessionId);
|
|
33
33
|
return {
|
|
34
34
|
pid,
|
|
35
35
|
sessionId,
|
|
@@ -45,7 +45,7 @@ export function buildClaudeSessionProjection({
|
|
|
45
45
|
jobId: short,
|
|
46
46
|
status,
|
|
47
47
|
updatedAt,
|
|
48
|
-
bridgeSessionId:
|
|
48
|
+
...(bridge ? { bridgeSessionId: bridge } : {}),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -58,74 +58,6 @@ export async function writeClaudeSessionProjection(sessionsDir, projection) {
|
|
|
58
58
|
return sessionPath;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export async function updateClaudeSessionProjection(sessionPath, patch) {
|
|
62
|
-
const parsed = JSON.parse(await fs.readFile(sessionPath, "utf8"));
|
|
63
|
-
const next = {
|
|
64
|
-
...parsed,
|
|
65
|
-
...patch,
|
|
66
|
-
updatedAt: patch.updatedAt || Date.now(),
|
|
67
|
-
};
|
|
68
|
-
await fs.writeFile(sessionPath, `${JSON.stringify(next)}\n`, "utf8");
|
|
69
|
-
return next;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function projectionSessionId(projection) {
|
|
73
|
-
return String(
|
|
74
|
-
projection?.sessionId ?? projection?.session_id ?? projection?.id ?? "",
|
|
75
|
-
).trim();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function findClaudeSessionProjectionBySessionId(
|
|
79
|
-
sessionsDir,
|
|
80
|
-
sessionId,
|
|
81
|
-
) {
|
|
82
|
-
const expected = String(sessionId || "").trim();
|
|
83
|
-
if (!sessionsDir || !expected) return null;
|
|
84
|
-
let entries;
|
|
85
|
-
try {
|
|
86
|
-
entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (error?.code === "ENOENT") return null;
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
94
|
-
const filePath = path.join(sessionsDir, entry.name);
|
|
95
|
-
try {
|
|
96
|
-
const projection = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
97
|
-
if (projectionSessionId(projection) === expected) {
|
|
98
|
-
return { path: filePath, projection };
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error instanceof SyntaxError) continue;
|
|
102
|
-
if (error?.code === "ENOENT") continue;
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export async function refreshClaudeSessionProjectionCwd({
|
|
110
|
-
sessionsDir,
|
|
111
|
-
sessionId,
|
|
112
|
-
cwd,
|
|
113
|
-
updatedAt = Date.now(),
|
|
114
|
-
} = {}) {
|
|
115
|
-
const nextCwd = String(cwd || "").trim();
|
|
116
|
-
if (!nextCwd) return { updated: false, reason: "missing_cwd" };
|
|
117
|
-
const found = await findClaudeSessionProjectionBySessionId(
|
|
118
|
-
sessionsDir,
|
|
119
|
-
sessionId,
|
|
120
|
-
);
|
|
121
|
-
if (!found) return { updated: false, reason: "projection_not_found" };
|
|
122
|
-
const projection = await updateClaudeSessionProjection(found.path, {
|
|
123
|
-
cwd: nextCwd,
|
|
124
|
-
updatedAt,
|
|
125
|
-
});
|
|
126
|
-
return { updated: true, path: found.path, projection };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
61
|
export async function removeClaudeSessionProjection(sessionPath) {
|
|
130
62
|
await fs.rm(sessionPath, { force: true });
|
|
131
63
|
}
|
|
@@ -157,6 +157,25 @@ function acceptLine(state, line, marker) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// daemon 과 tmux wait 가 같은 기준으로 끝을 판정한다. 아직이면 null.
|
|
161
|
+
export function claudeWaitVerdict(transcript, idle) {
|
|
162
|
+
if (transcript?.error)
|
|
163
|
+
return { ok: false, status: "failed", error: transcript.error };
|
|
164
|
+
if (
|
|
165
|
+
!transcript?.userSeen ||
|
|
166
|
+
!transcript.turnEnded ||
|
|
167
|
+
!(idle || transcript.sectionClosed)
|
|
168
|
+
)
|
|
169
|
+
return null;
|
|
170
|
+
if (!transcript.response.trim())
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: "failed",
|
|
174
|
+
error: "turn ended without assistant text",
|
|
175
|
+
};
|
|
176
|
+
return { status: "completed", done: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
|
|
161
180
|
if (!transcriptPath) return null;
|
|
162
181
|
const marker = requestId ? `[tfx-live req=${requestId}]` : null;
|