triflux 10.27.0 → 10.28.1
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/bin/tfx-live.mjs +45 -0
- package/bin/triflux.mjs +4 -4
- package/config/mcp-registry.json +10 -6
- package/hooks/pipeline-stop.mjs +13 -1
- package/hub/team/claude-daemon-control.mjs +214 -0
- package/hub/team/claude-native-bridge.mjs +45 -73
- package/hub/team/daemon-pty-tmux-bridge.mjs +146 -0
- package/hub/team/headless.mjs +23 -42
- package/hub/team/interactive-native-launcher.mjs +88 -75
- package/hub/team/retry-state-machine.mjs +2 -2
- package/hub/team/uds-orchestrator.mjs +607 -0
- package/hub/workers/codex-app-server-worker.mjs +4 -1
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +686 -0
- package/hud/context-monitor.mjs +26 -4
- package/package.json +1 -1
- package/scripts/__tests__/mcp-gateway-health-check.test.mjs +53 -0
- package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +8 -8
- package/scripts/codex-gateway-preflight.mjs +82 -22
- package/scripts/codex-mcp-gateway-sync.mjs +65 -31
- package/scripts/lib/mcp-gateway-health-check.mjs +65 -0
- package/scripts/lib/mcp-guard-engine.mjs +52 -8
- package/scripts/mcp-gateway-config.mjs +13 -13
- package/scripts/mcp-gateway-ensure.mjs +27 -12
- package/scripts/mcp-gateway-integration-test.mjs +21 -15
- package/scripts/mcp-gateway-start.mjs +6 -3
- package/scripts/mcp-gateway-start.ps1 +3 -3
- package/scripts/mcp-gateway-verify.mjs +1 -1
- package/scripts/release/check-packages-mirror.mjs +4 -0
- package/scripts/release/prepare.mjs +7 -4
- package/scripts/session-stale-cleanup.mjs +31 -1
- package/scripts/sync-hub-mcp-settings.mjs +30 -14
- package/skills/tfx-live/SKILL.md +19 -0
- package/skills/tfx-research/SKILL.md +5 -7
- package/skills/tfx-research/skill.json +1 -1
- package/skills/tfx-setup/SKILL.md +4 -4
package/bin/tfx-live.mjs
CHANGED
|
@@ -42,6 +42,8 @@ function usage() {
|
|
|
42
42
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
43
43
|
" tfx-live goal-driven --session NAME --goal TEXT [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500] [--max-rounds 8] [--done-token DONE]",
|
|
44
44
|
" tfx-live peer [--cli-a codex] [--cli-b claude] [--session-a peerA] [--session-b peerB] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--bridge ABS] [--remote HOST] [--cwd DIR] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
45
|
+
" tfx-live orchestrate --task TEXT [--mode peer|codex-led|claude-led] [--codex-transport exec|app-server-uds] [--cwd DIR] [--timeout 120]",
|
|
46
|
+
" Runs the Claude(UDS)+Codex orchestration engine. --codex-transport app-server-uds drives a real `codex app-server` over WebSocket-over-UDS (experimental); default exec keeps the codex stdio one-shot path.",
|
|
45
47
|
].join("\n");
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -1690,6 +1692,47 @@ async function peer(flags) {
|
|
|
1690
1692
|
printJson(output);
|
|
1691
1693
|
}
|
|
1692
1694
|
|
|
1695
|
+
const ORCHESTRATION_MODES = ["peer", "codex-led", "claude-led"];
|
|
1696
|
+
const CODEX_ORCH_TRANSPORTS = ["exec", "app-server-uds"];
|
|
1697
|
+
|
|
1698
|
+
async function orchestrate(flags) {
|
|
1699
|
+
const mode = flags.mode ?? "peer";
|
|
1700
|
+
if (!ORCHESTRATION_MODES.includes(mode)) {
|
|
1701
|
+
throw new Error(`--mode must be one of: ${ORCHESTRATION_MODES.join(", ")}`);
|
|
1702
|
+
}
|
|
1703
|
+
const task = requireFlag(flags, "task");
|
|
1704
|
+
const codexTransport = flags["codex-transport"] ?? "exec";
|
|
1705
|
+
if (!CODEX_ORCH_TRANSPORTS.includes(codexTransport)) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`--codex-transport must be one of: ${CODEX_ORCH_TRANSPORTS.join(", ")}`,
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
1711
|
+
const timeoutMs = secondsFlag(flags, "timeout", 120_000);
|
|
1712
|
+
|
|
1713
|
+
// Lazy import keeps the orchestration engine (and its hub/team deps) off the
|
|
1714
|
+
// hot path for every other thin-CLI verb; only `orchestrate` pays the cost.
|
|
1715
|
+
const orchestratorUrl = new URL(
|
|
1716
|
+
"../hub/team/uds-orchestrator.mjs",
|
|
1717
|
+
import.meta.url,
|
|
1718
|
+
).href;
|
|
1719
|
+
const {
|
|
1720
|
+
runUdsOrchestration,
|
|
1721
|
+
createClaudeUdsEndpoint,
|
|
1722
|
+
createCodexExecEndpoint,
|
|
1723
|
+
createCodexAppServerUdsEndpoint,
|
|
1724
|
+
} = await import(orchestratorUrl);
|
|
1725
|
+
|
|
1726
|
+
const claude = createClaudeUdsEndpoint({ cwd, timeoutMs });
|
|
1727
|
+
const codex =
|
|
1728
|
+
codexTransport === "app-server-uds"
|
|
1729
|
+
? createCodexAppServerUdsEndpoint({ cwd, timeoutMs })
|
|
1730
|
+
: createCodexExecEndpoint({ workdir: cwd, timeout: timeoutMs });
|
|
1731
|
+
|
|
1732
|
+
const result = await runUdsOrchestration({ mode, task, claude, codex });
|
|
1733
|
+
printJson({ codexTransport, ...result });
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1693
1736
|
function printJson(value) {
|
|
1694
1737
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
1695
1738
|
}
|
|
@@ -1718,6 +1761,8 @@ async function main() {
|
|
|
1718
1761
|
await goalDriven(flags);
|
|
1719
1762
|
} else if (command === "peer") {
|
|
1720
1763
|
await peer(flags);
|
|
1764
|
+
} else if (command === "orchestrate") {
|
|
1765
|
+
await orchestrate(flags);
|
|
1721
1766
|
} else {
|
|
1722
1767
|
throw new Error(`Unknown subcommand: ${command}\n${usage()}`);
|
|
1723
1768
|
}
|
package/bin/triflux.mjs
CHANGED
|
@@ -4864,10 +4864,9 @@ async function cmdDoctor(options = {}) {
|
|
|
4864
4864
|
// skip 된 server 를 잡는다. 로그가 없으면 gateway 미설치/미실행으로 침묵.
|
|
4865
4865
|
section("MCP Gateway Health");
|
|
4866
4866
|
{
|
|
4867
|
-
const {
|
|
4868
|
-
"../scripts/lib/mcp-gateway-health-check.mjs"
|
|
4869
|
-
);
|
|
4870
|
-
const gatewayHealth = checkMcpGatewayHealth();
|
|
4867
|
+
const { checkMcpGatewayHealthLive, summarizeMcpGatewayHealth } =
|
|
4868
|
+
await import("../scripts/lib/mcp-gateway-health-check.mjs");
|
|
4869
|
+
const gatewayHealth = await checkMcpGatewayHealthLive();
|
|
4871
4870
|
const summary = summarizeMcpGatewayHealth(gatewayHealth);
|
|
4872
4871
|
addDoctorCheck(report, {
|
|
4873
4872
|
name: "mcp-gateway-health",
|
|
@@ -4875,6 +4874,7 @@ async function cmdDoctor(options = {}) {
|
|
|
4875
4874
|
log_path: gatewayHealth.logPath,
|
|
4876
4875
|
findings: gatewayHealth.findings,
|
|
4877
4876
|
started: gatewayHealth.started,
|
|
4877
|
+
live: gatewayHealth.live,
|
|
4878
4878
|
skipped: gatewayHealth.skipped,
|
|
4879
4879
|
...(summary.fix ? { fix: summary.fix } : {}),
|
|
4880
4880
|
});
|
package/config/mcp-registry.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"hub_base": "http://127.0.0.1:27888"
|
|
8
8
|
},
|
|
9
9
|
"policy_notes": {
|
|
10
|
-
"policy": "Server policy is the SSOT for client sync shape: hosted writes url+headers, gateway-sse writes http://127.0.0.1:81XX/sse with SSE metadata, and stdio writes command/args/env.",
|
|
11
|
-
"transport": "Server transport accepts \"hub-url\" for triflux Hub URL flow, \"http\" for direct Streamable HTTP MCP endpoints, or \"stdio\" for upstream-stdio-only MCP servers. Gateway-backed stdio upstreams use policy:\"gateway-
|
|
10
|
+
"policy": "Server policy is the SSOT for client sync shape: hosted writes url+headers, gateway-sse writes http://127.0.0.1:81XX/sse with SSE metadata, gateway-http writes http://127.0.0.1:81XX/mcp with HTTP metadata, and stdio writes command/args/env.",
|
|
11
|
+
"transport": "Server transport accepts \"hub-url\" for triflux Hub URL flow, \"http\" for direct Streamable HTTP MCP endpoints, or \"stdio\" for upstream-stdio-only MCP servers. Gateway-backed stdio upstreams should use policy:\"gateway-http\" plus gateway_port/gateway_path so clients receive reconnect-safe HTTP MCP config; legacy policy:\"gateway-sse\" remains supported only for backward compatibility.",
|
|
12
12
|
"headers": "Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {\"value\":\"literal\"} for non-secret static values, {\"env\":\"ENV_VAR_NAME\"} for secrets resolved at sync/runtime, or {\"env\":\"ENV_VAR_NAME\",\"prefix\":\"Bearer \"} for common authorization formats.",
|
|
13
13
|
"secret_safety": "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
|
|
14
14
|
"sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example gemini:tfx-hub."
|
|
@@ -42,18 +42,22 @@
|
|
|
42
42
|
"description": "Exa neural/semantic web search — 학술/기술 깊이. Key 발급: https://exa.ai/dashboard → secrets.env의 EXA_API_KEY"
|
|
43
43
|
},
|
|
44
44
|
"serena": {
|
|
45
|
-
"policy": "gateway-
|
|
45
|
+
"policy": "gateway-http",
|
|
46
|
+
"transport": "http",
|
|
46
47
|
"gateway_port": 8105,
|
|
48
|
+
"gateway_path": "/mcp",
|
|
47
49
|
"safe": true,
|
|
48
50
|
"targets": ["claude", "gemini", "codex", "antigravity"],
|
|
49
|
-
"description": "Serena MCP — supergateway
|
|
51
|
+
"description": "Serena MCP — local supergateway stateful Streamable HTTP endpoint (:8105/mcp)"
|
|
50
52
|
},
|
|
51
53
|
"brave-search": {
|
|
52
|
-
"policy": "gateway-
|
|
54
|
+
"policy": "gateway-http",
|
|
55
|
+
"transport": "http",
|
|
53
56
|
"gateway_port": 8101,
|
|
57
|
+
"gateway_path": "/mcp",
|
|
54
58
|
"safe": true,
|
|
55
59
|
"targets": ["claude", "gemini", "codex", "antigravity"],
|
|
56
|
-
"description": "Brave Search MCP — supergateway
|
|
60
|
+
"description": "Brave Search MCP — local supergateway stateful Streamable HTTP endpoint (:8101/mcp). BRAVE_API_KEY 환경변수 필요 (https://brave.com/search/api/). secrets.env 의 BRAVE_API_KEY 참조."
|
|
57
61
|
},
|
|
58
62
|
"tavily": {
|
|
59
63
|
"policy": "hosted",
|
package/hooks/pipeline-stop.mjs
CHANGED
|
@@ -142,7 +142,7 @@ function contextPercentsFromObject(value) {
|
|
|
142
142
|
];
|
|
143
143
|
|
|
144
144
|
const currentUsage = value.current_usage ?? value.currentUsage ?? {};
|
|
145
|
-
const
|
|
145
|
+
const explicitMaxTokens =
|
|
146
146
|
value.context_window_size ??
|
|
147
147
|
value.contextWindowSize ??
|
|
148
148
|
value.max_context_tokens ??
|
|
@@ -151,6 +151,18 @@ function contextPercentsFromObject(value) {
|
|
|
151
151
|
value.maxTokens ??
|
|
152
152
|
value.total_tokens ??
|
|
153
153
|
value.totalTokens;
|
|
154
|
+
// Fall back to a 1M context window when the payload omits one — Opus 4.x
|
|
155
|
+
// [1M] and Codex gpt-5.5 both run on a 1M window, and assuming 200K there
|
|
156
|
+
// false-positives at ~15% real usage. Override via
|
|
157
|
+
// TFX_CONTEXT_DEFAULT_MAX_TOKENS for narrower setups.
|
|
158
|
+
const envFallback = Number(process.env.TFX_CONTEXT_DEFAULT_MAX_TOKENS);
|
|
159
|
+
const fallbackMaxTokens =
|
|
160
|
+
Number.isFinite(envFallback) && envFallback > 0 ? envFallback : 1_000_000;
|
|
161
|
+
const explicitNumeric = Number(explicitMaxTokens);
|
|
162
|
+
const maxTokens =
|
|
163
|
+
Number.isFinite(explicitNumeric) && explicitNumeric > 0
|
|
164
|
+
? explicitNumeric
|
|
165
|
+
: fallbackMaxTokens;
|
|
154
166
|
candidates.push(
|
|
155
167
|
tokenPercent(value.used_tokens ?? value.usedTokens, maxTokens),
|
|
156
168
|
tokenPercent(
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import net from "node:net";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
buildClaudeSessionProjection,
|
|
9
|
+
removeClaudeSessionProjection,
|
|
10
|
+
writeClaudeSessionProjection,
|
|
11
|
+
} from "./claude-session-projection.mjs";
|
|
6
12
|
|
|
7
13
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
8
14
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
@@ -26,12 +32,23 @@ export function deriveClaudeDaemonPaths({
|
|
|
26
32
|
hash,
|
|
27
33
|
daemonDir,
|
|
28
34
|
controlSock: path.join(daemonDir, "control.sock"),
|
|
35
|
+
rendezvousDir: path.join(daemonDir, "rv"),
|
|
36
|
+
ptyDir: path.join(daemonDir, "pty"),
|
|
29
37
|
rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
|
|
30
38
|
sessionsDir: path.join(resolvedConfigDir, "sessions"),
|
|
31
39
|
jobsDir: path.join(resolvedConfigDir, "jobs"),
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
export function getProcStart(pid = process.pid) {
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
45
|
+
throw new Error(`invalid pid: ${pid}`);
|
|
46
|
+
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
49
|
+
}).trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
export function readFirstJsonLine(text) {
|
|
36
53
|
const line = String(text)
|
|
37
54
|
.split("\n")
|
|
@@ -991,6 +1008,52 @@ export function buildDaemonExecDispatchPayload({
|
|
|
991
1008
|
};
|
|
992
1009
|
}
|
|
993
1010
|
|
|
1011
|
+
export function buildClaudePromptDispatchPayload({
|
|
1012
|
+
short = crypto.randomBytes(4).toString("hex"),
|
|
1013
|
+
sessionId,
|
|
1014
|
+
cwd = process.cwd(),
|
|
1015
|
+
prompt,
|
|
1016
|
+
name = "tfx uds claude prompt",
|
|
1017
|
+
createdAt = Date.now(),
|
|
1018
|
+
cols = 120,
|
|
1019
|
+
rows = 40,
|
|
1020
|
+
} = {}) {
|
|
1021
|
+
if (!prompt) throw new Error("prompt is required");
|
|
1022
|
+
const uuid = crypto.randomUUID();
|
|
1023
|
+
const resolvedSessionId = sessionId || `${short}${uuid.slice(8)}`;
|
|
1024
|
+
return {
|
|
1025
|
+
proto: 1,
|
|
1026
|
+
short,
|
|
1027
|
+
sessionId: resolvedSessionId,
|
|
1028
|
+
createdAt,
|
|
1029
|
+
source: "shell",
|
|
1030
|
+
cwd,
|
|
1031
|
+
agent: "claude",
|
|
1032
|
+
launch: {
|
|
1033
|
+
mode: "prompt",
|
|
1034
|
+
args: [
|
|
1035
|
+
"--session-id",
|
|
1036
|
+
resolvedSessionId,
|
|
1037
|
+
"--agent",
|
|
1038
|
+
"claude",
|
|
1039
|
+
"--permission-mode",
|
|
1040
|
+
"auto",
|
|
1041
|
+
"--",
|
|
1042
|
+
prompt,
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
env: {},
|
|
1046
|
+
isolation: "none",
|
|
1047
|
+
respawnFlags: [],
|
|
1048
|
+
seed: {
|
|
1049
|
+
intent: "[redacted uds orchestration prompt]",
|
|
1050
|
+
name,
|
|
1051
|
+
},
|
|
1052
|
+
cols,
|
|
1053
|
+
rows,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
994
1057
|
export function findDaemonJobByShort(listResponse, short) {
|
|
995
1058
|
if (!Array.isArray(listResponse?.jobs)) return null;
|
|
996
1059
|
return listResponse.jobs.find((job) => job?.short === short) || null;
|
|
@@ -1128,3 +1191,154 @@ export async function sendKillBySessionId({
|
|
|
1128
1191
|
};
|
|
1129
1192
|
}
|
|
1130
1193
|
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Claude 데몬 dispatch 한 건의 공통 시퀀스를 캡슐화한다.
|
|
1197
|
+
* dispatch → waitForDaemonJobPid → resolveDaemonBridgeSessionId →
|
|
1198
|
+
* buildClaudeSessionProjection → writeClaudeSessionProjection.
|
|
1199
|
+
*
|
|
1200
|
+
* 호출 측이 reference 로 들고 있는 record 를 헬퍼가 새로 만들지 않도록
|
|
1201
|
+
* plain 필드만 반환한다. caller 는 이 결과를 자기 record 에 Object.assign 한다.
|
|
1202
|
+
*
|
|
1203
|
+
* @returns {Promise<{ok:boolean, short:string, sessionId:string, job:object,
|
|
1204
|
+
* pid:number, bridgeSessionId:string, sessionProjectionPath:string,
|
|
1205
|
+
* controlSock:string, paths:object}>}
|
|
1206
|
+
*/
|
|
1207
|
+
export async function dispatchClaudeDaemonJob({
|
|
1208
|
+
paths,
|
|
1209
|
+
controlSock,
|
|
1210
|
+
payload,
|
|
1211
|
+
agent,
|
|
1212
|
+
name,
|
|
1213
|
+
cwd,
|
|
1214
|
+
projection = "write",
|
|
1215
|
+
dispatchTimeoutMs = 5000,
|
|
1216
|
+
pidTimeoutMs,
|
|
1217
|
+
bridgeTimeoutMs,
|
|
1218
|
+
accessControlSock,
|
|
1219
|
+
_deps = {},
|
|
1220
|
+
} = {}) {
|
|
1221
|
+
if (!payload) throw new Error("payload is required");
|
|
1222
|
+
if (!payload.short) throw new Error("payload.short is required");
|
|
1223
|
+
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1224
|
+
if (!resolvedControlSock) throw new Error("controlSock is required");
|
|
1225
|
+
|
|
1226
|
+
const sendControl =
|
|
1227
|
+
_deps.sendClaudeControlRequest || sendClaudeControlRequest;
|
|
1228
|
+
const waitForPid = _deps.waitForDaemonJobPid || waitForDaemonJobPid;
|
|
1229
|
+
const resolveBridgeSessionId =
|
|
1230
|
+
_deps.resolveDaemonBridgeSessionId || resolveDaemonBridgeSessionId;
|
|
1231
|
+
const buildProjection =
|
|
1232
|
+
_deps.buildClaudeSessionProjection || buildClaudeSessionProjection;
|
|
1233
|
+
const writeProjection =
|
|
1234
|
+
_deps.writeClaudeSessionProjection || writeClaudeSessionProjection;
|
|
1235
|
+
const readProcStart = _deps.getProcStart || getProcStart;
|
|
1236
|
+
|
|
1237
|
+
const short = payload.short;
|
|
1238
|
+
const sessionsDir =
|
|
1239
|
+
paths?.sessionsDir ||
|
|
1240
|
+
(paths?.configDir ? path.join(paths.configDir, "sessions") : "");
|
|
1241
|
+
|
|
1242
|
+
// native-bridge 는 control.sock 존재를 미리 점검한다 (없으면 fast-fail).
|
|
1243
|
+
if (accessControlSock) await accessControlSock(resolvedControlSock);
|
|
1244
|
+
|
|
1245
|
+
const dispatch = await sendControl(
|
|
1246
|
+
resolvedControlSock,
|
|
1247
|
+
{
|
|
1248
|
+
proto: 1,
|
|
1249
|
+
op: "dispatch",
|
|
1250
|
+
d: payload,
|
|
1251
|
+
timeoutMs: dispatchTimeoutMs,
|
|
1252
|
+
},
|
|
1253
|
+
{ timeoutMs: dispatchTimeoutMs },
|
|
1254
|
+
);
|
|
1255
|
+
if (dispatch?.ok !== true) {
|
|
1256
|
+
throw new Error(`Claude daemon dispatch failed for ${name || short}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const pidOpts =
|
|
1260
|
+
pidTimeoutMs === undefined ? undefined : { timeoutMs: pidTimeoutMs };
|
|
1261
|
+
const job = await waitForPid(resolvedControlSock, short, pidOpts);
|
|
1262
|
+
const pid = job.pid;
|
|
1263
|
+
const bridgeSessionId = await resolveBridgeSessionId({
|
|
1264
|
+
daemonPaths: paths,
|
|
1265
|
+
short,
|
|
1266
|
+
job,
|
|
1267
|
+
...(bridgeTimeoutMs === undefined ? {} : { timeoutMs: bridgeTimeoutMs }),
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
let sessionProjectionPath = "";
|
|
1271
|
+
if (projection !== "skip") {
|
|
1272
|
+
const projectionRecord = buildProjection({
|
|
1273
|
+
pid,
|
|
1274
|
+
procStart: readProcStart(pid),
|
|
1275
|
+
sessionId: payload.sessionId,
|
|
1276
|
+
short,
|
|
1277
|
+
cwd,
|
|
1278
|
+
name,
|
|
1279
|
+
agent,
|
|
1280
|
+
startedAt: job.startedAt || Date.now(),
|
|
1281
|
+
updatedAt: Date.now(),
|
|
1282
|
+
bridgeSessionId,
|
|
1283
|
+
});
|
|
1284
|
+
sessionProjectionPath = await writeProjection(
|
|
1285
|
+
sessionsDir,
|
|
1286
|
+
projectionRecord,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
short,
|
|
1293
|
+
sessionId: payload.sessionId,
|
|
1294
|
+
job,
|
|
1295
|
+
pid,
|
|
1296
|
+
bridgeSessionId,
|
|
1297
|
+
sessionProjectionPath,
|
|
1298
|
+
controlSock: resolvedControlSock,
|
|
1299
|
+
paths,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* dispatchClaudeDaemonJob 의 대칭 정리 경로.
|
|
1305
|
+
* removeProjection + killDaemonJob (+ optional removeClaudeJobState) 를
|
|
1306
|
+
* 모두 시도하며, 각 스텝 오류는 기존 close()/cleanupDaemonDispatches 와
|
|
1307
|
+
* 동일하게 .catch 로 삼킨다.
|
|
1308
|
+
*/
|
|
1309
|
+
export async function teardownClaudeDaemonJob({
|
|
1310
|
+
controlSock,
|
|
1311
|
+
paths,
|
|
1312
|
+
short,
|
|
1313
|
+
sessionProjectionPath,
|
|
1314
|
+
sessionId,
|
|
1315
|
+
jobsDir,
|
|
1316
|
+
removeJobState = false,
|
|
1317
|
+
_deps = {},
|
|
1318
|
+
} = {}) {
|
|
1319
|
+
const removeProjection =
|
|
1320
|
+
_deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
|
|
1321
|
+
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
1322
|
+
const removeJobStateImpl = _deps.removeClaudeJobState;
|
|
1323
|
+
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1324
|
+
const resolvedJobsDir =
|
|
1325
|
+
jobsDir ||
|
|
1326
|
+
paths?.jobsDir ||
|
|
1327
|
+
(paths?.configDir ? path.join(paths.configDir, "jobs") : "");
|
|
1328
|
+
|
|
1329
|
+
const steps = [];
|
|
1330
|
+
if (sessionProjectionPath) {
|
|
1331
|
+
steps.push(removeProjection(sessionProjectionPath).catch(() => {}));
|
|
1332
|
+
}
|
|
1333
|
+
if (sessionId && (paths?.controlSock || resolvedControlSock)) {
|
|
1334
|
+
const daemonPaths = paths || { controlSock: resolvedControlSock };
|
|
1335
|
+
steps.push(sendKillBySessionId({ daemonPaths, sessionId }).catch(() => {}));
|
|
1336
|
+
}
|
|
1337
|
+
if (resolvedControlSock && short) {
|
|
1338
|
+
steps.push(killJob(resolvedControlSock, short).catch(() => {}));
|
|
1339
|
+
}
|
|
1340
|
+
if (removeJobState && removeJobStateImpl && resolvedJobsDir && short) {
|
|
1341
|
+
steps.push(removeJobStateImpl(resolvedJobsDir, short).catch(() => {}));
|
|
1342
|
+
}
|
|
1343
|
+
await Promise.all(steps);
|
|
1344
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import net from "node:net";
|
|
@@ -6,9 +6,13 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
buildDaemonExecDispatchPayload,
|
|
9
|
+
deriveClaudeDaemonPaths,
|
|
10
|
+
dispatchClaudeDaemonJob,
|
|
11
|
+
getProcStart,
|
|
9
12
|
killDaemonJob,
|
|
10
13
|
resolveDaemonBridgeSessionId,
|
|
11
14
|
sendClaudeControlRequest,
|
|
15
|
+
teardownClaudeDaemonJob,
|
|
12
16
|
waitForDaemonJobPid,
|
|
13
17
|
} from "./claude-daemon-control.mjs";
|
|
14
18
|
import {
|
|
@@ -18,6 +22,10 @@ import {
|
|
|
18
22
|
} from "./claude-session-projection.mjs";
|
|
19
23
|
import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
|
|
20
24
|
|
|
25
|
+
// daemon-control 이 deriveClaudeDaemonPaths / getProcStart 의 단일 owner 다.
|
|
26
|
+
// 기존 native-bridge import 경로 (headless 포함) 호환을 위해 re-export 한다.
|
|
27
|
+
export { deriveClaudeDaemonPaths, getProcStart };
|
|
28
|
+
|
|
21
29
|
const DEFAULT_ROWS = 40;
|
|
22
30
|
const DEFAULT_COLS = 120;
|
|
23
31
|
const MAX_TRANSCRIPT_BYTES = 64 * 1024;
|
|
@@ -30,39 +38,6 @@ export function resolveClaudeConfigDir(env = process.env) {
|
|
|
30
38
|
return path.join(os.homedir(), ".claude");
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
export function deriveClaudeDaemonPaths({
|
|
34
|
-
configDir = resolveClaudeConfigDir(),
|
|
35
|
-
uid = typeof process.getuid === "function" ? process.getuid() : 0,
|
|
36
|
-
tmpRoot = "/tmp",
|
|
37
|
-
} = {}) {
|
|
38
|
-
const resolvedConfigDir = path.resolve(configDir);
|
|
39
|
-
const hash = crypto
|
|
40
|
-
.createHash("sha256")
|
|
41
|
-
.update(resolvedConfigDir)
|
|
42
|
-
.digest("hex")
|
|
43
|
-
.slice(0, 8);
|
|
44
|
-
const daemonDir = path.join(tmpRoot, `cc-daemon-${uid}`, hash);
|
|
45
|
-
return {
|
|
46
|
-
configDir: resolvedConfigDir,
|
|
47
|
-
daemonDir,
|
|
48
|
-
controlSock: path.join(daemonDir, "control.sock"),
|
|
49
|
-
rendezvousDir: path.join(daemonDir, "rv"),
|
|
50
|
-
ptyDir: path.join(daemonDir, "pty"),
|
|
51
|
-
rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
|
|
52
|
-
sessionsDir: path.join(resolvedConfigDir, "sessions"),
|
|
53
|
-
jobsDir: path.join(resolvedConfigDir, "jobs"),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getProcStart(pid = process.pid) {
|
|
58
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
59
|
-
throw new Error(`invalid pid: ${pid}`);
|
|
60
|
-
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
61
|
-
encoding: "utf8",
|
|
62
|
-
env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
63
|
-
}).trim();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
41
|
export function buildPtyDataFrame(value) {
|
|
67
42
|
const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
|
|
68
43
|
const frame = Buffer.allocUnsafe(5 + payload.length);
|
|
@@ -379,6 +354,8 @@ export async function registerSwarmShard({
|
|
|
379
354
|
const removeJobStateImpl = _deps.removeClaudeJobState || removeClaudeJobState;
|
|
380
355
|
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
381
356
|
const accessControlSock = _deps.accessControlSock || fs.access;
|
|
357
|
+
const dispatchJob = _deps.dispatchClaudeDaemonJob || dispatchClaudeDaemonJob;
|
|
358
|
+
const teardownJob = _deps.teardownClaudeDaemonJob || teardownClaudeDaemonJob;
|
|
382
359
|
|
|
383
360
|
const paths = derivePaths({ configDir, tmpRoot });
|
|
384
361
|
const sessionsDir =
|
|
@@ -487,44 +464,30 @@ export async function registerSwarmShard({
|
|
|
487
464
|
name: displayName,
|
|
488
465
|
});
|
|
489
466
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
d: payload,
|
|
497
|
-
timeoutMs: 1000,
|
|
498
|
-
},
|
|
499
|
-
{ timeoutMs: 1000 },
|
|
500
|
-
);
|
|
501
|
-
if (dispatch?.ok !== true) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
`Claude daemon dispatch failed for swarm shard ${shardName}`,
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const job = await waitForPid(paths.controlSock, short, { timeoutMs: 1000 });
|
|
508
|
-
const pid = job.pid;
|
|
509
|
-
const bridgeSessionId = await resolveBridgeSessionId({
|
|
510
|
-
daemonPaths: paths,
|
|
511
|
-
short,
|
|
512
|
-
job,
|
|
513
|
-
timeoutMs: 1000,
|
|
514
|
-
});
|
|
515
|
-
const projection = buildProjection({
|
|
516
|
-
pid,
|
|
517
|
-
procStart: readProcStart(pid),
|
|
518
|
-
sessionId: payload.sessionId,
|
|
519
|
-
short,
|
|
520
|
-
cwd,
|
|
521
|
-
name: displayName,
|
|
467
|
+
// native-bridge 는 데몬이 없으면 빠르게 실패해야 하므로 1000ms 로 고정한다
|
|
468
|
+
// (headless 의 5000ms 와 다르므로 명시 전달).
|
|
469
|
+
const dispatched = await dispatchJob({
|
|
470
|
+
paths,
|
|
471
|
+
controlSock: paths.controlSock,
|
|
472
|
+
payload,
|
|
522
473
|
agent: cli,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
474
|
+
name: displayName,
|
|
475
|
+
cwd,
|
|
476
|
+
dispatchTimeoutMs: 1000,
|
|
477
|
+
pidTimeoutMs: 1000,
|
|
478
|
+
bridgeTimeoutMs: 1000,
|
|
479
|
+
accessControlSock,
|
|
480
|
+
_deps: {
|
|
481
|
+
sendClaudeControlRequest: sendControl,
|
|
482
|
+
waitForDaemonJobPid: waitForPid,
|
|
483
|
+
resolveDaemonBridgeSessionId: resolveBridgeSessionId,
|
|
484
|
+
buildClaudeSessionProjection: buildProjection,
|
|
485
|
+
writeClaudeSessionProjection: writeProjection,
|
|
486
|
+
getProcStart: readProcStart,
|
|
487
|
+
},
|
|
526
488
|
});
|
|
527
|
-
const sessionProjectionPath =
|
|
489
|
+
const sessionProjectionPath = dispatched.sessionProjectionPath;
|
|
490
|
+
void sessionsDir;
|
|
528
491
|
let closed = false;
|
|
529
492
|
|
|
530
493
|
return {
|
|
@@ -543,9 +506,18 @@ export async function registerSwarmShard({
|
|
|
543
506
|
async close() {
|
|
544
507
|
if (closed) return;
|
|
545
508
|
closed = true;
|
|
546
|
-
await
|
|
547
|
-
|
|
548
|
-
|
|
509
|
+
await teardownJob({
|
|
510
|
+
controlSock: paths.controlSock,
|
|
511
|
+
short,
|
|
512
|
+
sessionProjectionPath,
|
|
513
|
+
jobsDir,
|
|
514
|
+
removeJobState: true,
|
|
515
|
+
_deps: {
|
|
516
|
+
removeClaudeSessionProjection: removeProjection,
|
|
517
|
+
killDaemonJob: killJob,
|
|
518
|
+
removeClaudeJobState: removeJobStateImpl,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
549
521
|
},
|
|
550
522
|
};
|
|
551
523
|
}
|