@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,987 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import crypto from "node:crypto";
3
- import fs from "node:fs/promises";
4
- import net from "node:net";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import {
8
- buildDaemonExecDispatchPayload,
9
- deriveClaudeDaemonPaths,
10
- dispatchClaudeDaemonJob,
11
- getProcStart,
12
- killDaemonJob,
13
- resolveClaudeConfigDir,
14
- resolveDaemonBridgeSessionId,
15
- sendClaudeControlRequest,
16
- teardownClaudeDaemonJob,
17
- waitForDaemonJobPid,
18
- } from "./claude-daemon-control.mjs";
19
- import {
20
- buildClaudeSessionProjection,
21
- removeClaudeSessionProjection,
22
- writeClaudeSessionProjection,
23
- } from "./claude-session-projection.mjs";
24
- import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
25
-
26
- // daemon-control 이 deriveClaudeDaemonPaths / getProcStart /
27
- // resolveClaudeConfigDir 의 단일 owner 다. configDir 해석이 갈리면 양쪽이
28
- // 서로 다른 control.sock hash 를 보게 되므로 (HOME 오버라이드 split-brain)
29
- // 로컬 복제본을 두지 않는다. 기존 import 경로 호환을 위해 re-export 한다.
30
- export { deriveClaudeDaemonPaths, getProcStart, resolveClaudeConfigDir };
31
-
32
- const DEFAULT_ROWS = 40;
33
- const DEFAULT_COLS = 120;
34
- const MAX_TRANSCRIPT_BYTES = 64 * 1024;
35
- const ROSTER_LOCK_STALE_MS = 30_000;
36
- const ROSTER_LOCK_TIMEOUT_MS = 5_000;
37
- const ROSTER_LOCK_RETRY_MS = 10;
38
-
39
- export function buildPtyDataFrame(value) {
40
- const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
41
- const frame = Buffer.allocUnsafe(5 + payload.length);
42
- frame.writeUInt32BE(payload.length, 0);
43
- frame.writeUInt8(0, 4);
44
- payload.copy(frame, 5);
45
- return frame;
46
- }
47
-
48
- export function buildPtyControlFrame(value) {
49
- const payload = Buffer.from(JSON.stringify(value), "utf8");
50
- const frame = Buffer.allocUnsafe(5 + payload.length);
51
- frame.writeUInt32BE(payload.length, 0);
52
- frame.writeUInt8(1, 4);
53
- payload.copy(frame, 5);
54
- return frame;
55
- }
56
-
57
- export function extractPtyFrames(buffer) {
58
- const frames = [];
59
- let rest = Buffer.from(buffer);
60
- while (rest.length >= 5) {
61
- const length = rest.readUInt32BE(0);
62
- const frameLength = 5 + length;
63
- if (rest.length < frameLength) break;
64
- const kind = rest.readUInt8(4);
65
- const payload = rest.subarray(5, frameLength);
66
- if (kind === 0) frames.push({ kind, payload: Buffer.from(payload) });
67
- else if (kind === 1) {
68
- frames.push({ kind, ctrl: JSON.parse(payload.toString("utf8")) });
69
- } else {
70
- throw new Error(`unknown PTY frame kind ${kind}`);
71
- }
72
- rest = rest.subarray(frameLength);
73
- }
74
- return { frames, rest };
75
- }
76
-
77
- function trimTranscript(buffer) {
78
- if (buffer.length <= MAX_TRANSCRIPT_BYTES) return buffer;
79
- return buffer.subarray(buffer.length - MAX_TRANSCRIPT_BYTES);
80
- }
81
-
82
- function listen(server, sockPath) {
83
- return new Promise((resolve, reject) => {
84
- server.once("error", reject);
85
- server.listen(sockPath, () => {
86
- server.off("error", reject);
87
- resolve();
88
- });
89
- });
90
- }
91
-
92
- function closeServer(server) {
93
- return new Promise((resolve) => {
94
- try {
95
- server.close(() => resolve());
96
- } catch {
97
- resolve();
98
- }
99
- });
100
- }
101
-
102
- async function readRoster(rosterPath) {
103
- try {
104
- const parsed = JSON.parse(await fs.readFile(rosterPath, "utf8"));
105
- return {
106
- proto: parsed.proto || 1,
107
- supervisorPid: parsed.supervisorPid || 0,
108
- updatedAt: parsed.updatedAt || Date.now(),
109
- workers:
110
- parsed.workers && typeof parsed.workers === "object"
111
- ? { ...parsed.workers }
112
- : {},
113
- };
114
- } catch (error) {
115
- if (error?.code !== "ENOENT") throw error;
116
- return { proto: 1, supervisorPid: 0, updatedAt: Date.now(), workers: {} };
117
- }
118
- }
119
-
120
- async function writeRoster(rosterPath, roster) {
121
- await fs.mkdir(path.dirname(rosterPath), { recursive: true });
122
- const tmpPath = `${rosterPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
123
- const next = { ...roster, proto: 1, updatedAt: Date.now() };
124
- await fs.writeFile(tmpPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
125
- await fs.rename(tmpPath, rosterPath);
126
- }
127
-
128
- function sleep(ms) {
129
- return new Promise((resolve) => setTimeout(resolve, ms));
130
- }
131
-
132
- async function acquireRosterLock(rosterPath) {
133
- await fs.mkdir(path.dirname(rosterPath), { recursive: true });
134
- const lockPath = `${rosterPath}.lock`;
135
- const startedAt = Date.now();
136
- while (true) {
137
- try {
138
- await fs.mkdir(lockPath);
139
- return async () => {
140
- await fs.rm(lockPath, { recursive: true, force: true });
141
- };
142
- } catch (error) {
143
- if (error?.code !== "EEXIST") throw error;
144
- if (Date.now() - startedAt > ROSTER_LOCK_TIMEOUT_MS) {
145
- throw new Error(`Timed out waiting for roster lock: ${lockPath}`);
146
- }
147
- try {
148
- const stat = await fs.stat(lockPath);
149
- if (Date.now() - stat.mtimeMs > ROSTER_LOCK_STALE_MS) {
150
- await fs.rm(lockPath, { recursive: true, force: true });
151
- continue;
152
- }
153
- } catch (statError) {
154
- if (statError?.code !== "ENOENT") throw statError;
155
- }
156
- await sleep(ROSTER_LOCK_RETRY_MS);
157
- }
158
- }
159
- }
160
-
161
- async function withRosterLock(rosterPath, fn) {
162
- const release = await acquireRosterLock(rosterPath);
163
- try {
164
- return await fn();
165
- } finally {
166
- await release();
167
- }
168
- }
169
-
170
- export async function mergeRosterWorkers(rosterPath, workers) {
171
- return withRosterLock(rosterPath, async () => {
172
- const roster = await readRoster(rosterPath);
173
- roster.workers = { ...roster.workers, ...workers };
174
- await writeRoster(rosterPath, roster);
175
- return roster;
176
- });
177
- }
178
-
179
- export async function removeRosterWorkers(rosterPath, shorts) {
180
- return withRosterLock(rosterPath, async () => {
181
- const roster = await readRoster(rosterPath);
182
- let changed = false;
183
- for (const short of shorts) {
184
- if (Object.hasOwn(roster.workers, short)) {
185
- delete roster.workers[short];
186
- changed = true;
187
- }
188
- }
189
- if (changed) await writeRoster(rosterPath, roster);
190
- return changed;
191
- });
192
- }
193
-
194
- export async function removeClaudeJobState(jobsDir, short) {
195
- if (!short) return;
196
- await fs.rm(path.join(jobsDir, short), { recursive: true, force: true });
197
- }
198
-
199
- function shellSingleQuote(value) {
200
- return `'${String(value).replaceAll("'", "'\\''")}'`;
201
- }
202
-
203
- function buildSwarmShardBridgeCommand({ displayName, sessionId }) {
204
- const banner = `${displayName} native bridge active (${sessionId})`;
205
- return `printf '%s\\n' ${shellSingleQuote(banner)}; while true; do sleep 3600; done`;
206
- }
207
-
208
- function normalizeWorkerType(value) {
209
- return value === "interactive" ? "interactive" : "headless";
210
- }
211
-
212
- function buildInteractiveTransportSessionName(...parts) {
213
- const raw = parts.filter(Boolean).join("-");
214
- return raw
215
- .replace(/[^A-Za-z0-9_.-]+/g, "-")
216
- .replace(/^-+|-+$/g, "")
217
- .slice(0, 80);
218
- }
219
-
220
- function createStopOnce(transport) {
221
- let stopPromise = null;
222
- return () => {
223
- if (!stopPromise) {
224
- stopPromise = Promise.resolve()
225
- .then(() => transport.stop())
226
- .catch(() => {});
227
- }
228
- return stopPromise;
229
- };
230
- }
231
-
232
- function compactDisplayPart(value, fallback = "") {
233
- const text = String(value ?? "")
234
- .replace(/[\u0000-\u001f\u007f]+/g, " ")
235
- .replace(/\s+/g, " ")
236
- .trim();
237
- return text || fallback;
238
- }
239
-
240
- function toPositiveInteger(value) {
241
- const number = Number(value);
242
- return Number.isInteger(number) && number > 0 ? number : null;
243
- }
244
-
245
- function formatShardOrdinal({ shardIndex, shardCount }) {
246
- const index = toPositiveInteger(shardIndex);
247
- const count = toPositiveInteger(shardCount);
248
- if (!index || !count) return null;
249
- const width = Math.max(String(count).length, 2);
250
- return `${String(index).padStart(width, "0")}/${String(count).padStart(width, "0")}`;
251
- }
252
-
253
- function buildSwarmShardDisplayName({
254
- sessionId,
255
- cli,
256
- swarmName,
257
- shardIndex,
258
- shardCount,
259
- shardName,
260
- }) {
261
- const groupName = compactDisplayPart(swarmName || sessionId, "swarm");
262
- const agentName = compactDisplayPart(cli, "agent");
263
- const workerName = compactDisplayPart(shardName, "shard");
264
- return [
265
- "Triflux swarm",
266
- groupName,
267
- formatShardOrdinal({ shardIndex, shardCount }),
268
- agentName,
269
- workerName,
270
- ]
271
- .filter(Boolean)
272
- .join(" ");
273
- }
274
-
275
- function isLocalHost(host) {
276
- const value = String(host || "local").toLowerCase();
277
- return value === "local" || value === "localhost" || value === "127.0.0.1";
278
- }
279
-
280
- function deriveSwarmShort({ sessionId, shardName, host }) {
281
- return crypto
282
- .createHash("sha256")
283
- .update(`${sessionId}:${shardName}:${host || "local"}`)
284
- .digest("hex")
285
- .slice(0, 8);
286
- }
287
-
288
- export async function registerSwarmShard({
289
- sessionId,
290
- cli = "codex",
291
- role = "worker",
292
- swarmName,
293
- shardIndex,
294
- shardCount,
295
- shardName,
296
- cwd = process.cwd(),
297
- host = "local",
298
- interactive = false,
299
- workerType,
300
- launchCmd = "codex",
301
- env = {},
302
- configDir = resolveClaudeConfigDir(),
303
- tmpRoot = "/tmp",
304
- _deps = {},
305
- } = {}) {
306
- if (!sessionId) throw new Error("sessionId is required");
307
- if (!shardName) throw new Error("shardName is required");
308
-
309
- const displayName = buildSwarmShardDisplayName({
310
- sessionId,
311
- cli,
312
- swarmName,
313
- shardIndex,
314
- shardCount,
315
- shardName,
316
- });
317
- const warn = _deps.warn || ((message) => console.warn(message));
318
- if (!isLocalHost(host)) {
319
- warn(
320
- `[native-bridge] remote shard ${shardName}@${host}: skipping local registration; remote launcher must register on that host`,
321
- );
322
- return {
323
- ok: true,
324
- skipped: true,
325
- host,
326
- sessionId,
327
- shardName,
328
- displayName,
329
- close() {},
330
- };
331
- }
332
-
333
- const derivePaths = _deps.deriveClaudeDaemonPaths || deriveClaudeDaemonPaths;
334
- const buildPayload =
335
- _deps.buildDaemonExecDispatchPayload || buildDaemonExecDispatchPayload;
336
- const sendControl =
337
- _deps.sendClaudeControlRequest || sendClaudeControlRequest;
338
- const waitForPid = _deps.waitForDaemonJobPid || waitForDaemonJobPid;
339
- const resolveBridgeSessionId =
340
- _deps.resolveDaemonBridgeSessionId || resolveDaemonBridgeSessionId;
341
- const readProcStart = _deps.getProcStart || getProcStart;
342
- const buildProjection =
343
- _deps.buildClaudeSessionProjection || buildClaudeSessionProjection;
344
- const writeProjection =
345
- _deps.writeClaudeSessionProjection || writeClaudeSessionProjection;
346
- const removeProjection =
347
- _deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
348
- const mergeRoster = _deps.mergeRosterWorkers || mergeRosterWorkers;
349
- const removeRoster = _deps.removeRosterWorkers || removeRosterWorkers;
350
- const createTransport =
351
- _deps.createInteractiveTuiTransport || createInteractiveTuiTransport;
352
- const removeJobStateImpl = _deps.removeClaudeJobState || removeClaudeJobState;
353
- const killJob = _deps.killDaemonJob || killDaemonJob;
354
- const accessControlSock = _deps.accessControlSock || fs.access;
355
- const dispatchJob = _deps.dispatchClaudeDaemonJob || dispatchClaudeDaemonJob;
356
- const teardownJob = _deps.teardownClaudeDaemonJob || teardownClaudeDaemonJob;
357
-
358
- const paths = derivePaths({ configDir, tmpRoot });
359
- const sessionsDir =
360
- paths.sessionsDir || path.join(paths.configDir || configDir, "sessions");
361
- const jobsDir =
362
- paths.jobsDir || path.join(paths.configDir || configDir, "jobs");
363
- const short = deriveSwarmShort({ sessionId, shardName, host });
364
- const command = buildSwarmShardBridgeCommand({ displayName, sessionId });
365
- const resolvedWorkerType = interactive
366
- ? "interactive"
367
- : normalizeWorkerType(workerType);
368
-
369
- if (resolvedWorkerType === "interactive") {
370
- const rvSock = path.join(paths.rendezvousDir, `${short}.rv.sock`);
371
- const ptySock = path.join(paths.ptyDir, `${short}.pty.sock`);
372
- let facade = null;
373
- let rosterWritten = false;
374
- const transport = createTransport({
375
- sessionName: buildInteractiveTransportSessionName(
376
- "tfx",
377
- "swarm",
378
- sessionId,
379
- shardName,
380
- short,
381
- ),
382
- cwd,
383
- launchCmd,
384
- env,
385
- onData(chunk) {
386
- if (facade) facade.writeOutput(chunk);
387
- },
388
- });
389
- const stopTransport = createStopOnce(transport);
390
-
391
- try {
392
- facade = await startNativeWorkerFacade({
393
- short,
394
- rvSock,
395
- ptySock,
396
- pid: process.pid,
397
- initialDetail: `${cli}:${role}:interactive`,
398
- workerType: "interactive",
399
- onInput(payload) {
400
- void Promise.resolve()
401
- .then(() => transport.writeInput(payload))
402
- .catch(() => {});
403
- },
404
- onResize(size) {
405
- void Promise.resolve()
406
- .then(() => transport.resize(size))
407
- .catch(() => {});
408
- },
409
- onKill() {
410
- void stopTransport();
411
- },
412
- });
413
- await transport.start();
414
- const rosterEntry = buildNativeWorkerRosterEntry({
415
- short,
416
- pid: process.pid,
417
- procStart: readProcStart(process.pid),
418
- sessionId,
419
- cwd,
420
- startedAt: Date.now(),
421
- rendezvousSock: rvSock,
422
- ptySock,
423
- name: displayName,
424
- prompt: displayName,
425
- });
426
- await mergeRoster(paths.rosterPath, { [short]: rosterEntry });
427
- rosterWritten = true;
428
- } catch (error) {
429
- await stopTransport();
430
- if (facade) await facade.close({ exitCode: 1 }).catch(() => {});
431
- throw error;
432
- }
433
-
434
- let closed = false;
435
- return {
436
- ok: true,
437
- skipped: false,
438
- interactive: true,
439
- host: "local",
440
- sessionId,
441
- swarmSessionId: sessionId,
442
- shardName,
443
- cli,
444
- role,
445
- short,
446
- displayName,
447
- rosterPath: paths.rosterPath,
448
- async close() {
449
- if (closed) return;
450
- closed = true;
451
- if (rosterWritten) await removeRoster(paths.rosterPath, [short]);
452
- await stopTransport();
453
- if (facade) await facade.close().catch(() => {});
454
- },
455
- };
456
- }
457
-
458
- const payload = buildPayload({
459
- short,
460
- cwd,
461
- command,
462
- name: displayName,
463
- });
464
-
465
- // native-bridge 는 데몬이 없으면 빠르게 실패해야 하므로 1000ms 로 고정한다
466
- // (headless 의 5000ms 와 다르므로 명시 전달).
467
- const dispatched = await dispatchJob({
468
- paths,
469
- controlSock: paths.controlSock,
470
- payload,
471
- agent: cli,
472
- name: displayName,
473
- cwd,
474
- dispatchTimeoutMs: 1000,
475
- pidTimeoutMs: 1000,
476
- bridgeTimeoutMs: 1000,
477
- accessControlSock,
478
- _deps: {
479
- sendClaudeControlRequest: sendControl,
480
- waitForDaemonJobPid: waitForPid,
481
- resolveDaemonBridgeSessionId: resolveBridgeSessionId,
482
- buildClaudeSessionProjection: buildProjection,
483
- writeClaudeSessionProjection: writeProjection,
484
- getProcStart: readProcStart,
485
- },
486
- });
487
- const sessionProjectionPath = dispatched.sessionProjectionPath;
488
- void sessionsDir;
489
- let closed = false;
490
-
491
- return {
492
- ok: true,
493
- skipped: false,
494
- host: "local",
495
- sessionId: payload.sessionId,
496
- swarmSessionId: sessionId,
497
- shardName,
498
- cli,
499
- role,
500
- short,
501
- displayName,
502
- controlSock: paths.controlSock,
503
- sessionProjectionPath,
504
- async close() {
505
- if (closed) return;
506
- closed = true;
507
- await teardownJob({
508
- controlSock: paths.controlSock,
509
- short,
510
- sessionProjectionPath,
511
- jobsDir,
512
- removeJobState: true,
513
- _deps: {
514
- removeClaudeSessionProjection: removeProjection,
515
- killDaemonJob: killJob,
516
- removeClaudeJobState: removeJobStateImpl,
517
- },
518
- });
519
- },
520
- };
521
- }
522
-
523
- export function buildNativeWorkerRosterEntry({
524
- short,
525
- pid = process.pid,
526
- procStart,
527
- cwd = process.cwd(),
528
- startedAt = Date.now(),
529
- rendezvousSock,
530
- ptySock,
531
- sessionId = crypto.randomUUID(),
532
- cliVersion = "triflux-native-bridge",
533
- name = "Triflux native worker",
534
- prompt = "",
535
- cols = DEFAULT_COLS,
536
- rows = DEFAULT_ROWS,
537
- }) {
538
- if (!short) throw new Error("short is required");
539
- if (!rendezvousSock) throw new Error("rendezvousSock is required");
540
- if (!ptySock) throw new Error("ptySock is required");
541
- return {
542
- pid,
543
- procStart: procStart || getProcStart(pid),
544
- sessionId,
545
- rendezvousSock,
546
- ptySock,
547
- cliVersion,
548
- startedAt,
549
- attempt: 1,
550
- cwd,
551
- dispatch: {
552
- proto: 1,
553
- short,
554
- sessionId,
555
- createdAt: startedAt,
556
- source: "shell",
557
- cwd,
558
- launch: { mode: "prompt", args: ["--", prompt || name] },
559
- env: {},
560
- isolation: "none",
561
- respawnFlags: [],
562
- seed: {
563
- intent: prompt || name,
564
- name,
565
- },
566
- cols,
567
- rows,
568
- },
569
- };
570
- }
571
-
572
- export async function startNativeWorkerFacade({
573
- short,
574
- rvSock,
575
- ptySock,
576
- pid = process.pid,
577
- version = "triflux-native-bridge",
578
- initialDetail = "pending",
579
- workerType = "headless",
580
- onInput,
581
- onResize,
582
- onKill,
583
- } = {}) {
584
- if (!short) throw new Error("short is required");
585
- if (!rvSock) throw new Error("rvSock is required");
586
- if (!ptySock) throw new Error("ptySock is required");
587
-
588
- await fs.mkdir(path.dirname(rvSock), { recursive: true });
589
- await fs.mkdir(path.dirname(ptySock), { recursive: true });
590
- await fs.rm(rvSock, { force: true }).catch(() => {});
591
- await fs.rm(ptySock, { force: true }).catch(() => {});
592
-
593
- const rvSockets = new Set();
594
- const ptySockets = new Set();
595
- const events = [];
596
- let state = { state: "running", tempo: "idle", detail: initialDetail };
597
- let transcript = Buffer.alloc(0);
598
- let closed = false;
599
-
600
- const writeJsonLine = (socket, message) => {
601
- socket.write(`${JSON.stringify(message)}\n`);
602
- };
603
- const broadcastState = (patch = {}) => {
604
- state = { ...state, ...patch };
605
- for (const socket of rvSockets) {
606
- writeJsonLine(socket, { type: "heartbeat" });
607
- writeJsonLine(socket, { type: "state", patch: state });
608
- }
609
- };
610
- const broadcastPty = (frame) => {
611
- for (const socket of ptySockets) socket.write(frame);
612
- };
613
- const appendOutput = (value) => {
614
- const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
615
- transcript = trimTranscript(Buffer.concat([transcript, payload]));
616
- broadcastPty(buildPtyDataFrame(payload));
617
- };
618
- const handlePtyInput =
619
- workerType === "interactive" && typeof onInput === "function"
620
- ? onInput
621
- : appendOutput;
622
-
623
- const rvServer = net.createServer((socket) => {
624
- rvSockets.add(socket);
625
- events.push({ type: "rv-connect" });
626
- socket.setEncoding("utf8");
627
- socket.once("close", () => rvSockets.delete(socket));
628
- socket.on("error", (error) => {
629
- events.push({ type: "rv-socket-error", message: error.message });
630
- });
631
- writeJsonLine(socket, { type: "heartbeat" });
632
- writeJsonLine(socket, { type: "state", patch: state });
633
- socket.on("data", (chunk) => {
634
- for (const line of chunk.split("\n")) {
635
- if (!line.trim()) continue;
636
- let message;
637
- try {
638
- message = JSON.parse(line);
639
- } catch (error) {
640
- events.push({ type: "rv-error", message: error.message });
641
- continue;
642
- }
643
- events.push({ type: "rv-message", message });
644
- if (message.type === "shutdown") {
645
- writeJsonLine(socket, { type: "shutting-down" });
646
- socket.end();
647
- if (typeof onKill === "function") onKill({ source: "rv", message });
648
- } else if (message.type === "repaint") {
649
- writeJsonLine(socket, { type: "repaint-done" });
650
- } else if (message.type === "reply") {
651
- broadcastState({
652
- tempo: "idle",
653
- detail: `reply:${String(message.text || "").slice(0, 120)}`,
654
- });
655
- } else {
656
- writeJsonLine(socket, { type: "ack", requestType: message.type });
657
- }
658
- }
659
- });
660
- });
661
-
662
- const ptyServer = net.createServer((socket) => {
663
- ptySockets.add(socket);
664
- events.push({ type: "pty-connect" });
665
- socket.once("close", () => ptySockets.delete(socket));
666
- socket.on("error", (error) => {
667
- events.push({ type: "pty-socket-error", message: error.message });
668
- });
669
- socket.write(buildPtyControlFrame({ t: "hello", replPid: pid, version }));
670
- if (transcript.length > 0) socket.write(buildPtyDataFrame(transcript));
671
- socket.write(buildPtyControlFrame({ t: "live" }));
672
- let ptyBuffer = Buffer.alloc(0);
673
- socket.on("data", (chunk) => {
674
- try {
675
- const result = extractPtyFrames(Buffer.concat([ptyBuffer, chunk]));
676
- ptyBuffer = result.rest;
677
- for (const frame of result.frames) {
678
- events.push({
679
- type: "pty-frame",
680
- frame:
681
- frame.kind === 1
682
- ? frame.ctrl
683
- : { kind: 0, bytes: frame.payload.length },
684
- });
685
- if (frame.kind === 0) handlePtyInput(frame.payload);
686
- else if (frame.ctrl.t === "resize") {
687
- if (
688
- workerType === "interactive" &&
689
- typeof onResize === "function"
690
- ) {
691
- const cols = toPositiveInteger(frame.ctrl.cols);
692
- const rows = toPositiveInteger(frame.ctrl.rows);
693
- if (cols && rows) {
694
- void Promise.resolve()
695
- .then(() => onResize({ cols, rows }))
696
- .catch((error) => {
697
- events.push({
698
- type: "pty-resize-error",
699
- message: error.message,
700
- });
701
- });
702
- }
703
- }
704
- socket.write(buildPtyControlFrame({ t: "live" }));
705
- } else if (frame.ctrl.t === "kill") {
706
- socket.write(
707
- buildPtyControlFrame({
708
- t: "exit",
709
- code: 0,
710
- signal: frame.ctrl.sig,
711
- }),
712
- );
713
- socket.end();
714
- if (typeof onKill === "function") {
715
- onKill({ source: "pty", message: frame.ctrl });
716
- }
717
- }
718
- }
719
- } catch (error) {
720
- events.push({ type: "pty-error", message: error.message });
721
- socket.destroy(error);
722
- }
723
- });
724
- });
725
-
726
- try {
727
- await listen(rvServer, rvSock);
728
- await listen(ptyServer, ptySock);
729
- } catch (error) {
730
- await Promise.all([closeServer(rvServer), closeServer(ptyServer)]);
731
- await fs.rm(rvSock, { force: true }).catch(() => {});
732
- await fs.rm(ptySock, { force: true }).catch(() => {});
733
- throw error;
734
- }
735
-
736
- return {
737
- short,
738
- rvSock,
739
- ptySock,
740
- getEvents() {
741
- return events.slice();
742
- },
743
- update(patch) {
744
- if (closed) return;
745
- broadcastState(patch);
746
- },
747
- writeOutput(value) {
748
- if (closed) return;
749
- appendOutput(value);
750
- },
751
- async close({ exitCode = 0 } = {}) {
752
- if (closed) return;
753
- closed = true;
754
- broadcastPty(buildPtyControlFrame({ t: "exit", code: exitCode }));
755
- for (const socket of rvSockets) socket.destroy();
756
- for (const socket of ptySockets) socket.destroy();
757
- await Promise.all([closeServer(rvServer), closeServer(ptyServer)]);
758
- await fs.rm(rvSock, { force: true }).catch(() => {});
759
- await fs.rm(ptySock, { force: true }).catch(() => {});
760
- },
761
- };
762
- }
763
-
764
- export async function startClaudeNativeBridge({
765
- sessionName,
766
- assignments,
767
- configDir = resolveClaudeConfigDir(),
768
- cwd = process.cwd(),
769
- pid,
770
- procStart,
771
- tmpRoot = os.tmpdir(),
772
- onKill,
773
- workerType = "headless",
774
- launchCmd = "codex",
775
- env = {},
776
- createTransport = createInteractiveTuiTransport,
777
- } = {}) {
778
- if (!sessionName) throw new Error("sessionName is required");
779
- const paths = deriveClaudeDaemonPaths({ configDir, tmpRoot: "/tmp" });
780
- void tmpRoot; // UDS sockets must stay short on macOS; use /tmp below.
781
- const socketHash = crypto
782
- .createHash("sha256")
783
- .update(`${configDir}:${sessionName}:${process.pid}:${Date.now()}`)
784
- .digest("hex")
785
- .slice(0, 10);
786
- const socketRoot = path.join("/tmp", `tfx-cn-${socketHash}`);
787
- await fs.mkdir(socketRoot, { recursive: true });
788
- const startedAt = Date.now();
789
- const facades = [];
790
- const sentinels = [];
791
- const transportStops = [];
792
- const rosterWorkers = {};
793
- const byPaneName = new Map();
794
-
795
- try {
796
- for (let index = 0; index < assignments.length; index += 1) {
797
- const assignment = assignments[index] || {};
798
- const paneName = `worker-${index + 1}`;
799
- const cli = assignment.cli || "codex";
800
- const role = assignment.role || paneName;
801
- const short = crypto
802
- .createHash("sha256")
803
- .update(`${sessionName}:${index}:${startedAt}:${crypto.randomUUID()}`)
804
- .digest("hex")
805
- .slice(0, 8);
806
- const sentinel =
807
- pid == null
808
- ? spawn(process.execPath, ["-e", "setInterval(() => {}, 1 << 30)"], {
809
- stdio: "ignore",
810
- })
811
- : null;
812
- if (sentinel) {
813
- sentinel.unref();
814
- sentinels.push(sentinel);
815
- }
816
- const workerPid = pid || sentinel?.pid;
817
- if (!workerPid) throw new Error("native bridge sentinel pid unavailable");
818
- const rvSock = path.join(socketRoot, `${short}.rv.sock`);
819
- const ptySock = path.join(socketRoot, `${short}.pty.sock`);
820
- const workerCwd = assignment.cwd || assignment.workdir || cwd;
821
- const assignmentWorkerType = normalizeWorkerType(
822
- assignment.workerType || workerType,
823
- );
824
- let facade = null;
825
- if (assignmentWorkerType === "interactive") {
826
- const transport = createTransport({
827
- sessionName: buildInteractiveTransportSessionName(
828
- sessionName,
829
- paneName,
830
- short,
831
- ),
832
- cwd: workerCwd,
833
- launchCmd: assignment.launchCmd || launchCmd,
834
- env: assignment.env || env,
835
- onData(chunk) {
836
- if (facade) facade.writeOutput(chunk);
837
- },
838
- });
839
- const stopTransport = createStopOnce(transport);
840
- transportStops.push(stopTransport);
841
- facade = await startNativeWorkerFacade({
842
- short,
843
- rvSock,
844
- ptySock,
845
- pid: workerPid,
846
- initialDetail: `${cli}:${role}:interactive`,
847
- workerType: "interactive",
848
- onInput(payload) {
849
- void Promise.resolve()
850
- .then(() => transport.writeInput(payload))
851
- .catch(() => {});
852
- },
853
- onResize(size) {
854
- void Promise.resolve()
855
- .then(() => transport.resize(size))
856
- .catch(() => {});
857
- },
858
- onKill(event) {
859
- void stopTransport();
860
- if (typeof onKill === "function") onKill(event);
861
- },
862
- });
863
- try {
864
- await transport.start();
865
- } catch (error) {
866
- await stopTransport();
867
- await facade.close({ exitCode: 1 }).catch(() => {});
868
- throw error;
869
- }
870
- } else {
871
- facade = await startNativeWorkerFacade({
872
- short,
873
- rvSock,
874
- ptySock,
875
- pid: workerPid,
876
- initialDetail: `${cli}:${role}:pending`,
877
- onKill,
878
- });
879
- }
880
- rosterWorkers[short] = buildNativeWorkerRosterEntry({
881
- short,
882
- pid: workerPid,
883
- procStart: procStart || getProcStart(workerPid),
884
- cwd: workerCwd,
885
- startedAt,
886
- rendezvousSock: rvSock,
887
- ptySock,
888
- name: `Triflux ${cli} ${role}`,
889
- prompt: assignment.prompt || "",
890
- });
891
- facades.push(facade);
892
- byPaneName.set(paneName, {
893
- facade,
894
- cli,
895
- role,
896
- short,
897
- workerType: assignmentWorkerType,
898
- });
899
- }
900
-
901
- await mergeRosterWorkers(paths.rosterPath, rosterWorkers);
902
- } catch (error) {
903
- await Promise.all(transportStops.map((stop) => stop()));
904
- await Promise.all(facades.map((facade) => facade.close()));
905
- for (const sentinel of sentinels) {
906
- try {
907
- sentinel.kill("SIGTERM");
908
- } catch {
909
- /* already gone */
910
- }
911
- }
912
- await fs.rm(socketRoot, { recursive: true, force: true }).catch(() => {});
913
- throw error;
914
- }
915
-
916
- return {
917
- rosterPath: paths.rosterPath,
918
- controlSock: paths.controlSock,
919
- workers: Object.keys(rosterWorkers),
920
- handleProgress(event) {
921
- const worker = byPaneName.get(event?.paneName);
922
- if (!worker) return;
923
- if (worker.workerType === "interactive") return;
924
- if (event.type === "worker_added" || event.type === "dispatched") {
925
- worker.facade.update({
926
- state: "running",
927
- tempo: "busy",
928
- detail: `${worker.cli}:${worker.role}:${event.type}`,
929
- });
930
- } else if (event.type === "progress") {
931
- const lastLine =
932
- String(event.snapshot || "")
933
- .split("\n")
934
- .filter((line) => line.trim())
935
- .pop() || "";
936
- worker.facade.update({
937
- state: "running",
938
- tempo: "busy",
939
- detail: lastLine.slice(0, 160) || `${worker.cli}:running`,
940
- });
941
- if (lastLine) worker.facade.writeOutput(`${lastLine}\r\n`);
942
- } else if (event.type === "completed") {
943
- const ok = event.matched && event.exitCode === 0;
944
- worker.facade.update({
945
- state: "running",
946
- tempo: "idle",
947
- detail: ok ? `${worker.cli}:completed` : `${worker.cli}:failed`,
948
- });
949
- worker.facade.writeOutput(
950
- `triflux ${worker.cli} ${ok ? "completed" : "failed"} exit=${event.exitCode ?? "null"}\r\n`,
951
- );
952
- }
953
- },
954
- completeWorker(result) {
955
- const worker = byPaneName.get(result?.paneName);
956
- if (!worker) return;
957
- if (worker.workerType === "interactive") return;
958
- worker.facade.writeOutput(
959
- `\r\n${String(result.output || "").slice(0, 4096)}\r\n`,
960
- );
961
- worker.facade.update({
962
- state: "running",
963
- tempo: "idle",
964
- detail:
965
- result.matched && result.exitCode === 0
966
- ? `${worker.cli}:handoff-ready`
967
- : `${worker.cli}:needs-attention`,
968
- });
969
- },
970
- async close() {
971
- await Promise.all(transportStops.map((stop) => stop()));
972
- await Promise.all(facades.map((facade) => facade.close()));
973
- await removeRosterWorkers(paths.rosterPath, Object.keys(rosterWorkers));
974
- for (const sentinel of sentinels) {
975
- if (sentinel.exitCode !== null || sentinel.signalCode !== null) {
976
- continue;
977
- }
978
- try {
979
- sentinel.kill("SIGTERM");
980
- } catch {
981
- /* already gone */
982
- }
983
- }
984
- await fs.rm(socketRoot, { recursive: true, force: true }).catch(() => {});
985
- },
986
- };
987
- }