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.
@@ -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
- 10,
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
- // TODO: emit synapse.session.stale via Hub deliveryEmitter
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(cloneSession(session));
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
- // TODO: emit synapse.session.removed via Hub deliveryEmitter
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(cloneSession(session));
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.status = "stale";
138
- notifyStale(current);
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 || sessions.has(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
- // TODO: emit synapse.session.started via Hub deliveryEmitter
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") session.host = partialMeta.host;
217
+ if (typeof partialMeta.host === "string") updated.host = partialMeta.host;
199
218
  if (typeof partialMeta.worktreePath === "string") {
200
- session.worktreePath = partialMeta.worktreePath;
219
+ updated.worktreePath = partialMeta.worktreePath;
201
220
  }
202
- if (typeof partialMeta.branch === "string") session.branch = partialMeta.branch;
221
+ if (typeof partialMeta.branch === "string") updated.branch = partialMeta.branch;
203
222
  if (Array.isArray(partialMeta.dirtyFiles)) {
204
- session.dirtyFiles = [...partialMeta.dirtyFiles];
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
- session.taskSummary = partialMeta.taskSummary;
228
+ updated.taskSummary = partialMeta.taskSummary;
208
229
  }
209
230
  if (typeof partialMeta.isRemote === "boolean") {
210
- session.isRemote = partialMeta.isRemote;
231
+ updated.isRemote = partialMeta.isRemote;
211
232
  }
212
233
  }
213
234
 
214
- if (session.isRemote !== wasRemote) {
235
+ sessions.set(normalized, updated);
236
+
237
+ if (updated.isRemote !== wasRemote) {
215
238
  startMonitor(normalized);
216
239
  }
217
240
 
218
- persist();
219
- // TODO: emit synapse.session.heartbeat via Hub deliveryEmitter
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.13",
3
+ "version": "10.9.14",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {