@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 +1 -1
- package/hub/server.mjs +71 -52
- package/hub/team/conductor-mesh-bridge.mjs +121 -0
- package/hub/team/conductor.mjs +671 -671
- package/hub/team/headless.mjs +1156 -1151
- package/hub/team/remote-session.mjs +299 -0
- package/hub/team/swarm-hypervisor.mjs +60 -1
- package/hub/team/swarm-planner.mjs +4 -0
- package/hub/team/worktree-lifecycle.mjs +65 -8
- package/hub/team/wt-manager.mjs +407 -0
- package/hub/team/wt-templates.json +43 -0
- package/hub/workers/codex-mcp.mjs +507 -510
- package/package.json +11 -3
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
|
-
|
|
318
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
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
|
+
}
|