@triflux/remote 10.53.0 → 10.55.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/team/backend.mjs +10 -3
- package/hub/team/cli/commands/start/index.mjs +0 -2
- package/hub/team/handoff.mjs +1 -1
- package/hub/team/headless.mjs +41 -57
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +31 -18
- package/hub/team/psmux.mjs +36 -20
- package/hub/team/remote-session.mjs +66 -56
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +80 -59
- package/hub/team/terminal-opener.mjs +5 -19
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +0 -6
- package/hub/workers/factory.mjs +1 -10
- package/hub/workers/interface.mjs +0 -7
- package/package.json +2 -6
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/cto/brief.mjs +0 -73
- package/cto/collect.mjs +0 -490
- package/cto/current.schema.json +0 -148
- package/cto/events.mjs +0 -208
- package/cto/hygiene.mjs +0 -320
- package/cto/lake-root.mjs +0 -127
- package/cto/status.mjs +0 -124
- package/hub/lib/cto-env.mjs +0 -1
- package/hub/team/notify.mjs +0 -385
- package/hub/team/process-cleanup.mjs +0 -386
- package/hub/team/routing.mjs +0 -249
- package/hub/team/staleState.mjs +0 -403
- package/hub/team/worker-signal.mjs +0 -263
- package/hub/team/worker-signal.types.d.ts +0 -52
- package/hub/workers/codex-mcp.mjs +0 -853
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
// hub/team/process-cleanup.mjs — 고아 node/python 프로세스 감지 및 정리
|
|
2
|
-
// Windows: Get-CimInstance Win32_Process로 parent PID + cmdLine 접근
|
|
3
|
-
// Unix: ps aux 파싱
|
|
4
|
-
import { execFileSync, execFile as nodeExecFile } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
|
|
7
|
-
|
|
8
|
-
const execFileAsync = promisify(nodeExecFile);
|
|
9
|
-
|
|
10
|
-
// ── 상수 ────────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const TARGET_PROCESS_NAMES = ["node", "python", "python3"];
|
|
13
|
-
const SIGTERM_GRACE_MS = 5000;
|
|
14
|
-
|
|
15
|
-
export function forceKillPid(
|
|
16
|
-
pid,
|
|
17
|
-
{
|
|
18
|
-
isWindows = IS_WINDOWS,
|
|
19
|
-
execFileSyncFn = execFileSync,
|
|
20
|
-
killFn = process.kill,
|
|
21
|
-
} = {},
|
|
22
|
-
) {
|
|
23
|
-
if (isWindows) {
|
|
24
|
-
try {
|
|
25
|
-
execFileSyncFn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
26
|
-
stdio: "ignore",
|
|
27
|
-
timeout: 5000,
|
|
28
|
-
windowsHide: true,
|
|
29
|
-
});
|
|
30
|
-
return;
|
|
31
|
-
} catch (taskkillError) {
|
|
32
|
-
try {
|
|
33
|
-
killFn(pid);
|
|
34
|
-
return;
|
|
35
|
-
} catch {
|
|
36
|
-
throw taskkillError;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
killFn(pid, "SIGKILL");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// cmdLine 패턴 기반 화이트리스트 (고아 후보에서 제외)
|
|
45
|
-
const WHITELIST_CMDLINE = [/oh-my-claudecode/i, /triflux[\\/]hub[\\/]s/i];
|
|
46
|
-
|
|
47
|
-
// 프로세스명 기반 화이트리스트
|
|
48
|
-
const WHITELIST_NAMES = ["claude", "CCXProcess"];
|
|
49
|
-
|
|
50
|
-
// ── PowerShell / ps 파싱 ─────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Windows: Get-CimInstance Win32_Process로 프로세스 목록 조회
|
|
54
|
-
* @param {Function} execFileFn - DI용 execFile 구현
|
|
55
|
-
* @returns {Promise<Array<{pid,name,parentPid,cmdLine,ramMB}>>}
|
|
56
|
-
*/
|
|
57
|
-
async function queryWindowsProcesses(execFileFn) {
|
|
58
|
-
const script = [
|
|
59
|
-
"Get-CimInstance Win32_Process |",
|
|
60
|
-
" Select-Object ProcessId,Name,ParentProcessId,CommandLine,WorkingSetSize |",
|
|
61
|
-
" ConvertTo-Json -Compress",
|
|
62
|
-
].join(" ");
|
|
63
|
-
|
|
64
|
-
const { stdout } = await execFileFn(
|
|
65
|
-
"pwsh",
|
|
66
|
-
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
67
|
-
{ encoding: "utf8", timeout: 15_000, windowsHide: true },
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const raw = JSON.parse(stdout.trim());
|
|
71
|
-
const items = Array.isArray(raw) ? raw : [raw];
|
|
72
|
-
|
|
73
|
-
return items.filter(Boolean).map((p) => ({
|
|
74
|
-
pid: Number(p.ProcessId),
|
|
75
|
-
name: String(p.Name || "").replace(/\.exe$/i, ""),
|
|
76
|
-
parentPid: Number(p.ParentProcessId) || 0,
|
|
77
|
-
cmdLine: String(p.CommandLine || ""),
|
|
78
|
-
ramMB: Math.round((Number(p.WorkingSetSize) || 0) / 1024 / 1024),
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Unix: ps aux 파싱
|
|
84
|
-
* @param {Function} execFileFn - DI용 execFile 구현
|
|
85
|
-
* @returns {Promise<Array<{pid,name,parentPid,cmdLine,ramMB}>>}
|
|
86
|
-
*/
|
|
87
|
-
async function queryUnixProcesses(execFileFn) {
|
|
88
|
-
const { stdout } = await execFileFn(
|
|
89
|
-
"ps",
|
|
90
|
-
["-eo", "pid=,ppid=,rss=,comm=,args="],
|
|
91
|
-
{ encoding: "utf8", timeout: 10_000 },
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return stdout
|
|
95
|
-
.trim()
|
|
96
|
-
.split("\n")
|
|
97
|
-
.filter(Boolean)
|
|
98
|
-
.map((line) => {
|
|
99
|
-
const parts = line.trim().split(/\s+/);
|
|
100
|
-
const pid = Number(parts[0]);
|
|
101
|
-
const parentPid = Number(parts[1]);
|
|
102
|
-
const ramMB = Math.round((Number(parts[2]) || 0) / 1024);
|
|
103
|
-
const name = String(parts[3] || "");
|
|
104
|
-
const cmdLine = parts.slice(4).join(" ");
|
|
105
|
-
return { pid, name, parentPid, cmdLine, ramMB };
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 프로세스 시작 시각을 Unix 기준 ms로 조회 (Windows 전용, best-effort)
|
|
111
|
-
* @param {number} pid
|
|
112
|
-
* @param {Function} execFileFn
|
|
113
|
-
* @returns {Promise<number>} epoch ms, 실패 시 0
|
|
114
|
-
*/
|
|
115
|
-
async function getWindowsProcessStartMs(pid, execFileFn) {
|
|
116
|
-
try {
|
|
117
|
-
const script = `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate | Get-Date -Format 'o'`;
|
|
118
|
-
const { stdout } = await execFileFn(
|
|
119
|
-
"pwsh",
|
|
120
|
-
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
121
|
-
{ encoding: "utf8", timeout: 5_000, windowsHide: true },
|
|
122
|
-
);
|
|
123
|
-
const ts = Date.parse(stdout.trim());
|
|
124
|
-
return Number.isNaN(ts) ? 0 : ts;
|
|
125
|
-
} catch {
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── 화이트리스트 판정 ──────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 프로세스가 화이트리스트에 해당하는지 판정
|
|
134
|
-
* @param {{name:string, cmdLine:string}} proc
|
|
135
|
-
* @param {Set<number>} parentPids - 화이트리스트 부모 PID 집합
|
|
136
|
-
* @returns {boolean}
|
|
137
|
-
*/
|
|
138
|
-
function isWhitelisted(proc, parentPids) {
|
|
139
|
-
const nameLower = proc.name.toLowerCase();
|
|
140
|
-
|
|
141
|
-
// 프로세스명 기반
|
|
142
|
-
if (WHITELIST_NAMES.some((n) => nameLower.includes(n.toLowerCase()))) {
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// cmdLine 패턴 기반
|
|
147
|
-
if (WHITELIST_CMDLINE.some((re) => re.test(proc.cmdLine))) {
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// CCXProcess 자식 (Adobe Creative Cloud)
|
|
152
|
-
if (parentPids.has(proc.parentPid)) {
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── psmux 교차검증 ──────────────────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* psmux list-sessions 결과에서 활성 세션 pane PID 집합을 수집
|
|
163
|
-
* @param {Function} execFileFn
|
|
164
|
-
* @returns {Promise<Set<number>>}
|
|
165
|
-
*/
|
|
166
|
-
async function getActivePsmuxPids(execFileFn) {
|
|
167
|
-
try {
|
|
168
|
-
const { stdout: sessOut } = await execFileFn(
|
|
169
|
-
"psmux",
|
|
170
|
-
["list-sessions", "-F", "#{session_name}"],
|
|
171
|
-
{ encoding: "utf8", timeout: 5_000, windowsHide: true },
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const sessions = sessOut.trim().split(/\r?\n/).filter(Boolean);
|
|
175
|
-
if (sessions.length === 0) return new Set();
|
|
176
|
-
|
|
177
|
-
const pids = new Set();
|
|
178
|
-
|
|
179
|
-
await Promise.all(
|
|
180
|
-
sessions.map(async (session) => {
|
|
181
|
-
try {
|
|
182
|
-
const { stdout: paneOut } = await execFileFn(
|
|
183
|
-
"psmux",
|
|
184
|
-
["list-panes", "-t", session, "-a", "-F", "#{pane_pid}"],
|
|
185
|
-
{ encoding: "utf8", timeout: 5_000, windowsHide: true },
|
|
186
|
-
);
|
|
187
|
-
paneOut
|
|
188
|
-
.trim()
|
|
189
|
-
.split(/\r?\n/)
|
|
190
|
-
.filter(Boolean)
|
|
191
|
-
.forEach((p) => {
|
|
192
|
-
const n = Number(p.trim());
|
|
193
|
-
if (n > 0) pids.add(n);
|
|
194
|
-
});
|
|
195
|
-
} catch {
|
|
196
|
-
// 세션 쿼리 실패 시 해당 세션만 건너뜀
|
|
197
|
-
}
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
return pids;
|
|
202
|
-
} catch {
|
|
203
|
-
// psmux 미설치 또는 실패 시 빈 집합 반환
|
|
204
|
-
return new Set();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── 공개 API ────────────────────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 고아 프로세스 목록을 반환한다.
|
|
212
|
-
*
|
|
213
|
-
* @param {object} [opts]
|
|
214
|
-
* @param {Function} [opts.execFileFn] - DI용 execFile (기본: node:child_process.execFile의 promisify 버전)
|
|
215
|
-
* @param {boolean} [opts.skipPsmuxCheck] - psmux 교차검증 생략 (테스트용)
|
|
216
|
-
* @returns {Promise<Array<{pid,name,ramMB,parentPid,cmdLine,age}>>}
|
|
217
|
-
*/
|
|
218
|
-
export async function findOrphanProcesses(opts = {}) {
|
|
219
|
-
const execFileFn = opts.execFileFn ?? execFileAsync;
|
|
220
|
-
|
|
221
|
-
let allProcs;
|
|
222
|
-
try {
|
|
223
|
-
allProcs = IS_WINDOWS
|
|
224
|
-
? await queryWindowsProcesses(execFileFn)
|
|
225
|
-
: await queryUnixProcesses(execFileFn);
|
|
226
|
-
} catch {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (allProcs.length === 0) return [];
|
|
231
|
-
|
|
232
|
-
const pidSet = new Set(allProcs.map((p) => p.pid));
|
|
233
|
-
|
|
234
|
-
// CCXProcess 부모 PID 집합 구성
|
|
235
|
-
const ccxParentPids = new Set(
|
|
236
|
-
allProcs
|
|
237
|
-
.filter((p) => p.name.toLowerCase() === "ccxprocess")
|
|
238
|
-
.map((p) => p.pid),
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
// 활성 psmux PID 교차검증
|
|
242
|
-
const activePsmuxPids = opts.skipPsmuxCheck
|
|
243
|
-
? new Set()
|
|
244
|
-
: await getActivePsmuxPids(execFileFn);
|
|
245
|
-
|
|
246
|
-
const now = Date.now();
|
|
247
|
-
|
|
248
|
-
const candidates = allProcs.filter((p) => {
|
|
249
|
-
const nameLower = p.name.toLowerCase();
|
|
250
|
-
|
|
251
|
-
// 대상 프로세스만
|
|
252
|
-
if (
|
|
253
|
-
!TARGET_PROCESS_NAMES.some(
|
|
254
|
-
(t) => nameLower === t || nameLower === `${t}.exe`,
|
|
255
|
-
)
|
|
256
|
-
) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 화이트리스트
|
|
261
|
-
if (isWhitelisted(p, ccxParentPids)) return false;
|
|
262
|
-
|
|
263
|
-
// 활성 psmux 세션 소속 PID 제외
|
|
264
|
-
if (activePsmuxPids.has(p.pid)) return false;
|
|
265
|
-
|
|
266
|
-
// 부모가 없거나(0, 1 제외 시 존재하지 않는 PID) 고아
|
|
267
|
-
const parentAlive = p.parentPid <= 1 || pidSet.has(p.parentPid);
|
|
268
|
-
if (parentAlive) return false;
|
|
269
|
-
|
|
270
|
-
return true;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// age 계산 (Windows만 best-effort, Unix는 0)
|
|
274
|
-
const results = await Promise.all(
|
|
275
|
-
candidates.map(async (p) => {
|
|
276
|
-
let startMs = 0;
|
|
277
|
-
if (IS_WINDOWS) {
|
|
278
|
-
startMs = await getWindowsProcessStartMs(p.pid, execFileFn);
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
pid: p.pid,
|
|
282
|
-
name: p.name,
|
|
283
|
-
ramMB: p.ramMB,
|
|
284
|
-
parentPid: p.parentPid,
|
|
285
|
-
cmdLine: p.cmdLine,
|
|
286
|
-
age: startMs > 0 ? now - startMs : 0,
|
|
287
|
-
};
|
|
288
|
-
}),
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
return results;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* createProcessCleanup — scan/kill/getOrphans 인터페이스를 반환한다.
|
|
296
|
-
*
|
|
297
|
-
* @param {object} [opts]
|
|
298
|
-
* @param {Function} [opts.execFileFn] - DI용 execFile (promisify된 버전)
|
|
299
|
-
* @param {boolean} [opts.dryRun] - true이면 kill 없이 목록만 반환
|
|
300
|
-
* @param {boolean} [opts.skipPsmuxCheck] - psmux 교차검증 생략 (테스트용)
|
|
301
|
-
* @returns {{ scan: Function, kill: Function, getOrphans: Function }}
|
|
302
|
-
*/
|
|
303
|
-
export function createProcessCleanup(opts = {}) {
|
|
304
|
-
const execFileFn = opts.execFileFn ?? execFileAsync;
|
|
305
|
-
const dryRun = opts.dryRun ?? false;
|
|
306
|
-
const isWindows = opts.isWindows ?? IS_WINDOWS;
|
|
307
|
-
const execFileSyncFn = opts.execFileSyncFn ?? execFileSync;
|
|
308
|
-
const killFn = opts.killFn ?? process.kill;
|
|
309
|
-
const sleepFn =
|
|
310
|
-
opts.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
311
|
-
const sigtermGraceMs = opts.sigtermGraceMs ?? SIGTERM_GRACE_MS;
|
|
312
|
-
|
|
313
|
-
let lastOrphans = [];
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* 고아 프로세스를 스캔하여 내부 상태에 저장하고 목록을 반환한다.
|
|
317
|
-
* @returns {Promise<Array<{pid,name,ramMB,parentPid,cmdLine,age}>>}
|
|
318
|
-
*/
|
|
319
|
-
async function scan() {
|
|
320
|
-
const found = await findOrphanProcesses({
|
|
321
|
-
execFileFn,
|
|
322
|
-
skipPsmuxCheck: opts.skipPsmuxCheck,
|
|
323
|
-
});
|
|
324
|
-
lastOrphans = found;
|
|
325
|
-
return found;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* 마지막 scan 결과의 프로세스를 kill한다.
|
|
330
|
-
* dryRun=true이면 kill 없이 목록만 반환한다.
|
|
331
|
-
* SIGTERM → 5s 대기 → 강제 종료(taskkill/SIGKILL) 순서.
|
|
332
|
-
* @returns {Promise<Array<{pid,name,killed,error}>>}
|
|
333
|
-
*/
|
|
334
|
-
async function kill() {
|
|
335
|
-
if (dryRun) {
|
|
336
|
-
return lastOrphans.map((p) => ({
|
|
337
|
-
pid: p.pid,
|
|
338
|
-
name: p.name,
|
|
339
|
-
killed: false,
|
|
340
|
-
dryRun: true,
|
|
341
|
-
}));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const results = await Promise.all(
|
|
345
|
-
lastOrphans.map(async (p) => {
|
|
346
|
-
try {
|
|
347
|
-
// SIGTERM
|
|
348
|
-
killFn(p.pid, "SIGTERM");
|
|
349
|
-
|
|
350
|
-
// 5초 대기 후 살아있으면 강제 종료
|
|
351
|
-
await sleepFn(sigtermGraceMs);
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
// 프로세스가 아직 살아있는지 확인 (signal 0)
|
|
355
|
-
killFn(p.pid, 0);
|
|
356
|
-
// 여전히 살아있음 → Windows는 taskkill/process.kill, 그 외는 SIGKILL
|
|
357
|
-
forceKillPid(p.pid, { isWindows, execFileSyncFn, killFn });
|
|
358
|
-
} catch {
|
|
359
|
-
// ESRCH: 이미 종료됨 — 정상
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return { pid: p.pid, name: p.name, killed: true };
|
|
363
|
-
} catch (err) {
|
|
364
|
-
return {
|
|
365
|
-
pid: p.pid,
|
|
366
|
-
name: p.name,
|
|
367
|
-
killed: false,
|
|
368
|
-
error: String(err.message || err),
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
}),
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
return results;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* 마지막 scan 결과를 반환한다 (재스캔 없음).
|
|
379
|
-
* @returns {Array<{pid,name,ramMB,parentPid,cmdLine,age}>}
|
|
380
|
-
*/
|
|
381
|
-
function getOrphans() {
|
|
382
|
-
return lastOrphans;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return { scan, kill, getOrphans };
|
|
386
|
-
}
|
package/hub/team/routing.mjs
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 라우팅 결정 함수
|
|
3
|
-
* @param {object} opts
|
|
4
|
-
* @param {Array<{id:string, description?:string, agent?:string, depends_on?:string[], complexity?:string}>} opts.subtasks
|
|
5
|
-
* @param {string} opts.graph_type - "INDEPENDENT" | "SEQUENTIAL" | "DAG"
|
|
6
|
-
* @param {boolean} opts.thorough - thorough 모드 여부
|
|
7
|
-
* @returns {{
|
|
8
|
-
* strategy: "quick_single" | "thorough_single" | "quick_team" | "thorough_team" | "batch_single",
|
|
9
|
-
* reason: string,
|
|
10
|
-
* dag_width: number,
|
|
11
|
-
* max_complexity: string,
|
|
12
|
-
* dagContext: { dag_width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}>, max_complexity: string, taskResults: Record<string, *> }
|
|
13
|
-
* }}
|
|
14
|
-
*/
|
|
15
|
-
export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
|
|
16
|
-
const N = subtasks.length;
|
|
17
|
-
if (N === 0) {
|
|
18
|
-
const dagContext = {
|
|
19
|
-
dag_width: 0,
|
|
20
|
-
levels: {},
|
|
21
|
-
edges: [],
|
|
22
|
-
max_complexity: "S",
|
|
23
|
-
taskResults: {},
|
|
24
|
-
};
|
|
25
|
-
return {
|
|
26
|
-
strategy: "quick_single",
|
|
27
|
-
reason: "empty_subtasks",
|
|
28
|
-
dag_width: 0,
|
|
29
|
-
max_complexity: "S",
|
|
30
|
-
dagContext,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const {
|
|
35
|
-
width: dag_width,
|
|
36
|
-
levels,
|
|
37
|
-
edges,
|
|
38
|
-
} = computeDagInfo(subtasks, graph_type);
|
|
39
|
-
const max_complexity = getMaxComplexity(subtasks);
|
|
40
|
-
const dagContext = {
|
|
41
|
-
dag_width,
|
|
42
|
-
levels,
|
|
43
|
-
edges,
|
|
44
|
-
max_complexity,
|
|
45
|
-
taskResults: {},
|
|
46
|
-
};
|
|
47
|
-
const isHighComplexity = ["L", "XL"].includes(max_complexity);
|
|
48
|
-
const allSameAgent = new Set(subtasks.map((s) => s.agent)).size === 1;
|
|
49
|
-
const allSmall = subtasks.every(
|
|
50
|
-
(s) => normalizeComplexity(s.complexity) === "S",
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// N==1: 단일 태스크
|
|
54
|
-
if (N === 1) {
|
|
55
|
-
if (thorough || isHighComplexity) {
|
|
56
|
-
return {
|
|
57
|
-
strategy: "thorough_single",
|
|
58
|
-
reason: "single_high_complexity",
|
|
59
|
-
dag_width,
|
|
60
|
-
max_complexity,
|
|
61
|
-
dagContext,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
strategy: "quick_single",
|
|
66
|
-
reason: "single_low_complexity",
|
|
67
|
-
dag_width,
|
|
68
|
-
max_complexity,
|
|
69
|
-
dagContext,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// dag_width==1: 사실상 순차 -> single
|
|
74
|
-
if (dag_width === 1) {
|
|
75
|
-
if (thorough || isHighComplexity) {
|
|
76
|
-
return {
|
|
77
|
-
strategy: "thorough_single",
|
|
78
|
-
reason: "sequential_chain",
|
|
79
|
-
dag_width,
|
|
80
|
-
max_complexity,
|
|
81
|
-
dagContext,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
strategy: "quick_single",
|
|
86
|
-
reason: "sequential_chain",
|
|
87
|
-
dag_width,
|
|
88
|
-
max_complexity,
|
|
89
|
-
dagContext,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 동일 에이전트 + 모두 S: 프롬프트 병합 -> batch single
|
|
94
|
-
if (allSameAgent && allSmall) {
|
|
95
|
-
return {
|
|
96
|
-
strategy: "batch_single",
|
|
97
|
-
reason: "same_agent_small_batch",
|
|
98
|
-
dag_width,
|
|
99
|
-
max_complexity,
|
|
100
|
-
dagContext,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// dag_width >= 2: 팀
|
|
105
|
-
if (thorough || isHighComplexity) {
|
|
106
|
-
return {
|
|
107
|
-
strategy: "thorough_team",
|
|
108
|
-
reason: "parallel_high_complexity",
|
|
109
|
-
dag_width,
|
|
110
|
-
max_complexity,
|
|
111
|
-
dagContext,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
strategy: "quick_team",
|
|
116
|
-
reason: "parallel_low_complexity",
|
|
117
|
-
dag_width,
|
|
118
|
-
max_complexity,
|
|
119
|
-
dagContext,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* DAG 정보 계산 — 레벨별 태스크 배열, 간선, 최대 폭
|
|
125
|
-
* @param {Array<{id:string, depends_on?:string[]}>} subtasks
|
|
126
|
-
* @param {string} graph_type
|
|
127
|
-
* @returns {{ width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}> }}
|
|
128
|
-
*/
|
|
129
|
-
function computeDagInfo(subtasks, graph_type) {
|
|
130
|
-
if (graph_type === "SEQUENTIAL") {
|
|
131
|
-
const levels = {};
|
|
132
|
-
const edges = [];
|
|
133
|
-
subtasks.forEach((t, i) => {
|
|
134
|
-
levels[i] = [t.id];
|
|
135
|
-
if (i > 0) edges.push({ from: subtasks[i - 1].id, to: t.id });
|
|
136
|
-
});
|
|
137
|
-
return { width: 1, levels, edges };
|
|
138
|
-
}
|
|
139
|
-
if (graph_type === "INDEPENDENT") {
|
|
140
|
-
const levels = { 0: subtasks.map((t) => t.id) };
|
|
141
|
-
return { width: subtasks.length, levels, edges: [] };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// DAG: 레벨별 계산 (순환 의존 방어)
|
|
145
|
-
const taskLevels = {};
|
|
146
|
-
const visiting = new Set();
|
|
147
|
-
|
|
148
|
-
function getLevel(task) {
|
|
149
|
-
if (taskLevels[task.id] !== undefined) return taskLevels[task.id];
|
|
150
|
-
if (visiting.has(task.id)) {
|
|
151
|
-
taskLevels[task.id] = 0; // 순환 끊기
|
|
152
|
-
return 0;
|
|
153
|
-
}
|
|
154
|
-
if (!task.depends_on || task.depends_on.length === 0) {
|
|
155
|
-
taskLevels[task.id] = 0;
|
|
156
|
-
return 0;
|
|
157
|
-
}
|
|
158
|
-
visiting.add(task.id);
|
|
159
|
-
const depLevels = task.depends_on.map((depId) => {
|
|
160
|
-
const dep = subtasks.find((s) => s.id === depId);
|
|
161
|
-
return dep ? getLevel(dep) : 0;
|
|
162
|
-
});
|
|
163
|
-
visiting.delete(task.id);
|
|
164
|
-
taskLevels[task.id] = Math.max(...depLevels) + 1;
|
|
165
|
-
return taskLevels[task.id];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
subtasks.forEach(getLevel);
|
|
169
|
-
|
|
170
|
-
// 레벨별 태스크 그룹핑
|
|
171
|
-
const levels = {};
|
|
172
|
-
for (const [id, level] of Object.entries(taskLevels)) {
|
|
173
|
-
if (!levels[level]) levels[level] = [];
|
|
174
|
-
levels[level].push(id);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 간선 수집
|
|
178
|
-
const edges = [];
|
|
179
|
-
for (const task of subtasks) {
|
|
180
|
-
if (task.depends_on) {
|
|
181
|
-
for (const depId of task.depends_on) {
|
|
182
|
-
edges.push({ from: depId, to: task.id });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const width = Math.max(...Object.values(levels).map((arr) => arr.length), 1);
|
|
188
|
-
return { width, levels, edges };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 선행 태스크의 결과를 dagContext.edges에서 조회하여 반환
|
|
193
|
-
* @param {string} taskId - 조회 대상 태스크 ID
|
|
194
|
-
* @param {{ dagContext?: { edges: Array<{from:string, to:string}>, taskResults: Record<string, *> } }} pipelineState
|
|
195
|
-
* @returns {Record<string, *>} 선행 태스크 ID → 결과 매핑
|
|
196
|
-
*/
|
|
197
|
-
export function getUpstreamResults(taskId, pipelineState) {
|
|
198
|
-
const ctx = pipelineState?.dagContext;
|
|
199
|
-
if (!ctx) return {};
|
|
200
|
-
const upstreamIds = ctx.edges
|
|
201
|
-
.filter((e) => e.to === taskId)
|
|
202
|
-
.map((e) => e.from);
|
|
203
|
-
const results = {};
|
|
204
|
-
for (const id of upstreamIds) {
|
|
205
|
-
if (id in (ctx.taskResults || {})) {
|
|
206
|
-
results[id] = ctx.taskResults[id];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return results;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 태스크 완료 시 결과를 dagContext에 기록
|
|
214
|
-
* @param {string} taskId - 완료된 태스크 ID
|
|
215
|
-
* @param {*} result - 태스크 결과
|
|
216
|
-
* @param {{ dagContext?: { taskResults: Record<string, *> } }} pipelineState
|
|
217
|
-
* @returns {boolean} 기록 성공 여부
|
|
218
|
-
*/
|
|
219
|
-
export function updateTaskResult(taskId, result, pipelineState) {
|
|
220
|
-
const ctx = pipelineState?.dagContext;
|
|
221
|
-
if (!ctx) return false;
|
|
222
|
-
if (!ctx.taskResults) ctx.taskResults = {};
|
|
223
|
-
ctx.taskResults[taskId] = result;
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 최대 복잡도 추출
|
|
229
|
-
* @param {Array<{complexity?:string}>} subtasks
|
|
230
|
-
* @returns {"S" | "M" | "L" | "XL"}
|
|
231
|
-
*/
|
|
232
|
-
function getMaxComplexity(subtasks) {
|
|
233
|
-
const order = { S: 0, M: 1, L: 2, XL: 3 };
|
|
234
|
-
let max = "S";
|
|
235
|
-
for (const s of subtasks) {
|
|
236
|
-
const complexity = normalizeComplexity(s.complexity);
|
|
237
|
-
if (order[complexity] > order[max]) max = complexity;
|
|
238
|
-
}
|
|
239
|
-
return max;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* complexity 기본값 보정
|
|
244
|
-
* @param {string | undefined} complexity
|
|
245
|
-
* @returns {"S" | "M" | "L" | "XL"}
|
|
246
|
-
*/
|
|
247
|
-
function normalizeComplexity(complexity) {
|
|
248
|
-
return ["S", "M", "L", "XL"].includes(complexity) ? complexity : "M";
|
|
249
|
-
}
|