@triflux/remote 10.0.0-alpha.2 → 10.0.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.
package/hub/index.mjs CHANGED
@@ -18,4 +18,4 @@ export { createSwarmLocks } from './team/swarm-locks.mjs';
18
18
  export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from './team/swarm-planner.mjs';
19
19
  export { createSwarmHypervisor, SWARM_STATES } from './team/swarm-hypervisor.mjs';
20
20
  export { reconcile, buildRedundantIds, shouldRunRedundant } from './team/swarm-reconciler.mjs';
21
- export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree } from './team/worktree-lifecycle.mjs';
21
+ export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree, fetchRemoteShard } from './team/worktree-lifecycle.mjs';
package/hub/server.mjs CHANGED
@@ -63,6 +63,53 @@ const STATIC_CONTENT_TYPES = Object.freeze({
63
63
  // Each entry is an array of request timestamps within the current window.
64
64
  const rateLimitMap = new Map();
65
65
 
66
+ function formatHostForUrl(host = '127.0.0.1') {
67
+ return String(host).includes(':') ? `[${host}]` : host;
68
+ }
69
+
70
+ function buildHubUrl(host, port) {
71
+ return `http://${formatHostForUrl(host || '127.0.0.1')}:${port}/mcp`;
72
+ }
73
+
74
+ function isPidAlive(pid, killFn = process.kill) {
75
+ const resolvedPid = Number(pid);
76
+ if (!Number.isFinite(resolvedPid) || resolvedPid <= 0) return false;
77
+ try {
78
+ killFn(resolvedPid, 0);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ async function tryReuseExistingHub({
86
+ port,
87
+ host = '127.0.0.1',
88
+ readCurrentState = readState,
89
+ readInfo = getHubInfo,
90
+ checkHealth = isServerHealthy,
91
+ killFn = process.kill,
92
+ } = {}) {
93
+ const existing = readCurrentState();
94
+ const existingPort = Number(existing?.port);
95
+ if (!isPidAlive(existing?.pid, killFn) || !Number.isFinite(existingPort) || existingPort <= 0) {
96
+ return null;
97
+ }
98
+ if (Number.isFinite(Number(port)) && existingPort !== Number(port)) return null;
99
+ if (!(await checkHealth(existingPort))) return null;
100
+
101
+ const info = readInfo() ?? existing;
102
+ const infoHost = typeof info?.host === 'string' ? info.host : host;
103
+ return {
104
+ reused: true,
105
+ external: true,
106
+ port: existingPort,
107
+ pid: Number(existing.pid),
108
+ url: info?.url ?? buildHubUrl(infoHost, existingPort),
109
+ stop: async () => false,
110
+ };
111
+ }
112
+
66
113
  function checkRateLimit(ip) {
67
114
  const now = Date.now();
68
115
  const cutoff = now - RATE_LIMIT_WINDOW_MS;
@@ -314,24 +361,8 @@ export async function startHub({
314
361
  } = {}) {
315
362
  const port = portOpt ?? parseInt(process.env.TFX_HUB_PORT || '27888', 10);
316
363
 
317
- // Singleton reuse: if a healthy hub is already running on the target port, return a lightweight handle
318
- const existingState = readState();
319
- if (existingState?.pid && Number(existingState.port) === Number(port)) {
320
- const pidAlive = (() => {
321
- try { process.kill(Number(existingState.pid), 0); return true; } catch { return false; }
322
- })();
323
- if (pidAlive && await isServerHealthy(port)) {
324
- const existingInfo = getHubInfo();
325
- return {
326
- reused: true,
327
- external: true,
328
- port: Number(port),
329
- pid: Number(existingState.pid),
330
- url: existingInfo?.url ?? `http://${host}:${port}/mcp`,
331
- stop: async () => false,
332
- };
333
- }
334
- }
364
+ const existingHub = await tryReuseExistingHub({ port, host });
365
+ if (existingHub) return existingHub;
335
366
 
336
367
  const hubIdleTimeoutMs = parsePositiveInt(process.env.TFX_HUB_IDLE_TIMEOUT_MS, HUB_IDLE_TIMEOUT_DEFAULT_MS);
337
368
  const hubIdleSweepMs = parsePositiveInt(
@@ -365,6 +396,12 @@ export async function startHub({
365
396
  lockHeld = false;
366
397
  };
367
398
 
399
+ const lockedExistingHub = await tryReuseExistingHub({ port, host });
400
+ if (lockedExistingHub) {
401
+ releaseStartupLock();
402
+ return lockedExistingHub;
403
+ }
404
+
368
405
  const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
369
406
  if (HUB_TOKEN) {
370
407
  mkdirSync(join(homedir(), '.claude'), { recursive: true });
@@ -1019,7 +1056,7 @@ export async function startHub({
1019
1056
  pid: process.pid,
1020
1057
  hubToken: HUB_TOKEN,
1021
1058
  authMode: HUB_TOKEN ? 'token-required' : 'localhost-only',
1022
- url: `http://${host}:${port}/mcp`,
1059
+ url: buildHubUrl(host, port),
1023
1060
  pipe_path: pipe.path,
1024
1061
  pipePath: pipe.path,
1025
1062
  assign_callback_pipe_path: assignCallbacks.path,
@@ -1029,7 +1066,7 @@ export async function startHub({
1029
1066
  idleTimeoutMs: hubIdleTimeoutMs,
1030
1067
  };
1031
1068
 
1032
- writeFileSync(PID_FILE, JSON.stringify({
1069
+ writeState({
1033
1070
  pid: process.pid,
1034
1071
  port,
1035
1072
  host,
@@ -1038,16 +1075,13 @@ export async function startHub({
1038
1075
  pipe_path: pipe.path,
1039
1076
  pipePath: pipe.path,
1040
1077
  assign_callback_pipe_path: assignCallbacks.path,
1078
+ assignCallbackPipePath: assignCallbacks.path,
1079
+ authMode: HUB_TOKEN ? 'token-required' : 'localhost-only',
1080
+ startedAt,
1041
1081
  started: startedAtMs,
1042
1082
  version,
1043
- session_id: sessionId,
1044
- }));
1045
- writeState({
1046
- pid: process.pid,
1047
- port,
1048
- version,
1049
1083
  sessionId,
1050
- startedAt,
1084
+ session_id: sessionId,
1051
1085
  });
1052
1086
  releaseStartupLock();
1053
1087
 
@@ -1128,12 +1162,7 @@ export async function startHub({
1128
1162
  }
1129
1163
 
1130
1164
  export function getHubInfo() {
1131
- if (!existsSync(PID_FILE)) return null;
1132
- try {
1133
- return JSON.parse(readFileSync(PID_FILE, 'utf8'));
1134
- } catch {
1135
- return null;
1136
- }
1165
+ return readState();
1137
1166
  }
1138
1167
 
1139
1168
  /**
@@ -1146,29 +1175,19 @@ export function getHubInfo() {
1146
1175
  */
1147
1176
  export async function getOrCreateServer(opts = {}) {
1148
1177
  const { _deps, ...startOpts } = opts;
1149
- const checkHealth = _deps?.isHealthy ?? isServerHealthy;
1150
- const readInfo = _deps?.getInfo ?? getHubInfo;
1151
- const readCurrentState = _deps?.readState ?? readState;
1178
+ const existingHub = await tryReuseExistingHub({
1179
+ port: startOpts.port,
1180
+ host: startOpts.host,
1181
+ checkHealth: _deps?.isHealthy ?? isServerHealthy,
1182
+ readInfo: _deps?.getInfo ?? getHubInfo,
1183
+ readCurrentState: _deps?.readState ?? readState,
1184
+ });
1152
1185
  const boot = _deps?.startHub ?? startHub;
1153
1186
 
1154
- const existing = readCurrentState();
1155
- if (existing?.pid && existing?.port) {
1156
- const pidAlive = (() => {
1157
- try { process.kill(Number(existing.pid), 0); return true; } catch { return false; }
1158
- })();
1159
-
1160
- if (pidAlive && await checkHealth(existing.port)) {
1161
- return {
1162
- reused: true,
1163
- port: existing.port,
1164
- pid: existing.pid,
1165
- url: readInfo()?.url ?? `http://127.0.0.1:${existing.port}/mcp`,
1166
- };
1167
- }
1168
- }
1187
+ if (existingHub) return existingHub;
1169
1188
 
1170
1189
  const server = await boot(startOpts);
1171
- return { reused: false, ...server };
1190
+ return server.reused === true ? server : { reused: false, ...server };
1172
1191
  }
1173
1192
 
1174
1193
  /**
@@ -0,0 +1,121 @@
1
+ import { createMessage, MSG_TYPES } from "../../mesh/mesh-protocol.mjs";
2
+
3
+ const BRIDGE_AGENT_ID = "conductor";
4
+
5
+ /**
6
+ * Creates a bridge that converts Conductor EventEmitter events
7
+ * into Mesh protocol messages and keeps the registry in sync.
8
+ *
9
+ * @param {object} conductor - A conductor instance (with on/off)
10
+ * @param {object} registry - A mesh-registry instance
11
+ * @param {object} [opts]
12
+ * @param {string} [opts.bridgeAgentId="conductor"] - Agent ID for the bridge
13
+ * @param {function} [opts.onMessage] - Called with each generated mesh message
14
+ * @returns {object} Bridge API
15
+ */
16
+ export function createConductorMeshBridge(conductor, registry, opts = {}) {
17
+ const {
18
+ bridgeAgentId = BRIDGE_AGENT_ID,
19
+ onMessage,
20
+ } = opts;
21
+
22
+ const sessionAgentMap = new Map();
23
+ let attached = false;
24
+
25
+ function emit(message) {
26
+ if (typeof onMessage === "function") {
27
+ onMessage(message);
28
+ }
29
+ }
30
+
31
+ function agentIdForSession(sessionId) {
32
+ return `session:${sessionId}`;
33
+ }
34
+
35
+ /**
36
+ * Handle conductor stateChange events.
37
+ * - starting → register agent in registry
38
+ * - terminal states → unregister agent
39
+ * - all transitions → emit mesh EVENT message
40
+ */
41
+ function handleStateChange(event) {
42
+ const { sessionId, from, to, reason } = event;
43
+ const agentId = agentIdForSession(sessionId);
44
+
45
+ // Register on first starting transition
46
+ if (to === "starting" && !sessionAgentMap.has(sessionId)) {
47
+ registry.register(agentId, ["session"]);
48
+ sessionAgentMap.set(sessionId, agentId);
49
+ }
50
+
51
+ // Emit mesh event
52
+ const msg = createMessage(
53
+ MSG_TYPES.EVENT,
54
+ bridgeAgentId,
55
+ "*",
56
+ { event: "stateChange", sessionId, from, to, reason },
57
+ );
58
+ emit(msg);
59
+
60
+ // Unregister on terminal states
61
+ if (to === "dead" || to === "completed") {
62
+ registry.unregister(agentId);
63
+ sessionAgentMap.delete(sessionId);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Handle conductor completed events.
69
+ */
70
+ function handleCompleted(event) {
71
+ const msg = createMessage(
72
+ MSG_TYPES.EVENT,
73
+ bridgeAgentId,
74
+ "*",
75
+ { event: "completed", sessionId: event.sessionId },
76
+ );
77
+ emit(msg);
78
+ }
79
+
80
+ /**
81
+ * Handle conductor dead events.
82
+ */
83
+ function handleDead(event) {
84
+ const msg = createMessage(
85
+ MSG_TYPES.EVENT,
86
+ bridgeAgentId,
87
+ "*",
88
+ { event: "dead", sessionId: event.sessionId, reason: event.reason },
89
+ );
90
+ emit(msg);
91
+ }
92
+
93
+ /**
94
+ * Attaches event listeners to the conductor.
95
+ */
96
+ function attach() {
97
+ if (attached) return;
98
+ conductor.on("stateChange", handleStateChange);
99
+ conductor.on("completed", handleCompleted);
100
+ conductor.on("dead", handleDead);
101
+ attached = true;
102
+ }
103
+
104
+ /**
105
+ * Detaches event listeners and cleans up registry entries.
106
+ */
107
+ function detach() {
108
+ if (!attached) return;
109
+ conductor.off("stateChange", handleStateChange);
110
+ conductor.off("completed", handleCompleted);
111
+ conductor.off("dead", handleDead);
112
+
113
+ for (const [, agentId] of sessionAgentMap) {
114
+ registry.unregister(agentId);
115
+ }
116
+ sessionAgentMap.clear();
117
+ attached = false;
118
+ }
119
+
120
+ return { attach, detach, get isAttached() { return attached; } };
121
+ }