@triflux/remote 10.50.2 → 10.51.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 (97) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +0 -73
  3. package/cto/current.schema.json +0 -4
  4. package/cto/status.mjs +2 -50
  5. package/hub/index.mjs +0 -8
  6. package/hub/team/agent-map.json +0 -1
  7. package/hub/team/ansi.mjs +1 -35
  8. package/hub/team/backend.mjs +9 -145
  9. package/hub/team/cli/commands/attach.mjs +1 -17
  10. package/hub/team/cli/commands/control.mjs +12 -38
  11. package/hub/team/cli/commands/debug.mjs +1 -37
  12. package/hub/team/cli/commands/focus.mjs +5 -31
  13. package/hub/team/cli/commands/interrupt.mjs +6 -25
  14. package/hub/team/cli/commands/kill.mjs +1 -30
  15. package/hub/team/cli/commands/list.mjs +0 -16
  16. package/hub/team/cli/commands/send.mjs +6 -26
  17. package/hub/team/cli/commands/start/index.mjs +41 -133
  18. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  19. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  20. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  21. package/hub/team/cli/commands/status.mjs +2 -57
  22. package/hub/team/cli/commands/stop.mjs +14 -19
  23. package/hub/team/cli/help.mjs +4 -5
  24. package/hub/team/cli/manifest.mjs +5 -1
  25. package/hub/team/cli/render.mjs +0 -12
  26. package/hub/team/cli/services/hub-client.mjs +2 -88
  27. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  28. package/hub/team/handoff.mjs +3 -34
  29. package/hub/team/headless.mjs +13 -754
  30. package/hub/team/orchestrator.mjs +4 -30
  31. package/hub/team/pane.mjs +6 -28
  32. package/hub/team/psmux.mjs +0 -1
  33. package/hub/team/remote-session.mjs +1 -2
  34. package/hub/team/session.mjs +7 -91
  35. package/hub/team/swarm-intent.mjs +0 -198
  36. package/hub/team/tui-core.mjs +1 -1
  37. package/hub/team/tui-viewer.mjs +2 -2
  38. package/hub/team/tui.mjs +8 -269
  39. package/hub/team/wt-manager.mjs +2 -2
  40. package/hub/workers/factory.mjs +4 -37
  41. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  42. package/package.json +1 -1
  43. package/scripts/lib/agent-route-policy.mjs +162 -14
  44. package/scripts/lib/claudemd-scanner.mjs +2 -199
  45. package/scripts/lib/cli-claude.mjs +0 -2
  46. package/scripts/lib/context.mjs +3 -19
  47. package/scripts/lib/env-probe.mjs +5 -64
  48. package/scripts/lib/gemini-profiles.mjs +2 -69
  49. package/scripts/lib/machine-profile.mjs +3 -17
  50. package/scripts/lib/mcp-filter.mjs +109 -53
  51. package/scripts/lib/mcp-health.mjs +2 -2
  52. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  53. package/scripts/lib/psmux-info.mjs +11 -59
  54. package/scripts/lib/skill-template.mjs +0 -245
  55. package/hub/team/build-worker-prompt.mjs +0 -92
  56. package/hub/team/check-mcp-hub.mjs +0 -49
  57. package/hub/team/claude-native-bridge.mjs +0 -987
  58. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  59. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  60. package/hub/team/cli/services/native-control.mjs +0 -145
  61. package/hub/team/codex-review.mjs +0 -520
  62. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  63. package/hub/team/conductor.mjs +0 -1529
  64. package/hub/team/consensus-meta.mjs +0 -176
  65. package/hub/team/cto-pull.mjs +0 -31
  66. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  67. package/hub/team/dashboard.mjs +0 -322
  68. package/hub/team/event-log.mjs +0 -84
  69. package/hub/team/execution-mode.mjs +0 -324
  70. package/hub/team/extract-completion-payload.mjs +0 -110
  71. package/hub/team/headless-bridge-session.mjs +0 -17
  72. package/hub/team/health-probe.mjs +0 -435
  73. package/hub/team/interactive-native-launcher.mjs +0 -119
  74. package/hub/team/interactive-native-worker.mjs +0 -80
  75. package/hub/team/interactive-tui-transport.mjs +0 -479
  76. package/hub/team/launcher-template.mjs +0 -144
  77. package/hub/team/mcp-selector.mjs +0 -145
  78. package/hub/team/native-supervisor.mjs +0 -460
  79. package/hub/team/native.mjs +0 -727
  80. package/hub/team/recovery-store.mjs +0 -149
  81. package/hub/team/remote-probe.mjs +0 -292
  82. package/hub/team/remote-watcher.mjs +0 -506
  83. package/hub/team/runtime-strategy.mjs +0 -200
  84. package/hub/team/schemas/worker-completion.json +0 -39
  85. package/hub/team/sentinel-capture.mjs +0 -127
  86. package/hub/team/swarm-cli.mjs +0 -390
  87. package/hub/team/swarm-hypervisor.mjs +0 -2206
  88. package/hub/team/swarm-locks.mjs +0 -335
  89. package/hub/team/swarm-planner.mjs +0 -371
  90. package/hub/team/swarm-preflight.mjs +0 -366
  91. package/hub/team/swarm-reconciler.mjs +0 -242
  92. package/hub/team/tui-remote-adapter.mjs +0 -377
  93. package/hub/team/worker-completion-validator.mjs +0 -79
  94. package/hub/team/worktree-lifecycle.mjs +0 -639
  95. package/hub/team/wt-templates.json +0 -43
  96. package/hub/workers/antigravity-route-worker.mjs +0 -179
  97. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -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
- }