@triflux/remote 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.
Files changed (134) hide show
  1. package/cto/brief.mjs +0 -20
  2. package/cto/collect.mjs +1 -200
  3. package/cto/current.schema.json +2 -23
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -187
  7. package/hub/index.mjs +0 -17
  8. package/hub/team/agent-map.json +0 -1
  9. package/hub/team/agents-row-attach.py +120 -0
  10. package/hub/team/agents-row.mjs +200 -0
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/claude-daemon-control.mjs +1 -2
  14. package/hub/team/claude-session-projection.mjs +5 -73
  15. package/hub/team/cli/commands/attach.mjs +1 -17
  16. package/hub/team/cli/commands/control.mjs +12 -38
  17. package/hub/team/cli/commands/debug.mjs +2 -39
  18. package/hub/team/cli/commands/focus.mjs +5 -31
  19. package/hub/team/cli/commands/interrupt.mjs +6 -25
  20. package/hub/team/cli/commands/kill.mjs +1 -30
  21. package/hub/team/cli/commands/list.mjs +0 -16
  22. package/hub/team/cli/commands/send.mjs +6 -26
  23. package/hub/team/cli/commands/start/index.mjs +42 -133
  24. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  25. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  26. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  27. package/hub/team/cli/commands/status.mjs +2 -57
  28. package/hub/team/cli/commands/stop.mjs +14 -19
  29. package/hub/team/cli/help.mjs +4 -5
  30. package/hub/team/cli/manifest.mjs +5 -1
  31. package/hub/team/cli/render.mjs +0 -12
  32. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  33. package/hub/team/cli/services/state-store.mjs +5 -20
  34. package/hub/team/handoff.mjs +3 -34
  35. package/hub/team/headless.mjs +108 -1296
  36. package/hub/team/orchestrator.mjs +4 -30
  37. package/hub/team/pane.mjs +6 -28
  38. package/hub/team/psmux.mjs +3 -34
  39. package/hub/team/remote-session.mjs +1 -2
  40. package/hub/team/session-context.mjs +1 -1
  41. package/hub/team/session.mjs +7 -91
  42. package/hub/team/tui-core.mjs +1 -1
  43. package/hub/team/tui-viewer.mjs +11 -58
  44. package/hub/team/tui.mjs +8 -306
  45. package/hub/team/uds-orchestrator.mjs +2 -580
  46. package/hub/team/wt-manager.mjs +2 -2
  47. package/hub/workers/factory.mjs +6 -84
  48. package/hub/workers/interface.mjs +1 -7
  49. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  50. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  51. package/package.json +3 -8
  52. package/scripts/lib/agent-route-policy.mjs +143 -62
  53. package/scripts/lib/cli-claude.mjs +0 -14
  54. package/scripts/lib/env-probe.mjs +2 -131
  55. package/scripts/lib/gemini-profiles.mjs +2 -69
  56. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  57. package/scripts/lib/machine-profile.mjs +3 -17
  58. package/scripts/lib/mcp-filter.mjs +109 -53
  59. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  60. package/scripts/lib/mcp-health.mjs +2 -2
  61. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  62. package/scripts/lib/psmux-info.mjs +11 -59
  63. package/scripts/lib/skill-template.mjs +0 -245
  64. package/hub/hub-lifecycle.mjs +0 -248
  65. package/hub/pipe.mjs +0 -825
  66. package/hub/promote-penalties.mjs +0 -138
  67. package/hub/public/dashboard.html +0 -416
  68. package/hub/schema.sql +0 -209
  69. package/hub/server.mjs +0 -3048
  70. package/hub/store-adapter.mjs +0 -182
  71. package/hub/store.mjs +0 -1736
  72. package/hub/team/build-worker-prompt.mjs +0 -92
  73. package/hub/team/check-mcp-hub.mjs +0 -49
  74. package/hub/team/claude-native-bridge.mjs +0 -987
  75. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  76. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  77. package/hub/team/cli/services/hub-client.mjs +0 -264
  78. package/hub/team/cli/services/native-control.mjs +0 -145
  79. package/hub/team/codex-review.mjs +0 -520
  80. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  81. package/hub/team/conductor-registry.mjs +0 -121
  82. package/hub/team/conductor.mjs +0 -1529
  83. package/hub/team/consensus-meta.mjs +0 -176
  84. package/hub/team/cto-auto-collect.mjs +0 -115
  85. package/hub/team/cto-pull.mjs +0 -31
  86. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  87. package/hub/team/dashboard.mjs +0 -322
  88. package/hub/team/event-log.mjs +0 -84
  89. package/hub/team/execution-mode.mjs +0 -324
  90. package/hub/team/extract-completion-payload.mjs +0 -110
  91. package/hub/team/headless-bridge-session.mjs +0 -17
  92. package/hub/team/health-probe.mjs +0 -435
  93. package/hub/team/interactive-native-launcher.mjs +0 -119
  94. package/hub/team/interactive-native-worker.mjs +0 -80
  95. package/hub/team/interactive-tui-transport.mjs +0 -479
  96. package/hub/team/launcher-template.mjs +0 -144
  97. package/hub/team/lead-control.mjs +0 -191
  98. package/hub/team/mcp-selector.mjs +0 -145
  99. package/hub/team/native-supervisor.mjs +0 -460
  100. package/hub/team/native.mjs +0 -727
  101. package/hub/team/recovery-store.mjs +0 -149
  102. package/hub/team/remote-probe.mjs +0 -292
  103. package/hub/team/remote-watcher.mjs +0 -506
  104. package/hub/team/runtime-strategy.mjs +0 -200
  105. package/hub/team/schemas/worker-completion.json +0 -39
  106. package/hub/team/sentinel-capture.mjs +0 -127
  107. package/hub/team/session-sync.mjs +0 -316
  108. package/hub/team/swarm-cli.mjs +0 -390
  109. package/hub/team/swarm-hypervisor.mjs +0 -2206
  110. package/hub/team/swarm-intent.mjs +0 -216
  111. package/hub/team/swarm-locks.mjs +0 -335
  112. package/hub/team/swarm-planner.mjs +0 -371
  113. package/hub/team/swarm-preflight.mjs +0 -366
  114. package/hub/team/swarm-reconciler.mjs +0 -242
  115. package/hub/team/synapse-cli.mjs +0 -458
  116. package/hub/team/synapse-http.mjs +0 -311
  117. package/hub/team/synapse-registry.mjs +0 -618
  118. package/hub/team/tui-remote-adapter.mjs +0 -377
  119. package/hub/team/tui-synapse.mjs +0 -223
  120. package/hub/team/worker-completion-validator.mjs +0 -79
  121. package/hub/team/worker-sandbox.mjs +0 -125
  122. package/hub/team/worktree-lifecycle.mjs +0 -639
  123. package/hub/team/wt-templates.json +0 -43
  124. package/hub/tools.mjs +0 -900
  125. package/hub/workers/antigravity-route-worker.mjs +0 -179
  126. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  127. package/hub/workers/delegator-mcp.mjs +0 -1098
  128. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  129. package/scripts/lib/claudemd-scanner.mjs +0 -223
  130. package/scripts/lib/context.mjs +0 -67
  131. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  132. package/scripts/lib/handoff.mjs +0 -187
  133. package/scripts/lib/logger.mjs +0 -122
  134. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,435 +0,0 @@
