@triflux/remote 10.1.0 → 10.25.1

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 (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2165 -708
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +125 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +44 -16
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +20 -118
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1020 -279
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +68 -52
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +61 -33
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1522 -193
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +18 -34
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -44
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +927 -437
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +517 -63
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +662 -335
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,784 @@
1
+ import { execFileSync, 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
+ killDaemonJob,
10
+ sendClaudeControlRequest,
11
+ waitForDaemonJobPid,
12
+ } from "./claude-daemon-control.mjs";
13
+ import {
14
+ buildClaudeSessionProjection,
15
+ removeClaudeSessionProjection,
16
+ writeClaudeSessionProjection,
17
+ } from "./claude-session-projection.mjs";
18
+
19
+ const DEFAULT_ROWS = 40;
20
+ const DEFAULT_COLS = 120;
21
+ const MAX_TRANSCRIPT_BYTES = 64 * 1024;
22
+ const ROSTER_LOCK_STALE_MS = 30_000;
23
+ const ROSTER_LOCK_TIMEOUT_MS = 5_000;
24
+ const ROSTER_LOCK_RETRY_MS = 10;
25
+
26
+ export function resolveClaudeConfigDir(env = process.env) {
27
+ if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
28
+ return path.join(os.homedir(), ".claude");
29
+ }
30
+
31
+ export function deriveClaudeDaemonPaths({
32
+ configDir = resolveClaudeConfigDir(),
33
+ uid = typeof process.getuid === "function" ? process.getuid() : 0,
34
+ tmpRoot = "/tmp",
35
+ } = {}) {
36
+ const resolvedConfigDir = path.resolve(configDir);
37
+ const hash = crypto
38
+ .createHash("sha256")
39
+ .update(resolvedConfigDir)
40
+ .digest("hex")
41
+ .slice(0, 8);
42
+ const daemonDir = path.join(tmpRoot, `cc-daemon-${uid}`, hash);
43
+ return {
44
+ configDir: resolvedConfigDir,
45
+ daemonDir,
46
+ controlSock: path.join(daemonDir, "control.sock"),
47
+ rendezvousDir: path.join(daemonDir, "rv"),
48
+ ptyDir: path.join(daemonDir, "pty"),
49
+ rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
50
+ sessionsDir: path.join(resolvedConfigDir, "sessions"),
51
+ jobsDir: path.join(resolvedConfigDir, "jobs"),
52
+ };
53
+ }
54
+
55
+ export function getProcStart(pid = process.pid) {
56
+ if (!Number.isInteger(pid) || pid <= 0)
57
+ throw new Error(`invalid pid: ${pid}`);
58
+ return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
59
+ encoding: "utf8",
60
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
61
+ }).trim();
62
+ }
63
+
64
+ export function buildPtyDataFrame(value) {
65
+ const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
66
+ const frame = Buffer.allocUnsafe(5 + payload.length);
67
+ frame.writeUInt32BE(payload.length, 0);
68
+ frame.writeUInt8(0, 4);
69
+ payload.copy(frame, 5);
70
+ return frame;
71
+ }
72
+
73
+ export function buildPtyControlFrame(value) {
74
+ const payload = Buffer.from(JSON.stringify(value), "utf8");
75
+ const frame = Buffer.allocUnsafe(5 + payload.length);
76
+ frame.writeUInt32BE(payload.length, 0);
77
+ frame.writeUInt8(1, 4);
78
+ payload.copy(frame, 5);
79
+ return frame;
80
+ }
81
+
82
+ export function extractPtyFrames(buffer) {
83
+ const frames = [];
84
+ let rest = Buffer.from(buffer);
85
+ while (rest.length >= 5) {
86
+ const length = rest.readUInt32BE(0);
87
+ const frameLength = 5 + length;
88
+ if (rest.length < frameLength) break;
89
+ const kind = rest.readUInt8(4);
90
+ const payload = rest.subarray(5, frameLength);
91
+ if (kind === 0) frames.push({ kind, payload: Buffer.from(payload) });
92
+ else if (kind === 1) {
93
+ frames.push({ kind, ctrl: JSON.parse(payload.toString("utf8")) });
94
+ } else {
95
+ throw new Error(`unknown PTY frame kind ${kind}`);
96
+ }
97
+ rest = rest.subarray(frameLength);
98
+ }
99
+ return { frames, rest };
100
+ }
101
+
102
+ function trimTranscript(buffer) {
103
+ if (buffer.length <= MAX_TRANSCRIPT_BYTES) return buffer;
104
+ return buffer.subarray(buffer.length - MAX_TRANSCRIPT_BYTES);
105
+ }
106
+
107
+ function listen(server, sockPath) {
108
+ return new Promise((resolve, reject) => {
109
+ server.once("error", reject);
110
+ server.listen(sockPath, () => {
111
+ server.off("error", reject);
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+
117
+ function closeServer(server) {
118
+ return new Promise((resolve) => {
119
+ try {
120
+ server.close(() => resolve());
121
+ } catch {
122
+ resolve();
123
+ }
124
+ });
125
+ }
126
+
127
+ async function readRoster(rosterPath) {
128
+ try {
129
+ const parsed = JSON.parse(await fs.readFile(rosterPath, "utf8"));
130
+ return {
131
+ proto: parsed.proto || 1,
132
+ supervisorPid: parsed.supervisorPid || 0,
133
+ updatedAt: parsed.updatedAt || Date.now(),
134
+ workers:
135
+ parsed.workers && typeof parsed.workers === "object"
136
+ ? { ...parsed.workers }
137
+ : {},
138
+ };
139
+ } catch (error) {
140
+ if (error?.code !== "ENOENT") throw error;
141
+ return { proto: 1, supervisorPid: 0, updatedAt: Date.now(), workers: {} };
142
+ }
143
+ }
144
+
145
+ async function writeRoster(rosterPath, roster) {
146
+ await fs.mkdir(path.dirname(rosterPath), { recursive: true });
147
+ const tmpPath = `${rosterPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
148
+ const next = { ...roster, proto: 1, updatedAt: Date.now() };
149
+ await fs.writeFile(tmpPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
150
+ await fs.rename(tmpPath, rosterPath);
151
+ }
152
+
153
+ function sleep(ms) {
154
+ return new Promise((resolve) => setTimeout(resolve, ms));
155
+ }
156
+
157
+ async function acquireRosterLock(rosterPath) {
158
+ await fs.mkdir(path.dirname(rosterPath), { recursive: true });
159
+ const lockPath = `${rosterPath}.lock`;
160
+ const startedAt = Date.now();
161
+ while (true) {
162
+ try {
163
+ await fs.mkdir(lockPath);
164
+ return async () => {
165
+ await fs.rm(lockPath, { recursive: true, force: true });
166
+ };
167
+ } catch (error) {
168
+ if (error?.code !== "EEXIST") throw error;
169
+ if (Date.now() - startedAt > ROSTER_LOCK_TIMEOUT_MS) {
170
+ throw new Error(`Timed out waiting for roster lock: ${lockPath}`);
171
+ }
172
+ try {
173
+ const stat = await fs.stat(lockPath);
174
+ if (Date.now() - stat.mtimeMs > ROSTER_LOCK_STALE_MS) {
175
+ await fs.rm(lockPath, { recursive: true, force: true });
176
+ continue;
177
+ }
178
+ } catch (statError) {
179
+ if (statError?.code !== "ENOENT") throw statError;
180
+ }
181
+ await sleep(ROSTER_LOCK_RETRY_MS);
182
+ }
183
+ }
184
+ }
185
+
186
+ async function withRosterLock(rosterPath, fn) {
187
+ const release = await acquireRosterLock(rosterPath);
188
+ try {
189
+ return await fn();
190
+ } finally {
191
+ await release();
192
+ }
193
+ }
194
+
195
+ export async function mergeRosterWorkers(rosterPath, workers) {
196
+ return withRosterLock(rosterPath, async () => {
197
+ const roster = await readRoster(rosterPath);
198
+ roster.workers = { ...roster.workers, ...workers };
199
+ await writeRoster(rosterPath, roster);
200
+ return roster;
201
+ });
202
+ }
203
+
204
+ export async function removeRosterWorkers(rosterPath, shorts) {
205
+ return withRosterLock(rosterPath, async () => {
206
+ const roster = await readRoster(rosterPath);
207
+ let changed = false;
208
+ for (const short of shorts) {
209
+ if (Object.hasOwn(roster.workers, short)) {
210
+ delete roster.workers[short];
211
+ changed = true;
212
+ }
213
+ }
214
+ if (changed) await writeRoster(rosterPath, roster);
215
+ return changed;
216
+ });
217
+ }
218
+
219
+ export async function removeClaudeJobState(jobsDir, short) {
220
+ if (!short) return;
221
+ await fs.rm(path.join(jobsDir, short), { recursive: true, force: true });
222
+ }
223
+
224
+ function shellSingleQuote(value) {
225
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
226
+ }
227
+
228
+ function buildSwarmShardBridgeCommand({ displayName, sessionId }) {
229
+ const banner = `${displayName} native bridge active (${sessionId})`;
230
+ return `printf '%s\\n' ${shellSingleQuote(banner)}; while true; do sleep 3600; done`;
231
+ }
232
+
233
+ function compactDisplayPart(value, fallback = "") {
234
+ const text = String(value ?? "")
235
+ .replace(/[\u0000-\u001f\u007f]+/g, " ")
236
+ .replace(/\s+/g, " ")
237
+ .trim();
238
+ return text || fallback;
239
+ }
240
+
241
+ function toPositiveInteger(value) {
242
+ const number = Number(value);
243
+ return Number.isInteger(number) && number > 0 ? number : null;
244
+ }
245
+
246
+ function formatShardOrdinal({ shardIndex, shardCount }) {
247
+ const index = toPositiveInteger(shardIndex);
248
+ const count = toPositiveInteger(shardCount);
249
+ if (!index || !count) return null;
250
+ const width = Math.max(String(count).length, 2);
251
+ return `${String(index).padStart(width, "0")}/${String(count).padStart(width, "0")}`;
252
+ }
253
+
254
+ function buildSwarmShardDisplayName({
255
+ sessionId,
256
+ cli,
257
+ swarmName,
258
+ shardIndex,
259
+ shardCount,
260
+ shardName,
261
+ }) {
262
+ const groupName = compactDisplayPart(swarmName || sessionId, "swarm");
263
+ const agentName = compactDisplayPart(cli, "agent");
264
+ const workerName = compactDisplayPart(shardName, "shard");
265
+ return [
266
+ "Triflux swarm",
267
+ groupName,
268
+ formatShardOrdinal({ shardIndex, shardCount }),
269
+ agentName,
270
+ workerName,
271
+ ]
272
+ .filter(Boolean)
273
+ .join(" ");
274
+ }
275
+
276
+ function isLocalHost(host) {
277
+ const value = String(host || "local").toLowerCase();
278
+ return value === "local" || value === "localhost" || value === "127.0.0.1";
279
+ }
280
+
281
+ function deriveSwarmShort({ sessionId, shardName, host }) {
282
+ return crypto
283
+ .createHash("sha256")
284
+ .update(`${sessionId}:${shardName}:${host || "local"}`)
285
+ .digest("hex")
286
+ .slice(0, 8);
287
+ }
288
+
289
+ export async function registerSwarmShard({
290
+ sessionId,
291
+ cli = "codex",
292
+ role = "worker",
293
+ swarmName,
294
+ shardIndex,
295
+ shardCount,
296
+ shardName,
297
+ cwd = process.cwd(),
298
+ host = "local",
299
+ configDir = resolveClaudeConfigDir(),
300
+ tmpRoot = "/tmp",
301
+ _deps = {},
302
+ } = {}) {
303
+ if (!sessionId) throw new Error("sessionId is required");
304
+ if (!shardName) throw new Error("shardName is required");
305
+
306
+ const displayName = buildSwarmShardDisplayName({
307
+ sessionId,
308
+ cli,
309
+ swarmName,
310
+ shardIndex,
311
+ shardCount,
312
+ shardName,
313
+ });
314
+ const warn = _deps.warn || ((message) => console.warn(message));
315
+ if (!isLocalHost(host)) {
316
+ warn(
317
+ `[native-bridge] remote shard ${shardName}@${host}: skipping local registration; remote launcher must register on that host`,
318
+ );
319
+ return {
320
+ ok: true,
321
+ skipped: true,
322
+ host,
323
+ sessionId,
324
+ shardName,
325
+ displayName,
326
+ close() {},
327
+ };
328
+ }
329
+
330
+ const derivePaths = _deps.deriveClaudeDaemonPaths || deriveClaudeDaemonPaths;
331
+ const buildPayload =
332
+ _deps.buildDaemonExecDispatchPayload || buildDaemonExecDispatchPayload;
333
+ const sendControl =
334
+ _deps.sendClaudeControlRequest || sendClaudeControlRequest;
335
+ const waitForPid = _deps.waitForDaemonJobPid || waitForDaemonJobPid;
336
+ const readProcStart = _deps.getProcStart || getProcStart;
337
+ const buildProjection =
338
+ _deps.buildClaudeSessionProjection || buildClaudeSessionProjection;
339
+ const writeProjection =
340
+ _deps.writeClaudeSessionProjection || writeClaudeSessionProjection;
341
+ const removeProjection =
342
+ _deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
343
+ const removeJobStateImpl = _deps.removeClaudeJobState || removeClaudeJobState;
344
+ const killJob = _deps.killDaemonJob || killDaemonJob;
345
+ const accessControlSock = _deps.accessControlSock || fs.access;
346
+
347
+ const paths = derivePaths({ configDir, tmpRoot });
348
+ const sessionsDir =
349
+ paths.sessionsDir || path.join(paths.configDir || configDir, "sessions");
350
+ const jobsDir = paths.jobsDir || path.join(paths.configDir || configDir, "jobs");
351
+ const short = deriveSwarmShort({ sessionId, shardName, host });
352
+ const command = buildSwarmShardBridgeCommand({ displayName, sessionId });
353
+ const payload = buildPayload({
354
+ short,
355
+ cwd,
356
+ command,
357
+ name: displayName,
358
+ });
359
+
360
+ await accessControlSock(paths.controlSock);
361
+ const dispatch = await sendControl(
362
+ paths.controlSock,
363
+ {
364
+ proto: 1,
365
+ op: "dispatch",
366
+ d: payload,
367
+ timeoutMs: 1000,
368
+ },
369
+ { timeoutMs: 1000 },
370
+ );
371
+ if (dispatch?.ok !== true) {
372
+ throw new Error(
373
+ `Claude daemon dispatch failed for swarm shard ${shardName}`,
374
+ );
375
+ }
376
+
377
+ const job = await waitForPid(paths.controlSock, short, { timeoutMs: 1000 });
378
+ const pid = job.pid;
379
+ const projection = buildProjection({
380
+ pid,
381
+ procStart: readProcStart(pid),
382
+ sessionId: payload.sessionId,
383
+ short,
384
+ cwd,
385
+ name: displayName,
386
+ agent: cli,
387
+ startedAt: job.startedAt || Date.now(),
388
+ updatedAt: Date.now(),
389
+ });
390
+ const sessionProjectionPath = await writeProjection(sessionsDir, projection);
391
+ let closed = false;
392
+
393
+ return {
394
+ ok: true,
395
+ skipped: false,
396
+ host: "local",
397
+ sessionId: payload.sessionId,
398
+ swarmSessionId: sessionId,
399
+ shardName,
400
+ cli,
401
+ role,
402
+ short,
403
+ displayName,
404
+ controlSock: paths.controlSock,
405
+ sessionProjectionPath,
406
+ async close() {
407
+ if (closed) return;
408
+ closed = true;
409
+ await removeProjection(sessionProjectionPath).catch(() => {});
410
+ await killJob(paths.controlSock, short).catch(() => {});
411
+ await removeJobStateImpl(jobsDir, short).catch(() => {});
412
+ },
413
+ };
414
+ }
415
+
416
+ export function buildNativeWorkerRosterEntry({
417
+ short,
418
+ pid = process.pid,
419
+ procStart,
420
+ cwd = process.cwd(),
421
+ startedAt = Date.now(),
422
+ rendezvousSock,
423
+ ptySock,
424
+ sessionId = crypto.randomUUID(),
425
+ cliVersion = "triflux-native-bridge",
426
+ name = "Triflux native worker",
427
+ prompt = "",
428
+ cols = DEFAULT_COLS,
429
+ rows = DEFAULT_ROWS,
430
+ }) {
431
+ if (!short) throw new Error("short is required");
432
+ if (!rendezvousSock) throw new Error("rendezvousSock is required");
433
+ if (!ptySock) throw new Error("ptySock is required");
434
+ return {
435
+ pid,
436
+ procStart: procStart || getProcStart(pid),
437
+ sessionId,
438
+ rendezvousSock,
439
+ ptySock,
440
+ cliVersion,
441
+ startedAt,
442
+ attempt: 1,
443
+ cwd,
444
+ dispatch: {
445
+ proto: 1,
446
+ short,
447
+ sessionId,
448
+ createdAt: startedAt,
449
+ source: "shell",
450
+ cwd,
451
+ launch: { mode: "prompt", args: ["--", prompt || name] },
452
+ env: {},
453
+ isolation: "none",
454
+ respawnFlags: [],
455
+ seed: {
456
+ intent: prompt || name,
457
+ name,
458
+ },
459
+ cols,
460
+ rows,
461
+ },
462
+ };
463
+ }
464
+
465
+ export async function startNativeWorkerFacade({
466
+ short,
467
+ rvSock,
468
+ ptySock,
469
+ pid = process.pid,
470
+ version = "triflux-native-bridge",
471
+ initialDetail = "pending",
472
+ onKill,
473
+ } = {}) {
474
+ if (!short) throw new Error("short is required");
475
+ if (!rvSock) throw new Error("rvSock is required");
476
+ if (!ptySock) throw new Error("ptySock is required");
477
+
478
+ await fs.mkdir(path.dirname(rvSock), { recursive: true });
479
+ await fs.mkdir(path.dirname(ptySock), { recursive: true });
480
+ await fs.rm(rvSock, { force: true }).catch(() => {});
481
+ await fs.rm(ptySock, { force: true }).catch(() => {});
482
+
483
+ const rvSockets = new Set();
484
+ const ptySockets = new Set();
485
+ const events = [];
486
+ let state = { state: "running", tempo: "idle", detail: initialDetail };
487
+ let transcript = Buffer.alloc(0);
488
+ let closed = false;
489
+
490
+ const writeJsonLine = (socket, message) => {
491
+ socket.write(`${JSON.stringify(message)}\n`);
492
+ };
493
+ const broadcastState = (patch = {}) => {
494
+ state = { ...state, ...patch };
495
+ for (const socket of rvSockets) {
496
+ writeJsonLine(socket, { type: "heartbeat" });
497
+ writeJsonLine(socket, { type: "state", patch: state });
498
+ }
499
+ };
500
+ const broadcastPty = (frame) => {
501
+ for (const socket of ptySockets) socket.write(frame);
502
+ };
503
+ const appendOutput = (value) => {
504
+ const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
505
+ transcript = trimTranscript(Buffer.concat([transcript, payload]));
506
+ broadcastPty(buildPtyDataFrame(payload));
507
+ };
508
+
509
+ const rvServer = net.createServer((socket) => {
510
+ rvSockets.add(socket);
511
+ events.push({ type: "rv-connect" });
512
+ socket.setEncoding("utf8");
513
+ socket.once("close", () => rvSockets.delete(socket));
514
+ writeJsonLine(socket, { type: "heartbeat" });
515
+ writeJsonLine(socket, { type: "state", patch: state });
516
+ socket.on("data", (chunk) => {
517
+ for (const line of chunk.split("\n")) {
518
+ if (!line.trim()) continue;
519
+ let message;
520
+ try {
521
+ message = JSON.parse(line);
522
+ } catch (error) {
523
+ events.push({ type: "rv-error", message: error.message });
524
+ continue;
525
+ }
526
+ events.push({ type: "rv-message", message });
527
+ if (message.type === "shutdown") {
528
+ writeJsonLine(socket, { type: "shutting-down" });
529
+ socket.end();
530
+ if (typeof onKill === "function") onKill({ source: "rv", message });
531
+ } else if (message.type === "repaint") {
532
+ writeJsonLine(socket, { type: "repaint-done" });
533
+ } else if (message.type === "reply") {
534
+ broadcastState({
535
+ tempo: "idle",
536
+ detail: `reply:${String(message.text || "").slice(0, 120)}`,
537
+ });
538
+ } else {
539
+ writeJsonLine(socket, { type: "ack", requestType: message.type });
540
+ }
541
+ }
542
+ });
543
+ });
544
+
545
+ const ptyServer = net.createServer((socket) => {
546
+ ptySockets.add(socket);
547
+ events.push({ type: "pty-connect" });
548
+ socket.once("close", () => ptySockets.delete(socket));
549
+ socket.write(buildPtyControlFrame({ t: "hello", replPid: pid, version }));
550
+ if (transcript.length > 0) socket.write(buildPtyDataFrame(transcript));
551
+ socket.write(buildPtyControlFrame({ t: "live" }));
552
+ let ptyBuffer = Buffer.alloc(0);
553
+ socket.on("data", (chunk) => {
554
+ try {
555
+ const result = extractPtyFrames(Buffer.concat([ptyBuffer, chunk]));
556
+ ptyBuffer = result.rest;
557
+ for (const frame of result.frames) {
558
+ events.push({
559
+ type: "pty-frame",
560
+ frame:
561
+ frame.kind === 1
562
+ ? frame.ctrl
563
+ : { kind: 0, bytes: frame.payload.length },
564
+ });
565
+ if (frame.kind === 0) appendOutput(frame.payload);
566
+ else if (frame.ctrl.t === "resize") {
567
+ socket.write(buildPtyControlFrame({ t: "live" }));
568
+ } else if (frame.ctrl.t === "kill") {
569
+ socket.write(
570
+ buildPtyControlFrame({
571
+ t: "exit",
572
+ code: 0,
573
+ signal: frame.ctrl.sig,
574
+ }),
575
+ );
576
+ socket.end();
577
+ if (typeof onKill === "function") {
578
+ onKill({ source: "pty", message: frame.ctrl });
579
+ }
580
+ }
581
+ }
582
+ } catch (error) {
583
+ events.push({ type: "pty-error", message: error.message });
584
+ socket.destroy(error);
585
+ }
586
+ });
587
+ });
588
+
589
+ try {
590
+ await listen(rvServer, rvSock);
591
+ await listen(ptyServer, ptySock);
592
+ } catch (error) {
593
+ await Promise.all([closeServer(rvServer), closeServer(ptyServer)]);
594
+ await fs.rm(rvSock, { force: true }).catch(() => {});
595
+ await fs.rm(ptySock, { force: true }).catch(() => {});
596
+ throw error;
597
+ }
598
+
599
+ return {
600
+ short,
601
+ rvSock,
602
+ ptySock,
603
+ getEvents() {
604
+ return events.slice();
605
+ },
606
+ update(patch) {
607
+ if (closed) return;
608
+ broadcastState(patch);
609
+ },
610
+ writeOutput(value) {
611
+ if (closed) return;
612
+ appendOutput(value);
613
+ },
614
+ async close({ exitCode = 0 } = {}) {
615
+ if (closed) return;
616
+ closed = true;
617
+ broadcastPty(buildPtyControlFrame({ t: "exit", code: exitCode }));
618
+ for (const socket of rvSockets) socket.destroy();
619
+ for (const socket of ptySockets) socket.destroy();
620
+ await Promise.all([closeServer(rvServer), closeServer(ptyServer)]);
621
+ await fs.rm(rvSock, { force: true }).catch(() => {});
622
+ await fs.rm(ptySock, { force: true }).catch(() => {});
623
+ },
624
+ };
625
+ }
626
+
627
+ export async function startClaudeNativeBridge({
628
+ sessionName,
629
+ assignments,
630
+ configDir = resolveClaudeConfigDir(),
631
+ cwd = process.cwd(),
632
+ pid,
633
+ procStart,
634
+ tmpRoot = os.tmpdir(),
635
+ onKill,
636
+ } = {}) {
637
+ if (!sessionName) throw new Error("sessionName is required");
638
+ const paths = deriveClaudeDaemonPaths({ configDir, tmpRoot: "/tmp" });
639
+ void tmpRoot; // UDS sockets must stay short on macOS; use /tmp below.
640
+ const socketHash = crypto
641
+ .createHash("sha256")
642
+ .update(`${configDir}:${sessionName}:${process.pid}:${Date.now()}`)
643
+ .digest("hex")
644
+ .slice(0, 10);
645
+ const socketRoot = path.join("/tmp", `tfx-cn-${socketHash}`);
646
+ await fs.mkdir(socketRoot, { recursive: true });
647
+ const startedAt = Date.now();
648
+ const facades = [];
649
+ const sentinels = [];
650
+ const rosterWorkers = {};
651
+ const byPaneName = new Map();
652
+
653
+ try {
654
+ for (let index = 0; index < assignments.length; index += 1) {
655
+ const assignment = assignments[index] || {};
656
+ const paneName = `worker-${index + 1}`;
657
+ const cli = assignment.cli || "codex";
658
+ const role = assignment.role || paneName;
659
+ const short = crypto
660
+ .createHash("sha256")
661
+ .update(`${sessionName}:${index}:${startedAt}:${crypto.randomUUID()}`)
662
+ .digest("hex")
663
+ .slice(0, 8);
664
+ const sentinel =
665
+ pid == null
666
+ ? spawn(process.execPath, ["-e", "setInterval(() => {}, 1 << 30)"], {
667
+ stdio: "ignore",
668
+ })
669
+ : null;
670
+ if (sentinel) {
671
+ sentinel.unref();
672
+ sentinels.push(sentinel);
673
+ }
674
+ const workerPid = pid || sentinel?.pid;
675
+ if (!workerPid) throw new Error("native bridge sentinel pid unavailable");
676
+ const rvSock = path.join(socketRoot, `${short}.rv.sock`);
677
+ const ptySock = path.join(socketRoot, `${short}.pty.sock`);
678
+ const facade = await startNativeWorkerFacade({
679
+ short,
680
+ rvSock,
681
+ ptySock,
682
+ pid: workerPid,
683
+ initialDetail: `${cli}:${role}:pending`,
684
+ onKill,
685
+ });
686
+ const workerCwd = assignment.cwd || assignment.workdir || cwd;
687
+ rosterWorkers[short] = buildNativeWorkerRosterEntry({
688
+ short,
689
+ pid: workerPid,
690
+ procStart: procStart || getProcStart(workerPid),
691
+ cwd: workerCwd,
692
+ startedAt,
693
+ rendezvousSock: rvSock,
694
+ ptySock,
695
+ name: `Triflux ${cli} ${role}`,
696
+ prompt: assignment.prompt || "",
697
+ });
698
+ facades.push(facade);
699
+ byPaneName.set(paneName, { facade, cli, role, short });
700
+ }
701
+
702
+ await mergeRosterWorkers(paths.rosterPath, rosterWorkers);
703
+ } catch (error) {
704
+ await Promise.all(facades.map((facade) => facade.close()));
705
+ for (const sentinel of sentinels) {
706
+ try {
707
+ sentinel.kill("SIGTERM");
708
+ } catch {
709
+ /* already gone */
710
+ }
711
+ }
712
+ await fs.rm(socketRoot, { recursive: true, force: true }).catch(() => {});
713
+ throw error;
714
+ }
715
+
716
+ return {
717
+ rosterPath: paths.rosterPath,
718
+ controlSock: paths.controlSock,
719
+ workers: Object.keys(rosterWorkers),
720
+ handleProgress(event) {
721
+ const worker = byPaneName.get(event?.paneName);
722
+ if (!worker) return;
723
+ if (event.type === "worker_added" || event.type === "dispatched") {
724
+ worker.facade.update({
725
+ state: "running",
726
+ tempo: "busy",
727
+ detail: `${worker.cli}:${worker.role}:${event.type}`,
728
+ });
729
+ } else if (event.type === "progress") {
730
+ const lastLine =
731
+ String(event.snapshot || "")
732
+ .split("\n")
733
+ .filter((line) => line.trim())
734
+ .pop() || "";
735
+ worker.facade.update({
736
+ state: "running",
737
+ tempo: "busy",
738
+ detail: lastLine.slice(0, 160) || `${worker.cli}:running`,
739
+ });
740
+ if (lastLine) worker.facade.writeOutput(`${lastLine}\r\n`);
741
+ } else if (event.type === "completed") {
742
+ const ok = event.matched && event.exitCode === 0;
743
+ worker.facade.update({
744
+ state: "running",
745
+ tempo: "idle",
746
+ detail: ok ? `${worker.cli}:completed` : `${worker.cli}:failed`,
747
+ });
748
+ worker.facade.writeOutput(
749
+ `triflux ${worker.cli} ${ok ? "completed" : "failed"} exit=${event.exitCode ?? "null"}\r\n`,
750
+ );
751
+ }
752
+ },
753
+ completeWorker(result) {
754
+ const worker = byPaneName.get(result?.paneName);
755
+ if (!worker) return;
756
+ worker.facade.writeOutput(
757
+ `\r\n${String(result.output || "").slice(0, 4096)}\r\n`,
758
+ );
759
+ worker.facade.update({
760
+ state: "running",
761
+ tempo: "idle",
762
+ detail:
763
+ result.matched && result.exitCode === 0
764
+ ? `${worker.cli}:handoff-ready`
765
+ : `${worker.cli}:needs-attention`,
766
+ });
767
+ },
768
+ async close() {
769
+ await Promise.all(facades.map((facade) => facade.close()));
770
+ await removeRosterWorkers(paths.rosterPath, Object.keys(rosterWorkers));
771
+ for (const sentinel of sentinels) {
772
+ if (sentinel.exitCode !== null || sentinel.signalCode !== null) {
773
+ continue;
774
+ }
775
+ try {
776
+ sentinel.kill("SIGTERM");
777
+ } catch {
778
+ /* already gone */
779
+ }
780
+ }
781
+ await fs.rm(socketRoot, { recursive: true, force: true }).catch(() => {});
782
+ },
783
+ };
784
+ }