@triflux/remote 10.44.0 → 10.45.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/pipe.mjs +10 -15
- package/hub/schema.sql +1 -0
- package/hub/server.mjs +172 -93
- package/hub/store.mjs +127 -61
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +80 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +38 -2
- package/hub/team/headless.mjs +493 -82
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +39 -16
- package/hub/team/session.mjs +57 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/tools.mjs +10 -1
- package/hub/workers/codex-mcp.mjs +8 -3
- package/hub/workers/delegator-mcp.mjs +1 -1
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +84 -24
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +2 -0
- package/scripts/lib/codex-profile-config.mjs +26 -8
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
package/hub/team/session.mjs
CHANGED
|
@@ -475,6 +475,63 @@ export function attachSession(sessionName) {
|
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
/**
|
|
479
|
+
* non-TTY 환경용 attach fallback: 요청 프로세스가 명시적으로 넘긴 lead pane을
|
|
480
|
+
* 분할해 그 pane 안에서 대상 세션에 attach한다.
|
|
481
|
+
*
|
|
482
|
+
* 호출 프로세스가 pipe(non-TTY)로 실행돼 attachSession()이 불가능해도,
|
|
483
|
+
* tmux 서버에 보내는 split-window는 호출자 TTY가 필요 없다. 새 pane에는
|
|
484
|
+
* tmux가 실제 pty를 붙여주므로 그 안의 attach는 정상 동작한다.
|
|
485
|
+
* env -u TMUX는 nested-attach 보호 해제용이다.
|
|
486
|
+
*
|
|
487
|
+
* @param {string} sessionName attach할 대상 세션
|
|
488
|
+
* @param {string} targetPane 요청 lead의 `$TMUX_PANE` (`%N`)
|
|
489
|
+
* @param {string} tmuxEnv 요청 lead의 `$TMUX` (`socket,pid,index`)
|
|
490
|
+
* @returns {boolean} 분할 attach 성공 여부 (false면 호출자가 기존 생략 경로 유지)
|
|
491
|
+
*/
|
|
492
|
+
export function buildSplitAttachArgs(sessionName, targetPane, tmuxEnv) {
|
|
493
|
+
const normalizedSessionName = String(sessionName || "");
|
|
494
|
+
if (!normalizedSessionName.trim()) {
|
|
495
|
+
throw new Error("session name is required");
|
|
496
|
+
}
|
|
497
|
+
if (!/^%\d+$/u.test(String(targetPane || ""))) {
|
|
498
|
+
throw new Error("requesting lead TMUX_PANE is required");
|
|
499
|
+
}
|
|
500
|
+
const socketPath = String(tmuxEnv || "").split(",", 1)[0];
|
|
501
|
+
if (!socketPath.trim()) {
|
|
502
|
+
throw new Error("requesting lead TMUX socket is required");
|
|
503
|
+
}
|
|
504
|
+
const quotedSocket = `'${socketPath.replace(/'/gu, "'\\''")}'`;
|
|
505
|
+
const quotedSessionName = `'${normalizedSessionName.replace(/'/gu, "'\\''")}'`;
|
|
506
|
+
return [
|
|
507
|
+
"split-window",
|
|
508
|
+
"-t",
|
|
509
|
+
targetPane,
|
|
510
|
+
"-v",
|
|
511
|
+
"-l",
|
|
512
|
+
"50%",
|
|
513
|
+
`env -u TMUX tmux -S ${quotedSocket} attach -t ${quotedSessionName}`,
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function splitAttachToClient(sessionName, opts = {}) {
|
|
518
|
+
const deps = opts._deps || {};
|
|
519
|
+
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
520
|
+
if (detectMux() !== "tmux") return false;
|
|
521
|
+
|
|
522
|
+
let args;
|
|
523
|
+
try {
|
|
524
|
+
args = buildSplitAttachArgs(sessionName, opts.targetPane, opts.tmuxEnv);
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 상하 분할: 좌우 분할은 절반 폭이 워커 pane 가독 하한(100cols) 미만인 경우가 흔함
|
|
530
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
531
|
+
const split = spawn("tmux", args);
|
|
532
|
+
return (split.status ?? 1) === 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
478
535
|
/**
|
|
479
536
|
* tmux 세션 존재 확인
|
|
480
537
|
* @param {string} sessionName
|
|
@@ -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 "@triflux/core/hub/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(
|
|
@@ -111,6 +111,35 @@ function wtResultSucceeded(result) {
|
|
|
111
111
|
return result?.success !== false;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function resolveLeadPane(value) {
|
|
115
|
+
const pane = String(value || "");
|
|
116
|
+
return /^%\d+$/u.test(pane) ? pane : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveLeadWindow(tmuxExec, targetPane) {
|
|
120
|
+
try {
|
|
121
|
+
const targetWindow = String(
|
|
122
|
+
tmuxExec(
|
|
123
|
+
`display-message -p -t ${targetPane} '#{session_name}:#{window_index}'`,
|
|
124
|
+
) ?? "",
|
|
125
|
+
).trim();
|
|
126
|
+
return targetWindow || null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveLeadSocket(tmuxExec, targetPane) {
|
|
133
|
+
try {
|
|
134
|
+
const socketPath = String(
|
|
135
|
+
tmuxExec(`display-message -p -t ${targetPane} '#{socket_path}'`) ?? "",
|
|
136
|
+
).trim();
|
|
137
|
+
return socketPath || null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
export function createTerminalOpener(deps = {}) {
|
|
115
144
|
const platform = resolvePlatform(deps);
|
|
116
145
|
const tmuxExec = deps.tmuxExec || defaultTmuxExec;
|
|
@@ -134,12 +163,22 @@ export function createTerminalOpener(deps = {}) {
|
|
|
134
163
|
|
|
135
164
|
const mux = resolveMux(deps);
|
|
136
165
|
if (isTmuxLikeMux(mux, platform)) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
const targetPane = resolveLeadPane(spec.targetPane);
|
|
167
|
+
if (!targetPane) return false;
|
|
168
|
+
const targetWindow = resolveLeadWindow(tmuxExec, targetPane);
|
|
169
|
+
if (!targetWindow) return false;
|
|
170
|
+
try {
|
|
171
|
+
// tmux new-window는 pane target(%N)을 거부하므로, 요청 pane에서 해석한
|
|
172
|
+
// 정확한 session:window 뒤에 새 window를 삽입한다.
|
|
173
|
+
tmuxExec(
|
|
174
|
+
`new-window -a -t ${shellQuote(targetWindow)} -n ${shellQuote(
|
|
175
|
+
title,
|
|
176
|
+
)} ${shellQuote(buildCommandString(spec))}`,
|
|
177
|
+
);
|
|
178
|
+
return true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
143
182
|
}
|
|
144
183
|
|
|
145
184
|
if (platform === "darwin") {
|
|
@@ -174,14 +213,30 @@ export function createTerminalOpener(deps = {}) {
|
|
|
174
213
|
|
|
175
214
|
const mux = resolveMux(deps);
|
|
176
215
|
if (isTmuxLikeMux(mux, platform)) {
|
|
177
|
-
const
|
|
216
|
+
const targetPane = resolveLeadPane(opts.targetPane);
|
|
217
|
+
if (!targetPane) return false;
|
|
218
|
+
const socketPath =
|
|
219
|
+
mux === "psmux" ? null : resolveLeadSocket(tmuxExec, targetPane);
|
|
220
|
+
if (mux !== "psmux" && !socketPath) return false;
|
|
221
|
+
const attachCommand =
|
|
222
|
+
mux === "psmux"
|
|
223
|
+
? buildAttachCommand(mux, sessionName, deps)
|
|
224
|
+
: `env -u TMUX tmux -S ${shellQuote(
|
|
225
|
+
socketPath,
|
|
226
|
+
)} attach-session -t ${shellQuote(sessionName)}`;
|
|
178
227
|
if (!attachCommand) return false;
|
|
179
|
-
tmuxExec
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
const targetWindow = resolveLeadWindow(tmuxExec, targetPane);
|
|
229
|
+
if (!targetWindow) return false;
|
|
230
|
+
try {
|
|
231
|
+
tmuxExec(
|
|
232
|
+
`new-window -a -t ${shellQuote(targetWindow)} -n ${shellQuote(
|
|
233
|
+
opts.title ?? sessionName,
|
|
234
|
+
)} ${shellQuote(attachCommand)}`,
|
|
235
|
+
);
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
185
240
|
}
|
|
186
241
|
|
|
187
242
|
return false;
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
|
|
12
12
|
import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
|
|
13
13
|
import { processHandoff } from "./handoff.mjs";
|
|
14
|
+
import { getMultiplexerType } from "./psmux.mjs";
|
|
14
15
|
import { createLogDashboard } from "./tui.mjs";
|
|
15
16
|
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
16
17
|
|
|
@@ -24,23 +25,36 @@ function argVal(flag) {
|
|
|
24
25
|
const SESSION = argVal("--session");
|
|
25
26
|
const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
|
|
26
27
|
const LAYOUT = argVal("--layout") ?? "single";
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const SOURCE = argVal("--source") === "files" ? "files" : "mux";
|
|
29
|
+
const LEAD_PANE = argVal("--lead-pane");
|
|
30
|
+
const parsedWorkerCount = Number.parseInt(argVal("--workers") || "0", 10);
|
|
31
|
+
const WORKER_COUNT = Number.isFinite(parsedWorkerCount)
|
|
32
|
+
? Math.min(64, Math.max(0, parsedWorkerCount))
|
|
33
|
+
: 0;
|
|
34
|
+
const MUX_BIN = getMultiplexerType();
|
|
35
|
+
|
|
36
|
+
if (!SESSION || !/^[a-zA-Z0-9_-]+$/u.test(SESSION)) {
|
|
29
37
|
process.stderr.write(
|
|
30
38
|
"Usage: node tui-viewer.mjs --session <name> [--result-dir <dir>] [--layout <name>]\n",
|
|
31
39
|
);
|
|
32
40
|
process.exit(1);
|
|
33
41
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
37
|
-
} catch {
|
|
38
|
-
process.stderr.write(
|
|
39
|
-
`ERROR: psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
|
|
40
|
-
);
|
|
42
|
+
if (SOURCE === "files" && WORKER_COUNT === 0) {
|
|
43
|
+
process.stderr.write("ERROR: --source files requires --workers <count>\n");
|
|
41
44
|
process.exit(1);
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
if (SOURCE === "mux") {
|
|
48
|
+
try {
|
|
49
|
+
execFileSync(MUX_BIN, ["-V"], { encoding: "utf8", timeout: 2000 });
|
|
50
|
+
} catch {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`ERROR: primary multiplexer 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// ── 메모리 보호 상수 ──
|
|
45
59
|
const MAX_BODY_BYTES = 10240;
|
|
46
60
|
|
|
@@ -59,11 +73,13 @@ const tui = tuiFactory({
|
|
|
59
73
|
worker: workerName,
|
|
60
74
|
openAll: false,
|
|
61
75
|
cwd: process.cwd(),
|
|
76
|
+
targetPane: LEAD_PANE,
|
|
62
77
|
}),
|
|
63
78
|
onOpenAllWorkers: () =>
|
|
64
79
|
openHeadlessDashboardTarget(SESSION, {
|
|
65
80
|
openAll: true,
|
|
66
81
|
cwd: process.cwd(),
|
|
82
|
+
targetPane: LEAD_PANE,
|
|
67
83
|
}),
|
|
68
84
|
});
|
|
69
85
|
const startTime = Date.now();
|
|
@@ -166,9 +182,16 @@ function estimateProgress(lines, context = {}) {
|
|
|
166
182
|
|
|
167
183
|
// ── psmux 래퍼 ──
|
|
168
184
|
function listPanes() {
|
|
185
|
+
if (SOURCE === "files") {
|
|
186
|
+
return Array.from({ length: WORKER_COUNT }, (_, index) => ({
|
|
187
|
+
index: index + 1,
|
|
188
|
+
title: `worker-${index + 1} (daemon)`,
|
|
189
|
+
pid: "",
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
169
192
|
try {
|
|
170
193
|
const out = execFileSync(
|
|
171
|
-
|
|
194
|
+
MUX_BIN,
|
|
172
195
|
[
|
|
173
196
|
"list-panes",
|
|
174
197
|
"-t",
|
|
@@ -192,9 +215,35 @@ function listPanes() {
|
|
|
192
215
|
}
|
|
193
216
|
|
|
194
217
|
function capturePane(paneIdx, lines = 20) {
|
|
218
|
+
if (SOURCE === "files") {
|
|
219
|
+
const base = join(RESULT_DIR, `${SESSION}-worker-${paneIdx}.txt`);
|
|
220
|
+
const artifacts = [
|
|
221
|
+
{ path: base, label: "output" },
|
|
222
|
+
{ path: `${base}.partial`, label: "daemon" },
|
|
223
|
+
{ path: `${base}.err`, label: "stderr" },
|
|
224
|
+
];
|
|
225
|
+
const snapshots = [];
|
|
226
|
+
for (const artifact of artifacts) {
|
|
227
|
+
try {
|
|
228
|
+
const content = readFileSync(artifact.path, "utf8").trim();
|
|
229
|
+
if (!content) continue;
|
|
230
|
+
snapshots.push(
|
|
231
|
+
`[${artifact.label}]\n${content.split("\n").slice(-lines).join("\n")}`,
|
|
232
|
+
);
|
|
233
|
+
} catch {
|
|
234
|
+
/* artifact is optional while the daemon job is starting */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return (
|
|
238
|
+
snapshots.join("\n") ||
|
|
239
|
+
`worker-${paneIdx} daemon active · waiting for output · ${Math.round(
|
|
240
|
+
(Date.now() - startTime) / 1000,
|
|
241
|
+
)}s`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
195
244
|
try {
|
|
196
245
|
return execFileSync(
|
|
197
|
-
|
|
246
|
+
MUX_BIN,
|
|
198
247
|
["capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p"],
|
|
199
248
|
{ encoding: "utf8", timeout: 2000 },
|
|
200
249
|
)
|
|
@@ -368,7 +417,7 @@ function ingest() {
|
|
|
368
417
|
const tokens = extractTokenLabel(snapshot);
|
|
369
418
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
370
419
|
|
|
371
|
-
if (resultSize > 10 || shellReturned) {
|
|
420
|
+
if (SOURCE !== "files" && (resultSize > 10 || shellReturned)) {
|
|
372
421
|
const resultContent = existsSync(resultFile)
|
|
373
422
|
? readFileSync(resultFile, "utf8")
|
|
374
423
|
: snapshot;
|
|
@@ -410,8 +459,10 @@ function ingest() {
|
|
|
410
459
|
// 진행 중
|
|
411
460
|
const progress = estimateProgress(lines, {
|
|
412
461
|
tokens,
|
|
413
|
-
|
|
414
|
-
|
|
462
|
+
// daemon output files grow while the job is still running; file size is
|
|
463
|
+
// activity evidence, not a completion signal in read-only file mode.
|
|
464
|
+
resultSize: SOURCE === "files" ? 0 : resultSize,
|
|
465
|
+
shellReturned: SOURCE === "files" ? false : shellReturned,
|
|
415
466
|
done: false,
|
|
416
467
|
});
|
|
417
468
|
const verdict = lastLine;
|
package/hub/tools.mjs
CHANGED
|
@@ -159,8 +159,13 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
159
159
|
transport_locators_json: {
|
|
160
160
|
description:
|
|
161
161
|
"tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
|
|
162
|
-
oneOf: [
|
|
162
|
+
oneOf: [
|
|
163
|
+
{ type: "array", items: { type: "object" } },
|
|
164
|
+
{ type: "string" },
|
|
165
|
+
],
|
|
163
166
|
},
|
|
167
|
+
hub_instance_id: { type: "string" },
|
|
168
|
+
store_fingerprint: { type: "string" },
|
|
164
169
|
heartbeat_ttl_ms: {
|
|
165
170
|
type: "integer",
|
|
166
171
|
minimum: 10000,
|
|
@@ -186,6 +191,8 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
186
191
|
agent_id: { type: "string" },
|
|
187
192
|
trace_id: { type: "string" },
|
|
188
193
|
include_metrics: { type: "boolean", default: true },
|
|
194
|
+
hub_instance_id: { type: "string" },
|
|
195
|
+
store_fingerprint: { type: "string" },
|
|
189
196
|
},
|
|
190
197
|
},
|
|
191
198
|
handler: wrap("STATUS_FAILED", (args) => {
|
|
@@ -206,6 +213,8 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
206
213
|
agent_id: { type: "string", pattern: "^[a-zA-Z0-9._:-]{3,64}$" },
|
|
207
214
|
reason: { type: "string" },
|
|
208
215
|
requested_by: { type: "string" },
|
|
216
|
+
hub_instance_id: { type: "string" },
|
|
217
|
+
store_fingerprint: { type: "string" },
|
|
209
218
|
},
|
|
210
219
|
},
|
|
211
220
|
handler: wrap("TAKEOVER_ROLE_FAILED", (args) => {
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
9
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
10
10
|
|
|
11
|
+
import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
|
|
11
12
|
import {
|
|
12
13
|
CODEX_MCP_EXECUTION_EXIT_CODE,
|
|
13
14
|
CODEX_MCP_TRANSPORT_EXIT_CODE,
|
|
@@ -134,11 +135,13 @@ function readTomlScalar(raw, key) {
|
|
|
134
135
|
export function resolveCodexProfileConfig(profileName) {
|
|
135
136
|
const result = { model: null, reasoningEffort: null };
|
|
136
137
|
if (typeof profileName !== "string" || !profileName) return result;
|
|
138
|
+
const normalizedProfileName = normalizeCodexProfileName(profileName);
|
|
139
|
+
if (!normalizedProfileName) return result;
|
|
137
140
|
|
|
138
141
|
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
139
142
|
try {
|
|
140
143
|
const raw = readFileSync(
|
|
141
|
-
join(codexHome, `${
|
|
144
|
+
join(codexHome, `${normalizedProfileName}.config.toml`),
|
|
142
145
|
"utf8",
|
|
143
146
|
);
|
|
144
147
|
result.model = readTomlScalar(raw, "model");
|
|
@@ -146,7 +149,9 @@ export function resolveCodexProfileConfig(profileName) {
|
|
|
146
149
|
} catch {
|
|
147
150
|
// Profile file absent (non-file alias). Derive effort from the naming
|
|
148
151
|
// convention; leave model unset so codex uses its config.toml default.
|
|
149
|
-
const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
|
|
152
|
+
const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
|
|
153
|
+
normalizedProfileName,
|
|
154
|
+
);
|
|
150
155
|
if (suffix) {
|
|
151
156
|
const map = {
|
|
152
157
|
ultra: "ultra",
|
|
@@ -198,7 +203,7 @@ export function buildCodexArguments(prompt, opts = {}) {
|
|
|
198
203
|
const profileDerivedUltra =
|
|
199
204
|
resolved.reasoningEffort?.toLowerCase() === "ultra";
|
|
200
205
|
if (typeof args.model !== "string") {
|
|
201
|
-
if (profileDerivedUltra) args.model = "gpt-
|
|
206
|
+
if (profileDerivedUltra) args.model = "gpt-6-astra";
|
|
202
207
|
else if (resolved.model) args.model = resolved.model;
|
|
203
208
|
}
|
|
204
209
|
if (resolved.reasoningEffort) {
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
11
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import * as z from "zod";
|
|
14
|
-
import { resolveCodexAgentPolicy } from "
|
|
14
|
+
import { resolveCodexAgentPolicy } from "../../scripts/lib/agent-route-policy.mjs";
|
|
15
15
|
import { resolveBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
|
|
16
16
|
import { runHeadlessWithCleanup } from "../team/headless.mjs";
|
|
17
17
|
import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
|