triflux 10.9.13 → 10.9.14
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/lib/spawn-trace.mjs +1 -1
- package/hub/server.mjs +21 -0
- package/hub/team/synapse-registry.mjs +49 -22
- package/package.json +1 -1
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const DEDUPE_WINDOW_MS = 5_000;
|
|
|
9
9
|
const RATE_WINDOW_MS = 1_000;
|
|
10
10
|
export const MAX_SPAWN_PER_SEC = resolvePositiveInteger(
|
|
11
11
|
process.env.TRIFLUX_MAX_SPAWN_RATE,
|
|
12
|
-
|
|
12
|
+
30,
|
|
13
13
|
);
|
|
14
14
|
export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
|
|
15
15
|
process.env.TRIFLUX_MAX_DESCENDANTS,
|
package/hub/server.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync as execSyncHub } from "node:child_process";
|
|
4
4
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
5
6
|
import {
|
|
6
7
|
existsSync,
|
|
7
8
|
mkdirSync,
|
|
@@ -40,7 +41,10 @@ import {
|
|
|
40
41
|
writeState,
|
|
41
42
|
} from "./state.mjs";
|
|
42
43
|
import { createStoreAdapter } from "./store-adapter.mjs";
|
|
44
|
+
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
43
45
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
46
|
+
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
47
|
+
import { createSynapseRegistry } from "./team/synapse-registry.mjs";
|
|
44
48
|
import { registerTeamBridge } from "./team-bridge.mjs";
|
|
45
49
|
import { createTools } from "./tools.mjs";
|
|
46
50
|
import { createDelegatorMcpWorker } from "./workers/delegator-mcp.mjs";
|
|
@@ -573,6 +577,22 @@ export async function startHub({
|
|
|
573
577
|
}
|
|
574
578
|
const delegatorService = new DelegatorService({ worker: delegatorWorker });
|
|
575
579
|
|
|
580
|
+
// Synapse Layer 4: session registry + git preflight + swarm locks
|
|
581
|
+
const synapseEmitter = new EventEmitter();
|
|
582
|
+
synapseEmitter.setMaxListeners(50);
|
|
583
|
+
const synapseRegistry = createSynapseRegistry({
|
|
584
|
+
persistPath: join(CACHE_DIR, "tfx-hub", "synapse-sessions.json"),
|
|
585
|
+
emitter: synapseEmitter,
|
|
586
|
+
});
|
|
587
|
+
const swarmLocks = createSwarmLocks({
|
|
588
|
+
repoRoot: PROJECT_ROOT,
|
|
589
|
+
persistPath: join(CACHE_DIR, "tfx-hub", "swarm-locks.json"),
|
|
590
|
+
});
|
|
591
|
+
const gitPreflight = createGitPreflight({
|
|
592
|
+
registry: synapseRegistry,
|
|
593
|
+
locks: swarmLocks,
|
|
594
|
+
});
|
|
595
|
+
|
|
576
596
|
const hitl = createHitlManager(store, router);
|
|
577
597
|
const pipe = createPipeServer({
|
|
578
598
|
router,
|
|
@@ -1521,6 +1541,7 @@ export async function startHub({
|
|
|
1521
1541
|
await pipe.stop();
|
|
1522
1542
|
await assignCallbacks.stop();
|
|
1523
1543
|
await delegatorWorker.stop().catch(() => {});
|
|
1544
|
+
try { synapseRegistry.destroy(); } catch {}
|
|
1524
1545
|
store.close();
|
|
1525
1546
|
try {
|
|
1526
1547
|
unlinkSync(PID_FILE);
|
|
@@ -42,6 +42,7 @@ function sanitizeSession(raw, fallbackSessionId = "") {
|
|
|
42
42
|
export function createSynapseRegistry(opts = {}) {
|
|
43
43
|
const {
|
|
44
44
|
persistPath,
|
|
45
|
+
emitter = null,
|
|
45
46
|
localHeartbeatIntervalMs = DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS,
|
|
46
47
|
localTimeoutMs = DEFAULT_LOCAL_TIMEOUT_MS,
|
|
47
48
|
remoteHeartbeatIntervalMs = DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS,
|
|
@@ -67,6 +68,8 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
67
68
|
return session.isRemote ? remoteTimeoutMs : localTimeoutMs;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
let persistTimer = null;
|
|
72
|
+
|
|
70
73
|
function persist() {
|
|
71
74
|
if (!persistPath) return;
|
|
72
75
|
try {
|
|
@@ -80,6 +83,15 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
function schedulePersist() {
|
|
87
|
+
if (persistTimer) return;
|
|
88
|
+
persistTimer = setTimeout(() => {
|
|
89
|
+
persistTimer = null;
|
|
90
|
+
persist();
|
|
91
|
+
}, 200);
|
|
92
|
+
if (typeof persistTimer.unref === "function") persistTimer.unref();
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
function restore() {
|
|
84
96
|
if (!persistPath || !existsSync(persistPath)) return;
|
|
85
97
|
try {
|
|
@@ -101,10 +113,11 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
function notifyStale(session) {
|
|
104
|
-
|
|
116
|
+
const clone = cloneSession(session);
|
|
117
|
+
emitter?.emit("synapse.session.stale", { sessionId: session.sessionId, session: clone });
|
|
105
118
|
for (const callback of staleCallbacks) {
|
|
106
119
|
try {
|
|
107
|
-
callback(
|
|
120
|
+
callback(clone);
|
|
108
121
|
} catch {
|
|
109
122
|
/* no-op */
|
|
110
123
|
}
|
|
@@ -112,10 +125,11 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
function notifyRemoved(session) {
|
|
115
|
-
|
|
128
|
+
const clone = cloneSession(session);
|
|
129
|
+
emitter?.emit("synapse.session.removed", { sessionId: session.sessionId, session: clone });
|
|
116
130
|
for (const callback of removedCallbacks) {
|
|
117
131
|
try {
|
|
118
|
-
callback(
|
|
132
|
+
callback(clone);
|
|
119
133
|
} catch {
|
|
120
134
|
/* no-op */
|
|
121
135
|
}
|
|
@@ -134,8 +148,10 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
134
148
|
|
|
135
149
|
const elapsedMs = now() - current.lastHeartbeat;
|
|
136
150
|
if (elapsedMs > timeoutFor(current) && current.status !== "stale") {
|
|
137
|
-
current
|
|
138
|
-
|
|
151
|
+
const staled = { ...current, status: "stale" };
|
|
152
|
+
sessions.set(sessionId, staled);
|
|
153
|
+
schedulePersist();
|
|
154
|
+
setImmediate(() => notifyStale(staled));
|
|
139
155
|
}
|
|
140
156
|
}, intervalFor(session));
|
|
141
157
|
|
|
@@ -150,8 +166,13 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
150
166
|
|
|
151
167
|
function register(meta) {
|
|
152
168
|
const sessionId = normalizeSessionId(meta?.sessionId);
|
|
153
|
-
if (!sessionId
|
|
154
|
-
return { ok: false, sessionId };
|
|
169
|
+
if (!sessionId) {
|
|
170
|
+
return { ok: false, sessionId, reason: "invalid_id" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sessions.has(sessionId)) {
|
|
174
|
+
console.warn("[synapse-registry] duplicate registration rejected:", sessionId);
|
|
175
|
+
return { ok: false, sessionId, reason: "duplicate" };
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
const session = sanitizeSession(
|
|
@@ -168,7 +189,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
168
189
|
startMonitor(sessionId);
|
|
169
190
|
persist();
|
|
170
191
|
|
|
171
|
-
|
|
192
|
+
emitter?.emit("synapse.session.started", { sessionId, session: cloneSession(session) });
|
|
172
193
|
return { ok: true, sessionId };
|
|
173
194
|
}
|
|
174
195
|
|
|
@@ -190,33 +211,35 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
190
211
|
if (!session) return false;
|
|
191
212
|
|
|
192
213
|
const wasRemote = session.isRemote;
|
|
193
|
-
|
|
194
|
-
session.lastHeartbeat = now();
|
|
195
|
-
session.status = "active";
|
|
214
|
+
const updated = { ...session, lastHeartbeat: now(), status: "active" };
|
|
196
215
|
|
|
197
216
|
if (partialMeta && typeof partialMeta === "object") {
|
|
198
|
-
if (typeof partialMeta.host === "string")
|
|
217
|
+
if (typeof partialMeta.host === "string") updated.host = partialMeta.host;
|
|
199
218
|
if (typeof partialMeta.worktreePath === "string") {
|
|
200
|
-
|
|
219
|
+
updated.worktreePath = partialMeta.worktreePath;
|
|
201
220
|
}
|
|
202
|
-
if (typeof partialMeta.branch === "string")
|
|
221
|
+
if (typeof partialMeta.branch === "string") updated.branch = partialMeta.branch;
|
|
203
222
|
if (Array.isArray(partialMeta.dirtyFiles)) {
|
|
204
|
-
|
|
223
|
+
updated.dirtyFiles = partialMeta.dirtyFiles.filter(
|
|
224
|
+
(f) => typeof f === "string" && f.length > 0,
|
|
225
|
+
);
|
|
205
226
|
}
|
|
206
227
|
if (typeof partialMeta.taskSummary === "string") {
|
|
207
|
-
|
|
228
|
+
updated.taskSummary = partialMeta.taskSummary;
|
|
208
229
|
}
|
|
209
230
|
if (typeof partialMeta.isRemote === "boolean") {
|
|
210
|
-
|
|
231
|
+
updated.isRemote = partialMeta.isRemote;
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
234
|
|
|
214
|
-
|
|
235
|
+
sessions.set(normalized, updated);
|
|
236
|
+
|
|
237
|
+
if (updated.isRemote !== wasRemote) {
|
|
215
238
|
startMonitor(normalized);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
schedulePersist();
|
|
242
|
+
emitter?.emit("synapse.session.heartbeat", { sessionId: normalized, session: cloneSession(updated), partial: partialMeta });
|
|
220
243
|
return true;
|
|
221
244
|
}
|
|
222
245
|
|
|
@@ -257,6 +280,10 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
257
280
|
for (const sessionId of monitors.keys()) {
|
|
258
281
|
stopMonitor(sessionId);
|
|
259
282
|
}
|
|
283
|
+
if (persistTimer) {
|
|
284
|
+
clearTimeout(persistTimer);
|
|
285
|
+
persistTimer = null;
|
|
286
|
+
}
|
|
260
287
|
persist();
|
|
261
288
|
}
|
|
262
289
|
|
|
@@ -272,4 +299,4 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
272
299
|
snapshot,
|
|
273
300
|
destroy,
|
|
274
301
|
});
|
|
275
|
-
}
|
|
302
|
+
}
|