1
- // hub/team/health-probe.mjs — 4단계 health model + INPUT_WAIT 감지
2
- // 기존 cli-adapter-base.mjs:stallThresholdMs(30s)와 headless.mjs:STALL_DEFAULTS(120s)를
3
- // 4단계 probe 모델로 교체. stdout+stderr 통합 스트림으로 평가 (F3 해결).
4
-
5
- import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
6
- import { tmpdir } from "node:os";
7
- import { dirname, join } from "node:path";
8
-
9
- /**
10
- * Health probe level 정의.
11
- * L0: Process alive (PID 존재 + exit code 없음)
12
- * L1: Output advancing (stdout+stderr 통합, 30s)
13
- * L1.5: INPUT_WAIT 감지 (질문 패턴이면 stall이 아니라 input-wait)
14
- * L2: MCP connected (opt-in, heartbeat, 30s)
15
- * L3: Prompt acknowledged (첫 tool call/텍스트, 120s)
16
- */
17
- export const PROBE_LEVELS = Object.freeze({
18
- L0: "alive",
19
- L1: "advancing",
20
- L2: "mcp_connected",
21
- L3: "prompt_ack",
22
- });
23
-
24
- /** 기본 설정 (기존 stallThresholdMs/stallTimeout 값 계승) */
25
- export const PROBE_DEFAULTS = Object.freeze({
26
- intervalMs: 5_000,
27
- probeTimeoutMs: 5_000,
28
- l1ThresholdMs: 30_000,
29
- l2ThresholdMs: 30_000,
30
- l3ThresholdMs: 120_000,
31
- // #168 P3: default off → on. checkMcp 미주입 시에도 probeL2 가 skip 반환하므로
32
- // safe. conductor wiring 이 checkMcp 를 주입하면 실제 L2 판정이 활성화된다.
33
- // opt-out: TFX_PROBE_L2=0 (conductor 에서 false 주입).
34
- enableL2: true,
35
- writeStateFile: false,
36
- stateDir: join(tmpdir(), "tfx-probe"),
37
- });
38
-
39
- /**
40
- * stdin 입력 대기 패턴 (Codex 질문 블로킹 감지)
41
- * Codex가 질문하며 stdin을 기다리는 경우 stall이 아니라 INPUT_WAIT로 분류.
42
- */
43
- const INPUT_WAIT_PATTERNS = [
44
- /\?\s*$/m, // 물음표로 끝나는 줄
45
- /\b(y\/n|yes\/no)\b/i, // y/n 프롬프트
46
- /\b(choose|select|pick)\b.*:/i, // choose/select 프롬프트
47
- /\b(confirm|approve|proceed)\b/i, // confirm 프롬프트
48
- /\b(enter|input|type)\b.*:/i, // 입력 요청
49
- /\[.*\]:\s*$/m, // [default]: 형태
50
- />\s*$/m, // > 프롬프트
51
- ];
52
-
53
- /**
54
- * 최근 output에서 INPUT_WAIT 패턴 감지.
55
- * @param {string} recentOutput — 최근 stdout+stderr 통합 텍스트
56
- * @returns {{ detected: boolean, pattern: string|null }}
57
- */
58
- export function detectInputWait(recentOutput) {
59
- if (!recentOutput) return { detected: false, pattern: null };
60
- // 마지막 5줄만 검사 (전체 output이 아닌 최근 출력)
61
- const lines = recentOutput
62
- .split(/\r?\n/)
63
- .filter(Boolean)
64
- .slice(-5)
65
- .join("\n");
66
- for (const re of INPUT_WAIT_PATTERNS) {
67
- if (re.test(lines)) {
68
- return { detected: true, pattern: re.source };
69
- }
70
- }
71
- return { detected: false, pattern: null };
72
- }
73
-
74
- /**
75
- * Health probe 팩토리.
76
- * @param {object} session — probe 대상 세션 상태 객체
77
- * @param {number|null} session.pid — 프로세스 PID
78
- * @param {function} session.getRecentOutput — () => string (최근 stdout+stderr)
79
- * @param {function} session.getOutputBytes — () => number (총 output 바이트)
80
- * @param {boolean} [session.alive] — 프로세스 alive 여부 (외부 업데이트)
81
- * @param {object} [opts] — PROBE_DEFAULTS 오버라이드
82
- * @param {function} [opts.onProbe] — (level, result) => void 콜백
83
- * @param {function} [opts.checkMcp] — () => Promise<boolean> MCP heartbeat 체커 (L2용)
84
- * @returns {{ start, stop, probe, getStatus }}
85
- */
86
- export function createHealthProbe(session, opts = {}) {
87
- const config = { ...PROBE_DEFAULTS, ...opts };
88
- let timer = null;
89
- let started = false;
90
- // stopped flag는 in-flight probe()가 stop() 사이의 await 점에서
91
- // writeState/unlink 와 race 하는 것을 막는다. start() 시 false 로 reset.
92
- let stopped = false;
93
- // P0 (#167 review): start() 마다 증가. probe() 가 시작 시 캡처 → writeState() 가 epoch
94
- // 비교로 stop()→start() 사이의 in-flight 가 새 run 의 state 를 덮는 race 차단.
95
- // stopped flag 만으로는 start() 가 stopped=false 로 reset 한 직후 race 못 막음.
96
- let runEpoch = 0;
97
- // P1-1 (#167 review): Set 으로 모든 in-flight probe 추적. interval 이 빨라 N+1 이 N 끝나기
98
- // 전에 시작되면 단일 var 는 N 을 덮어써 stopAndDrain() 시 N 이 누락된다.
99
- const inFlightProbes = new Set();
100
-
101
- // L1 tracking
102
- let lastOutputBytes = 0;
103
- let lastOutputChangeAt = Date.now();
104
-
105
- // L3 tracking
106
- let promptAcked = false;
107
- let spawnedAt = Date.now();
108
-
109
- const status = {
110
- l0: null, // 'ok' | 'fail'
111
- l1: null, // 'ok' | 'stall' | 'input_wait'
112
- l2: null, // 'ok' | 'fail' | 'skip'
113
- l3: null, // 'ok' | 'timeout'
114
- lastProbeAt: null,
115
- inputWaitPattern: null,
116
- };
117
-
118
- function getStateFilePath() {
119
- if (typeof config.stateFile === "string" && config.stateFile.length > 0) {
120
- return config.stateFile;
121
- }
122
- const pid = session.pid;
123
- if (pid == null || pid <= 0) return null;
124
- return join(config.stateDir, `${pid}.json`);
125
- }
126
-
127
- function deriveState(result) {
128
- if (result.l0 === "fail") return "exited";
129
- if (result.l1 === "input_wait") return "input_wait";
130
- if (result.l2 === "fail") return "mcp_initializing";
131
- if (result.l1 === "stall") return "stalled";
132
- if (result.l3 === "timeout") return "reasoning";
133
- return "active";
134
- }
135
-
136
- function writeState(result, probeEpoch) {
137
- // stop() 직후 in-flight probe()의 재생성 방지.
138
- // P0 (#167): probeEpoch !== runEpoch 면 이전 run 의 stale probe 가 새 run 의 state 를
139
- // 덮으려는 시도 → skip.
140
- if (stopped) return;
141
- if (probeEpoch !== undefined && probeEpoch !== runEpoch) return;
142
- if (!config.writeStateFile && !config.stateFile) return;
143
- const stateFile = getStateFilePath();
144
- if (!stateFile) return;
145
- const payload =
146
- JSON.stringify(
147
- {
148
- pid: session.pid ?? null,
149
- state: deriveState(result),
150
- result,
151
- updatedAt: new Date(result.ts).toISOString(),
152
- },
153
- null,
154
- 2,
155
- ) + "\n";
156
- // tmp+rename 으로 atomic write — heartbeat 의 sed 가 부분 파일을 읽는 race 제거.
157
- // tmp 는 같은 디렉토리에 둬야 EXDEV (cross-device link) 가 안 난다.
158
- const tmpPath = `${stateFile}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
159
- try {
160
- mkdirSync(dirname(stateFile), { recursive: true });
161
- writeFileSync(tmpPath, payload, "utf8");
162
- try {
163
- renameSync(tmpPath, stateFile);
164
- } catch (renameErr) {
165
- // Windows: 대상이 존재할 때 EPERM/EACCES.
166
- // P1-2 (#167 review): backup-then-swap 으로 기존 파일 보존. 옛 unlinkSync→renameSync
167
- // 패턴은 1차 unlink 후 2차 rename 실패 시 기존 파일과 tmp 둘 다 잃었다.
168
- if (
169
- renameErr?.code === "EEXIST" ||
170
- renameErr?.code === "EPERM" ||
171
- renameErr?.code === "EACCES"
172
- ) {
173
- const backupPath = `${stateFile}.old-${process.pid}-${Date.now()}`;
174
- let backupCreated = false;
175
- try {
176
- renameSync(stateFile, backupPath);
177
- backupCreated = true;
178
- } catch {
179
- // backup 실패 (대상 없음/잠김) — 그래도 진행 (tmp→stateFile 시도)
180
- }
181
- try {
182
- renameSync(tmpPath, stateFile);
183
- if (backupCreated) {
184
- try {
185
- unlinkSync(backupPath);
186
- } catch {}
187
- }
188
- } catch (secondErr) {
189
- // 2차도 실패 → backup 복구해서 기존 파일 보존
190
- if (backupCreated) {
191
- try {
192
- renameSync(backupPath, stateFile);
193
- } catch {}
194
- }
195
- throw secondErr;
196
- }
197
- } else {
198
- throw renameErr;
199
- }
200
- }
201
- } catch {
202
- // probe state is advisory only — tmp cleanup
203
- try {
204
- unlinkSync(tmpPath);
205
- } catch {}
206
- }
207
- }
208
-
209
- /**
210
- * L0: Process alive check.
211
- */
212
- function probeL0() {
213
- const alive =
214
- session.alive !== undefined
215
- ? session.alive
216
- : session.pid != null && session.pid > 0;
217
- status.l0 = alive ? "ok" : "fail";
218
- return status.l0;
219
- }
220
-
221
- /**
222
- * L1 + L1.5: Output advancing + INPUT_WAIT 감지.
223
- */
224
- function probeL1() {
225
- const currentBytes =
226
- typeof session.getOutputBytes === "function"
227
- ? session.getOutputBytes()
228
- : 0;
229
-
230
- const now = Date.now();
231
-
232
- if (currentBytes !== lastOutputBytes) {
233
- lastOutputBytes = currentBytes;
234
- lastOutputChangeAt = now;
235
- status.l1 = "ok";
236
- status.inputWaitPattern = null;
237
- return "ok";
238
- }
239
-
240
- const silenceMs = now - lastOutputChangeAt;
241
-
242
- if (silenceMs >= config.l1ThresholdMs) {
243
- // L1.5: INPUT_WAIT 감지 — stall 전에 질문 패턴 체크
244
- const recentOutput =
245
- typeof session.getRecentOutput === "function"
246
- ? session.getRecentOutput()
247
- : "";
248
- const inputWait = detectInputWait(recentOutput);
249
-
250
- if (inputWait.detected) {
251
- status.l1 = "input_wait";
252
- status.inputWaitPattern = inputWait.pattern;
253
- return "input_wait";
254
- }
255
-
256
- status.l1 = "stall";
257
- status.inputWaitPattern = null;
258
- return "stall";
259
- }
260
-
261
- // 아직 threshold 미달
262
- status.l1 = "ok";
263
- return "ok";
264
- }
265
-
266
- /**
267
- * L2: MCP connected (opt-in).
268
- */
269
- async function probeL2() {
270
- if (!config.enableL2) {
271
- status.l2 = "skip";
272
- return "skip";
273
- }
274
- if (typeof config.checkMcp !== "function") {
275
- status.l2 = "skip";
276
- return "skip";
277
- }
278
- try {
279
- const connected = await Promise.race([
280
- config.checkMcp(),
281
- new Promise((_, reject) =>
282
- setTimeout(
283
- () => reject(new Error("probe_timeout")),
284
- config.probeTimeoutMs,
285
- ),
286
- ),
287
- ]);
288
- status.l2 = connected ? "ok" : "fail";
289
- } catch {
290
- status.l2 = "fail";
291
- }
292
- return status.l2;
293
- }
294
-
295
- /**
296
- * L3: Prompt acknowledged.
297
- * 첫 번째 meaningful output이 나오면 ack.
298
- */
299
- function probeL3() {
300
- if (promptAcked) {
301
- status.l3 = "ok";
302
- return "ok";
303
- }
304
-
305
- const currentBytes =
306
- typeof session.getOutputBytes === "function"
307
- ? session.getOutputBytes()
308
- : 0;
309
-
310
- if (currentBytes > 0) {
311
- promptAcked = true;
312
- status.l3 = "ok";
313
- return "ok";
314
- }
315
-
316
- const elapsed = Date.now() - spawnedAt;
317
- if (elapsed >= config.l3ThresholdMs) {
318
- status.l3 = "timeout";
319
- return "timeout";
320
- }
321
-
322
- status.l3 = null; // 아직 판정 전
323
- return null;
324
- }
325
-
326
- /**
327
- * 전체 probe 실행 (L0→L1→L2→L3).
328
- * @returns {Promise<object|null>} probe 결과. stop() 이후 호출이면 null.
329
- */
330
- async function probe() {
331
- if (stopped) return null;
332
- // P0 (#167): probe 시작 시 epoch 캡처. start() 가 새 epoch 로 바꾸면 이 probe 의
333
- // writeState 는 stale 로 판정 → skip.
334
- const probeEpoch = runEpoch;
335
- const promise = (async () => {
336
- const result = {
337
- l0: probeL0(),
338
- l1: probeL1(),
339
- l2: await probeL2(),
340
- l3: probeL3(),
341
- inputWaitPattern: status.inputWaitPattern,
342
- ts: Date.now(),
343
- };
344
- status.lastProbeAt = result.ts;
345
- writeState(result, probeEpoch);
346
-
347
- if (typeof config.onProbe === "function") {
348
- config.onProbe(result);
349
- }
350
-
351
- return result;
352
- })();
353
- // P1-1 (#167): Set 으로 모든 in-flight 추적. 단일 var 패턴은 N+1 이 N 끝나기 전에
354
- // 시작되면 N 을 덮어써 stopAndDrain() 시 N 이 누락된다.
355
- inFlightProbes.add(promise);
356
- promise.finally(() => inFlightProbes.delete(promise));
357
- return promise;
358
- }
359
-
360
- function start() {
361
- if (started) return;
362
- started = true;
363
- stopped = false;
364
- // P0 (#167): epoch 증가 — 이전 run 의 in-flight probe 가 새 run 의 state 를 덮지 못하게.
365
- runEpoch += 1;
366
- spawnedAt = Date.now();
367
- lastOutputChangeAt = Date.now();
368
- lastOutputBytes = 0;
369
- promptAcked = false;
370
-
371
- timer = setInterval(() => {
372
- void probe();
373
- }, config.intervalMs);
374
- timer.unref?.();
375
-
376
- // 즉시 첫 probe 실행
377
- void probe();
378
- }
379
-
380
- function stop() {
381
- if (!started) return;
382
- started = false;
383
- // stopped 를 먼저 set 해야 in-flight probe()의 writeState() 가 skip 된다.
384
- // 이후 unlink — in-flight 가 끝나도 writeState 가 no-op 이므로 재생성 없음.
385
- stopped = true;
386
- if (timer) {
387
- clearInterval(timer);
388
- timer = null;
389
- }
390
- if (config.writeStateFile || config.stateFile) {
391
- try {
392
- const stateFile = getStateFilePath();
393
- if (stateFile) unlinkSync(stateFile);
394
- } catch {}
395
- }
396
- }
397
-
398
- /**
399
- * stop() 후 in-flight probe() 가 완료될 때까지 대기.
400
- * 결정적 종료가 필요한 테스트/teardown 용. conductor 의 sync stop() 호출자는
401
- * 그대로 stop() 만 호출하면 stopped flag 가 race 를 막는다.
402
- */
403
- async function stopAndDrain() {
404
- stop();
405
- // P1-1 (#167): 모든 in-flight probe 대기. allSettled 로 unhandled rejection 방지.
406
- if (inFlightProbes.size > 0) {
407
- await Promise.allSettled(Array.from(inFlightProbes));
408
- }
409
- }
410
-
411
- /** L1 tracking 리셋 (restart 후 호출) */
412
- function resetTracking() {
413
- lastOutputBytes = 0;
414
- lastOutputChangeAt = Date.now();
415
- promptAcked = false;
416
- spawnedAt = Date.now();
417
- status.l0 = null;
418
- status.l1 = null;
419
- status.l2 = null;
420
- status.l3 = null;
421
- status.inputWaitPattern = null;
422
- }
423
-
424
- return Object.freeze({
425
- start,
426
- stop,
427
- stopAndDrain,
428
- probe,
429
- resetTracking,
430
- getStatus: () => ({ ...status }),
431
- get started() {
432
- return started;
433
- },
434
- });
435
- }
@@ -1,119 +0,0 @@
1
- import crypto from "node:crypto";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- import {
6
- buildDaemonExecDispatchPayload,
7
- deriveClaudeDaemonPaths,
8
- dispatchClaudeDaemonJob,
9
- teardownClaudeDaemonJob,
10
- } from "./claude-daemon-control.mjs";
11
- import { encodeBridgeConfig } from "./daemon-pty-tmux-bridge.mjs";
12
-
13
- const DEFAULT_COLS = 120;
14
- const DEFAULT_ROWS = 40;
15
-
16
- export async function launchAdoptedInteractiveWorker({
17
- cwd = process.cwd(),
18
- sessionName,
19
- launchCmd = "codex",
20
- cli = "codex",
21
- role = "interactive",
22
- configDir,
23
- pid = process.pid,
24
- env = {},
25
- cols = DEFAULT_COLS,
26
- rows = DEFAULT_ROWS,
27
- _deps = {},
28
- } = {}) {
29
- const derivePaths = _deps.deriveClaudeDaemonPaths || deriveClaudeDaemonPaths;
30
- const encodeConfig = _deps.encodeBridgeConfig || encodeBridgeConfig;
31
- const buildPayload =
32
- _deps.buildDaemonExecDispatchPayload || buildDaemonExecDispatchPayload;
33
- const dispatchJob = _deps.dispatchClaudeDaemonJob || dispatchClaudeDaemonJob;
34
- const teardownJob = _deps.teardownClaudeDaemonJob || teardownClaudeDaemonJob;
35
-
36
- const resolvedCwd = path.resolve(cwd);
37
- const effectiveSessionName = sessionName || `tfx-${cli}-${role}-${pid}`;
38
- const paths = derivePaths({ configDir });
39
- const short = crypto.randomBytes(4).toString("hex");
40
- const displayName = `Triflux ${cli} ${role}`;
41
- const bridgePath = fileURLToPath(
42
- new URL("./daemon-pty-tmux-bridge.mjs", import.meta.url),
43
- );
44
- const bridgeConfig = encodeConfig({
45
- sessionName: effectiveSessionName,
46
- launchCmd,
47
- cwd: resolvedCwd,
48
- env,
49
- cols,
50
- rows,
51
- });
52
- const command = `${shellQuote(process.execPath)} ${shellQuote(
53
- bridgePath,
54
- )} --config ${bridgeConfig}`;
55
- const payload = buildPayload({
56
- short,
57
- cwd: resolvedCwd,
58
- command,
59
- name: displayName,
60
- cols,
61
- rows,
62
- });
63
- let dispatched;
64
- try {
65
- dispatched = await dispatchJob({
66
- paths,
67
- controlSock: paths.controlSock,
68
- payload,
69
- agent: cli,
70
- name: displayName,
71
- cwd: resolvedCwd,
72
- dispatchTimeoutMs: 5000,
73
- });
74
- } catch (error) {
75
- // dispatch may have started a daemon job (pid assigned) before a later
76
- // step (bridge-session resolve / projection write) threw. Tear down by
77
- // short + sessionId so a failed launch never leaks a running pty/tmux job.
78
- await teardownJob({
79
- controlSock: paths.controlSock,
80
- paths,
81
- short,
82
- sessionId: payload.sessionId,
83
- }).catch(() => {});
84
- throw error;
85
- }
86
-
87
- let closed = false;
88
- let closePromise = null;
89
-
90
- return {
91
- short,
92
- displayName,
93
- sessionId: dispatched.sessionId,
94
- pid: dispatched.pid,
95
- bridgeSessionId: dispatched.bridgeSessionId,
96
- sessionProjectionPath: dispatched.sessionProjectionPath,
97
- controlSock: paths.controlSock,
98
- close() {
99
- if (!closePromise) {
100
- closePromise = (async () => {
101
- if (closed) return;
102
- closed = true;
103
- await teardownJob({
104
- controlSock: paths.controlSock,
105
- paths,
106
- short,
107
- sessionProjectionPath: dispatched.sessionProjectionPath,
108
- sessionId: dispatched.sessionId,
109
- });
110
- })();
111
- }
112
- return closePromise;
113
- },
114
- };
115
- }
116
-
117
- function shellQuote(value) {
118
- return `'${String(value).replaceAll("'", "'\\''")}'`;
119
- }
@@ -1,80 +0,0 @@
1
- import { startNativeWorkerFacade } from "./claude-native-bridge.mjs";
2
- import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
3
-
4
- // Output currently flows through capture-pane snapshots, so attach clients may
5
- // receive whole-screen refreshes rather than byte-accurate terminal deltas.
6
- export async function startInteractiveNativeWorker({
7
- short,
8
- rvSock,
9
- ptySock,
10
- sessionName,
11
- cwd,
12
- launchCmd,
13
- pid = process.pid,
14
- env = {},
15
- createTransport = createInteractiveTuiTransport,
16
- } = {}) {
17
- let facade = null;
18
- let closePromise = null;
19
-
20
- const transport = createTransport({
21
- sessionName,
22
- cwd,
23
- launchCmd,
24
- env,
25
- onData(chunk) {
26
- if (facade) facade.writeOutput(chunk);
27
- },
28
- });
29
-
30
- async function close({ exitCode = 0, waitForPtyEnd = false } = {}) {
31
- if (!closePromise) {
32
- closePromise = (async () => {
33
- await transport.stop().catch(() => {});
34
- if (waitForPtyEnd) {
35
- await new Promise((resolve) => setTimeout(resolve, 10));
36
- }
37
- if (facade) await facade.close({ exitCode });
38
- })();
39
- }
40
- return closePromise;
41
- }
42
-
43
- try {
44
- facade = await startNativeWorkerFacade({
45
- short,
46
- rvSock,
47
- ptySock,
48
- pid,
49
- workerType: "interactive",
50
- onInput(payload) {
51
- void Promise.resolve()
52
- .then(() => transport.writeInput(payload))
53
- .catch(() => {
54
- void close({ exitCode: 1 }).catch(() => {});
55
- });
56
- },
57
- onKill() {
58
- void close({ waitForPtyEnd: true }).catch(() => {});
59
- },
60
- });
61
- } catch (error) {
62
- await transport.stop().catch(() => {});
63
- throw error;
64
- }
65
-
66
- try {
67
- await transport.start();
68
- } catch (error) {
69
- await close({ exitCode: 1 }).catch(() => {});
70
- throw error;
71
- }
72
-
73
- return {
74
- short,
75
- rvSock,
76
- ptySock,
77
- pid,
78
- close,
79
- };
80
- }