@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
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hub/cli-adapter-base.mjs — codex/gemini 공통 CLI adapter 인터페이스
|
|
2
2
|
// Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
|
|
7
7
|
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
@@ -70,71 +70,6 @@ export function parseRetryAfterMs(text, provider) {
|
|
|
70
70
|
return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// ── Codex CLI compatibility ─────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
let _cachedVersion = null;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
|
|
79
|
-
* 파싱 실패 시 0 반환 (구버전으로 간주).
|
|
80
|
-
* @returns {number} 마이너 버전 (예: 0.117.0 → 117)
|
|
81
|
-
*/
|
|
82
|
-
export function getCodexVersion() {
|
|
83
|
-
if (_cachedVersion !== null) return _cachedVersion;
|
|
84
|
-
const override = Number(process.env.TFX_CODEX_VERSION_MINOR);
|
|
85
|
-
if (Number.isFinite(override) && override > 0) {
|
|
86
|
-
_cachedVersion = override;
|
|
87
|
-
return _cachedVersion;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
const out = execSync("codex --version", {
|
|
91
|
-
encoding: "utf8",
|
|
92
|
-
timeout: 5000,
|
|
93
|
-
}).trim();
|
|
94
|
-
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
95
|
-
_cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
|
|
96
|
-
} catch {
|
|
97
|
-
// Command builders should remain stable in CI even when the real Codex
|
|
98
|
-
// CLI is absent. Runtime preflight still reports/install-gates Codex
|
|
99
|
-
// separately; this fallback only selects the modern argv shape.
|
|
100
|
-
_cachedVersion = 117;
|
|
101
|
-
}
|
|
102
|
-
return _cachedVersion;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 최소 마이너 버전 이상인지 확인.
|
|
107
|
-
* @param {number} minMinor
|
|
108
|
-
* @returns {boolean}
|
|
109
|
-
*/
|
|
110
|
-
export function gte(minMinor) {
|
|
111
|
-
return getCodexVersion() >= minMinor;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Codex CLI 기능별 분기 객체.
|
|
116
|
-
* 실측 기반 임계값: 0.114.0에서 exec/skip-git-repo-check/color 확인됨.
|
|
117
|
-
* --output-last-message는 0.114.0에 없음 (0.117+ 추정).
|
|
118
|
-
*/
|
|
119
|
-
export const FEATURES = {
|
|
120
|
-
/** exec 서브커맨드 사용 가능 여부 (0.110+ 이전부터 존재) */
|
|
121
|
-
get execSubcommand() {
|
|
122
|
-
return gte(110);
|
|
123
|
-
},
|
|
124
|
-
/** --output-last-message 플래그 지원 여부 (0.117+) */
|
|
125
|
-
get outputLastMessage() {
|
|
126
|
-
return gte(117);
|
|
127
|
-
},
|
|
128
|
-
/** --color <COLOR> 플래그 지원 여부 (exec와 동시 도입) */
|
|
129
|
-
get colorNever() {
|
|
130
|
-
return gte(110);
|
|
131
|
-
},
|
|
132
|
-
/** 플러그인 시스템 지원 여부 (향후 확장용) */
|
|
133
|
-
get pluginSystem() {
|
|
134
|
-
return gte(120);
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
73
|
// ── Shell utilities ─────────────────────────────────────────────
|
|
139
74
|
|
|
140
75
|
export function normalizePathForShell(value) {
|
|
@@ -182,7 +117,7 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
182
117
|
enforceCanonicalProfile,
|
|
183
118
|
} = opts;
|
|
184
119
|
|
|
185
|
-
const parts = ["codex"];
|
|
120
|
+
const parts = ["codex", "exec"];
|
|
186
121
|
// Select the effort profile via `-c` config overrides instead of
|
|
187
122
|
// `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
|
|
188
123
|
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
@@ -196,30 +131,17 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
196
131
|
})
|
|
197
132
|
: [];
|
|
198
133
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
for (const
|
|
208
|
-
parts.push("-c",
|
|
209
|
-
// NOTE: `codex exec`는 --cwd 플래그를 지원하지 않는다. Node spawn의 cwd
|
|
210
|
-
// 옵션으로 child process의 working directory를 제어한다 (conductor.mjs 참조).
|
|
211
|
-
// opts.cwd는 기록용으로만 받아두고 CLI command에는 반영하지 않는다.
|
|
212
|
-
// (이전 커밋에서 잘못 추가되어 shard 전체가 exit 2로 크래시한 회귀 #94 후속 이슈)
|
|
213
|
-
if (Array.isArray(mcpServers)) {
|
|
214
|
-
for (const server of mcpServers) {
|
|
215
|
-
parts.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
216
|
-
}
|
|
134
|
+
if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
135
|
+
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
136
|
+
if (resultFile) parts.push("--output-last-message", resultFile);
|
|
137
|
+
parts.push("--color", "never");
|
|
138
|
+
for (const override of profileOverrides)
|
|
139
|
+
parts.push("-c", shellQuote(override));
|
|
140
|
+
// `codex exec`는 --cwd를 받지 않아 child process의 cwd로 제어한다.
|
|
141
|
+
if (Array.isArray(mcpServers)) {
|
|
142
|
+
for (const server of mcpServers) {
|
|
143
|
+
parts.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
217
144
|
}
|
|
218
|
-
} else {
|
|
219
|
-
parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
220
|
-
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
221
|
-
for (const override of profileOverrides)
|
|
222
|
-
parts.push("-c", shellQuote(override));
|
|
223
145
|
}
|
|
224
146
|
|
|
225
147
|
const useStdin = resolveStdinPromptMode(stdinPrompt);
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -126,7 +126,6 @@ export function buildExecArgs(opts = {}) {
|
|
|
126
126
|
cwd: opts.cwd,
|
|
127
127
|
// stdinPrompt 가 명시되면 buildExecCommand 로 forward.
|
|
128
128
|
// headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
|
|
129
|
-
// launcher path (launcher-template.mjs) 는 결정론 위해 false 전달.
|
|
130
129
|
stdinPrompt: opts.stdinPrompt,
|
|
131
130
|
});
|
|
132
131
|
|
package/hub/codex-preflight.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { join } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { whichCommandAsync } from "./platform.mjs";
|
|
7
7
|
|
|
8
|
-
const MIN_RECOMMENDED_MINOR = 118;
|
|
9
8
|
let _cachedVersion = null;
|
|
10
9
|
|
|
11
10
|
function escapeRegExp(value) {
|
|
@@ -74,25 +73,11 @@ async function checkCodexInstalled() {
|
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function checkCodexVersion() {
|
|
78
|
-
const version = getCodexVersion();
|
|
79
|
-
const warnings =
|
|
80
|
-
version >= MIN_RECOMMENDED_MINOR
|
|
81
|
-
? []
|
|
82
|
-
: [
|
|
83
|
-
`Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`,
|
|
84
|
-
];
|
|
85
|
-
return { version, warnings };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
76
|
function checkApprovalMode(configText, opts = {}) {
|
|
89
|
-
const
|
|
90
|
-
const sandbox = readTomlString(configText, "sandbox");
|
|
77
|
+
const approvalPolicy = readTomlString(configText, "approval_policy");
|
|
91
78
|
const subcommand = opts.subcommand || "exec";
|
|
92
79
|
return {
|
|
93
|
-
needsBypass: subcommand === "exec" ||
|
|
94
|
-
approvalMode,
|
|
95
|
-
sandbox,
|
|
80
|
+
needsBypass: subcommand === "exec" || approvalPolicy !== "never",
|
|
96
81
|
};
|
|
97
82
|
}
|
|
98
83
|
|
|
@@ -154,7 +139,7 @@ async function checkMcpHealth(mcpServers, configText) {
|
|
|
154
139
|
/**
|
|
155
140
|
* Detects codex config drift between global (~/.codex/config.toml) and the
|
|
156
141
|
* target workdir. Covers:
|
|
157
|
-
* 1. workdir-local `.codex/config.toml` overriding global
|
|
142
|
+
* 1. workdir-local `.codex/config.toml` overriding global approval_policy/sandbox_mode
|
|
158
143
|
* 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
|
|
159
144
|
*
|
|
160
145
|
* @param {string} [workdir] absolute path to the invocation workdir
|
|
@@ -173,7 +158,7 @@ export function detectWorkdirDrift(workdir, globalConfigText = "") {
|
|
|
173
158
|
} catch {
|
|
174
159
|
localText = "";
|
|
175
160
|
}
|
|
176
|
-
const keys = ["
|
|
161
|
+
const keys = ["approval_policy", "sandbox_mode", "model"];
|
|
177
162
|
for (const key of keys) {
|
|
178
163
|
const local = readTomlString(localText, key);
|
|
179
164
|
const global = readTomlString(globalConfigText, key);
|
|
@@ -212,18 +197,10 @@ export async function runPreflight(opts = {}) {
|
|
|
212
197
|
}
|
|
213
198
|
|
|
214
199
|
const warnings = [...install.warnings];
|
|
215
|
-
const
|
|
216
|
-
warnings.push(...versionWarnings);
|
|
200
|
+
const version = getCodexVersion();
|
|
217
201
|
|
|
218
202
|
const configText = readConfigText(opts.configPath);
|
|
219
203
|
const approval = checkApprovalMode(configText, opts);
|
|
220
|
-
if (approval.approvalMode !== "full-auto") {
|
|
221
|
-
warnings.push(
|
|
222
|
-
`approval_mode is '${approval.approvalMode || "unset"}'; bypass flag will be used.`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
if (approval.sandbox)
|
|
226
|
-
warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
|
|
227
204
|
|
|
228
205
|
const mcp = await checkMcpHealth(opts.mcpServers, configText);
|
|
229
206
|
warnings.push(...mcp.warnings);
|
package/hub/index.mjs
CHANGED
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
// @triflux/core — barrel export (auto-generated by pack.mjs)
|
|
2
2
|
// Adapters
|
|
3
|
-
export { buildExecCommand, sleep,
|
|
3
|
+
export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
|
|
4
4
|
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } 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';
|
|
11
|
+
export { parseArgs, parseJsonSafe } from './bridge.mjs';
|
|
28
12
|
export { broker as accountBroker } from './account-broker.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 });
|
|
@@ -488,68 +470,6 @@ function hasExactGbrainServe(commandLine) {
|
|
|
488
470
|
);
|
|
489
471
|
}
|
|
490
472
|
|
|
491
|
-
function hasGbrainServeCommand(commandLine) {
|
|
492
|
-
return /(^|\s)gbrain\s+serve(\s|$)/i.test(commandLine.trim());
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function hasFsmonitorDaemonCommand(commandLine) {
|
|
496
|
-
return FSMONITOR_DAEMON_PATTERN.test(commandLine);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function categoryForShardProcess(proc, context) {
|
|
500
|
-
const name = normalizeName(proc.name);
|
|
501
|
-
const commandLine = normalizeCommandLine(proc.commandLine);
|
|
502
|
-
const worktreePath = normalizeCommandLine(context.worktreePath || "");
|
|
503
|
-
const shardMarker = context.shardName
|
|
504
|
-
? `.codex-swarm\\wt-${context.shardName}`
|
|
505
|
-
: "";
|
|
506
|
-
const hasWorktree = worktreePath && commandLine.includes(worktreePath);
|
|
507
|
-
const hasShardMarker = shardMarker && commandLine.includes(shardMarker);
|
|
508
|
-
|
|
509
|
-
if (name === "node.exe" || name === "node") {
|
|
510
|
-
if (hasWorktree || hasShardMarker) return "node";
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (name === "bash.exe" || name === "bash" || name === "sh") {
|
|
514
|
-
if (hasWorktree) return "bash";
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (name === "conhost.exe" || name === "conhost") {
|
|
518
|
-
if (
|
|
519
|
-
context.sessionIds?.length > 0 &&
|
|
520
|
-
context.sessionIds.some((id) => id && commandLine.includes(id))
|
|
521
|
-
) {
|
|
522
|
-
return "conhost";
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (name === "bun.exe" || name === "bun") {
|
|
527
|
-
if ((hasWorktree || hasShardMarker) && hasGbrainServeCommand(commandLine)) {
|
|
528
|
-
return "bun";
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (name === "git.exe" || name === "git") {
|
|
533
|
-
if (
|
|
534
|
-
(hasWorktree || hasShardMarker) &&
|
|
535
|
-
hasFsmonitorDaemonCommand(commandLine)
|
|
536
|
-
) {
|
|
537
|
-
return "git";
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function killPid(
|
|
545
|
-
pid,
|
|
546
|
-
{ killFn = process.kill, protectedPids = new Set() } = {},
|
|
547
|
-
) {
|
|
548
|
-
if (protectedPids.has(pid)) return false;
|
|
549
|
-
killFn(pid, "SIGKILL");
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
473
|
/**
|
|
554
474
|
* Return a process tree rooted at `rootPid`.
|
|
555
475
|
*
|
|
@@ -730,67 +650,6 @@ export function findProcessesByCommandLine(
|
|
|
730
650
|
});
|
|
731
651
|
}
|
|
732
652
|
|
|
733
|
-
/**
|
|
734
|
-
* Cleanup processes associated with a swarm shard.
|
|
735
|
-
*
|
|
736
|
-
* Sequence: kill known top PID snapshots, scan scoped command lines, skip
|
|
737
|
-
* protected PIDs, and kill only narrowly gated runtime categories.
|
|
738
|
-
*
|
|
739
|
-
* @param {{worktreePath?: string, sessionIds?: string[], topPids?: Array<number | {pid: number}>, runId?: string, shardName?: string, dryRun?: boolean, isWindows?: boolean, spawnSyncFn?: typeof spawnSync, killFn?: typeof process.kill, protectedPids?: Set<number>}} opts
|
|
740
|
-
* @returns {{killed: number, scanned: number, skipped: number, byCategory: {node: number, bash: number, conhost: number, bun: number, git: number}}}
|
|
741
|
-
*/
|
|
742
|
-
export function cleanupShardProcesses({
|
|
743
|
-
worktreePath,
|
|
744
|
-
sessionIds = [],
|
|
745
|
-
topPids = [],
|
|
746
|
-
runId,
|
|
747
|
-
shardName,
|
|
748
|
-
dryRun = false,
|
|
749
|
-
isWindows = IS_WINDOWS,
|
|
750
|
-
spawnSyncFn = spawnSync,
|
|
751
|
-
killFn = process.kill,
|
|
752
|
-
protectedPids,
|
|
753
|
-
} = {}) {
|
|
754
|
-
const protectedSet = getProtectedPids({
|
|
755
|
-
protectedPids,
|
|
756
|
-
isWindows,
|
|
757
|
-
spawnSyncFn,
|
|
758
|
-
includeAncestorScan: true,
|
|
759
|
-
});
|
|
760
|
-
const byCategory = { node: 0, bash: 0, conhost: 0, bun: 0, git: 0 };
|
|
761
|
-
let killed = 0;
|
|
762
|
-
let skipped = 0;
|
|
763
|
-
|
|
764
|
-
if (!dryRun && topPids.length > 0) {
|
|
765
|
-
killed += killProcessTreeSnapshot(topPids, {
|
|
766
|
-
killFn,
|
|
767
|
-
protectedPids: protectedSet,
|
|
768
|
-
}).killed;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const scanned = getProcessSnapshot({ isWindows, spawnSyncFn });
|
|
772
|
-
|
|
773
|
-
for (const proc of scanned) {
|
|
774
|
-
const category = categoryForShardProcess(proc, {
|
|
775
|
-
worktreePath,
|
|
776
|
-
sessionIds,
|
|
777
|
-
shardName,
|
|
778
|
-
});
|
|
779
|
-
if (!category) continue;
|
|
780
|
-
byCategory[category]++;
|
|
781
|
-
if (protectedSet.has(proc.pid)) {
|
|
782
|
-
skipped++;
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
if (dryRun) continue;
|
|
786
|
-
try {
|
|
787
|
-
if (killPid(proc.pid, { killFn, protectedPids: protectedSet })) killed++;
|
|
788
|
-
} catch {}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
return { killed, scanned: scanned.length, skipped, byCategory };
|
|
792
|
-
}
|
|
793
|
-
|
|
794
653
|
/**
|
|
795
654
|
* Cleanup narrowly gated orphan runtime processes.
|
|
796
655
|
*
|
|
@@ -968,22 +827,8 @@ export function cleanupStaleFsmonitorDaemons({
|
|
|
968
827
|
function cleanupOrphansUnix() {
|
|
969
828
|
const myPid = process.pid;
|
|
970
829
|
|
|
971
|
-
// Hub PID 보호
|
|
972
830
|
const protectedPids = new Set();
|
|
973
831
|
protectedPids.add(myPid);
|
|
974
|
-
try {
|
|
975
|
-
const hubPidPath = join(
|
|
976
|
-
homedir(),
|
|
977
|
-
".claude",
|
|
978
|
-
"cache",
|
|
979
|
-
"tfx-hub",
|
|
980
|
-
"hub.pid",
|
|
981
|
-
);
|
|
982
|
-
if (existsSync(hubPidPath)) {
|
|
983
|
-
const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
|
|
984
|
-
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
985
|
-
}
|
|
986
|
-
} catch {}
|
|
987
832
|
|
|
988
833
|
// 현재 프로세스의 조상 트리 보호
|
|
989
834
|
try {
|
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -7,13 +7,14 @@ import { join } from "node:path";
|
|
|
7
7
|
const LOG_DIR = join(homedir(), ".triflux", "logs");
|
|
8
8
|
const DEDUPE_WINDOW_MS = 5_000;
|
|
9
9
|
const RATE_WINDOW_MS = 1_000;
|
|
10
|
+
const DEFAULT_MAX_SPAWN_PER_SEC = 100;
|
|
10
11
|
// multi-worker headless dispatch 가 1 초 안에 ~25-30+ psmux/tmux 명령을 호출한다.
|
|
11
12
|
// 2-worker dispatchBatch 가 default 30 limit 을 초과해 `rate_limit` throw 로 mac
|
|
12
13
|
// 호환성 회귀 (smoke test 발견, 2026-05-15). 100 으로 상향 — 폭주 안전망은
|
|
13
14
|
// 여전히 (default 100/sec) 유지, env override 로 보수 설정 가능.
|
|
14
15
|
export let MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
15
16
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
16
|
-
|
|
17
|
+
DEFAULT_MAX_SPAWN_PER_SEC,
|
|
17
18
|
);
|
|
18
19
|
export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
|
|
19
20
|
process.env.TRIFLUX_MAX_DESCENDANTS,
|
|
@@ -160,7 +161,7 @@ export function getMaxSpawnPerSec() {
|
|
|
160
161
|
export function reload() {
|
|
161
162
|
MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
162
163
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
163
|
-
|
|
164
|
+
DEFAULT_MAX_SPAWN_PER_SEC,
|
|
164
165
|
);
|
|
165
166
|
return getMaxSpawnPerSec();
|
|
166
167
|
}
|
|
@@ -5,7 +5,6 @@ export const MAX_DURATION_MS = 86_400_000;
|
|
|
5
5
|
export const DEFAULT_ASSIGN_TIMEOUT_MS = 600_000;
|
|
6
6
|
export const DEFAULT_ASSIGN_TTL_MS = 600_000;
|
|
7
7
|
export const DEFAULT_HANDOFF_TTL_MS = 600_000;
|
|
8
|
-
export const DEFAULT_SWARM_LOCK_TTL_MS = 600_000;
|
|
9
8
|
export const DEFAULT_REGISTER_TIMEOUT_SEC = 600;
|
|
10
9
|
export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
|
|
11
10
|
export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
|
|
@@ -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
|
}
|