@triflux/remote 10.30.1 → 10.31.0
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/server.mjs +11 -0
- package/hub/team/synapse-registry.mjs +83 -1
- package/package.json +1 -1
package/hub/server.mjs
CHANGED
|
@@ -2054,6 +2054,16 @@ export async function startHub({
|
|
|
2054
2054
|
}, 60000);
|
|
2055
2055
|
sessionTimer.unref();
|
|
2056
2056
|
|
|
2057
|
+
const synapsePruneTimer = setInterval(() => {
|
|
2058
|
+
try {
|
|
2059
|
+
const { count } = synapseRegistry.pruneExpired();
|
|
2060
|
+
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2063
|
+
}
|
|
2064
|
+
}, 60000);
|
|
2065
|
+
synapsePruneTimer.unref();
|
|
2066
|
+
|
|
2057
2067
|
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2058
2068
|
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2059
2069
|
const orphanCleanupTimer = setInterval(
|
|
@@ -2288,6 +2298,7 @@ export async function startHub({
|
|
|
2288
2298
|
router.stopSweeper();
|
|
2289
2299
|
clearInterval(hitlTimer);
|
|
2290
2300
|
clearInterval(sessionTimer);
|
|
2301
|
+
clearInterval(synapsePruneTimer);
|
|
2291
2302
|
clearInterval(rateLimitTimer);
|
|
2292
2303
|
clearInterval(orphanCleanupTimer);
|
|
2293
2304
|
if (idleTimer) {
|
|
@@ -5,6 +5,7 @@ const DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
|
5
5
|
const DEFAULT_LOCAL_TIMEOUT_MS = 30_000;
|
|
6
6
|
const DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
7
7
|
const DEFAULT_REMOTE_TIMEOUT_MS = 90_000;
|
|
8
|
+
const DEFAULT_EXPIRE_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
8
9
|
// Interactive (Claude/Codex) sessions idle for long stretches; a 30s local
|
|
9
10
|
// timeout produces stale false-positives. 5-minute TTL + an `idle` state
|
|
10
11
|
// distinguishes "alive but inactive" from "presumed dead".
|
|
@@ -133,6 +134,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
133
134
|
remoteTimeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,
|
|
134
135
|
interactiveHeartbeatIntervalMs = DEFAULT_INTERACTIVE_HEARTBEAT_INTERVAL_MS,
|
|
135
136
|
interactiveTimeoutMs = DEFAULT_INTERACTIVE_TIMEOUT_MS,
|
|
137
|
+
expireTimeoutMs = DEFAULT_EXPIRE_TIMEOUT_MS,
|
|
136
138
|
} = opts;
|
|
137
139
|
|
|
138
140
|
const sessions = new Map();
|
|
@@ -355,10 +357,89 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
355
357
|
return true;
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
// stale/expired 세션이 expireTimeoutMs 넘게 누적되면 Map에서 제거.
|
|
361
|
+
// live(active/idle)는 lastHeartbeat가 오래돼도 보존 — git-preflight dirty-file 가드가 의존.
|
|
362
|
+
function pruneExpired(opts2 = {}) {
|
|
363
|
+
if (destroyed) return { removed: [], count: 0 };
|
|
364
|
+
|
|
365
|
+
const cutoff =
|
|
366
|
+
typeof opts2.olderThanMs === "number"
|
|
367
|
+
? opts2.olderThanMs
|
|
368
|
+
: expireTimeoutMs;
|
|
369
|
+
const removed = [];
|
|
370
|
+
const currentTime = now();
|
|
371
|
+
|
|
372
|
+
for (const [sessionId, session] of sessions) {
|
|
373
|
+
if (
|
|
374
|
+
isLiveStatus(session.status) ||
|
|
375
|
+
currentTime - session.lastHeartbeat <= cutoff
|
|
376
|
+
) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
stopMonitor(sessionId);
|
|
381
|
+
sessions.delete(sessionId);
|
|
382
|
+
removed.push(sessionId);
|
|
383
|
+
notifyRemoved(session);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (removed.length > 0) persist();
|
|
387
|
+
return { removed, count: removed.length };
|
|
388
|
+
}
|
|
389
|
+
|
|
358
390
|
function heartbeat(sessionId, partialMeta = null) {
|
|
359
391
|
const normalized = normalizeSessionId(sessionId);
|
|
360
392
|
const session = sessions.get(normalized);
|
|
361
|
-
if (!session)
|
|
393
|
+
if (!session) {
|
|
394
|
+
// self-heal은 위치 정보(worktreePath 또는 cwd)가 있는 heartbeat에서만
|
|
395
|
+
// 발동한다. UserPromptSubmit heartbeat는 항상 worktreePath를 보내므로
|
|
396
|
+
// SessionStart register POST drop 복구 경로는 커버되고, taskSummary만 담은
|
|
397
|
+
// 빈약한 heartbeat로 위치 없는 가비지 세션이 register 되는 것은 막는다
|
|
398
|
+
// (그 경우는 기존대로 false → 400/heartbeat failed).
|
|
399
|
+
const hasLocator =
|
|
400
|
+
partialMeta &&
|
|
401
|
+
typeof partialMeta === "object" &&
|
|
402
|
+
!Array.isArray(partialMeta) &&
|
|
403
|
+
((typeof partialMeta.worktreePath === "string" &&
|
|
404
|
+
partialMeta.worktreePath.length > 0) ||
|
|
405
|
+
(typeof partialMeta.cwd === "string" && partialMeta.cwd.length > 0));
|
|
406
|
+
const canSelfHeal = Boolean(normalized) && hasLocator;
|
|
407
|
+
if (!canSelfHeal) return false;
|
|
408
|
+
|
|
409
|
+
// 미등록 세션 heartbeat = SessionStart register POST가 drop된 경우.
|
|
410
|
+
// self-heal로 register 복구(영구 미등록 방지).
|
|
411
|
+
//
|
|
412
|
+
// sessionKind는 명시값이 없으면 "interactive"로 본다. 실제 복구 경로인
|
|
413
|
+
// heartbeatInteractiveSession은 worktreePath/host/taskSummary만 보내고
|
|
414
|
+
// sessionKind는 생략하는데, sanitizeSession 기본값("headless")으로 떨어지면
|
|
415
|
+
// 복구된 Claude 세션이 30초 local timeout을 받아 idle/5분 TTL을 잃고 곧장
|
|
416
|
+
// stale로 전이돼 getActive()/peer discovery에서 사라진다(이 PRD가 고치려던
|
|
417
|
+
// 원래 증상의 재현). heartbeat 기반 liveness로 살아나는 세션은 interactive다.
|
|
418
|
+
const sanitizedKind =
|
|
419
|
+
typeof partialMeta.sessionKind === "string"
|
|
420
|
+
? partialMeta.sessionKind
|
|
421
|
+
: "interactive";
|
|
422
|
+
const healed = sanitizeSession(
|
|
423
|
+
{
|
|
424
|
+
...partialMeta,
|
|
425
|
+
sessionId: normalized,
|
|
426
|
+
sessionKind: sanitizedKind,
|
|
427
|
+
status: "active",
|
|
428
|
+
lastHeartbeat: now(),
|
|
429
|
+
},
|
|
430
|
+
normalized,
|
|
431
|
+
);
|
|
432
|
+
if (!healed) return false;
|
|
433
|
+
|
|
434
|
+
sessions.set(normalized, healed);
|
|
435
|
+
startMonitor(normalized);
|
|
436
|
+
persist();
|
|
437
|
+
emitter?.emit("synapse.session.started", {
|
|
438
|
+
sessionId: normalized,
|
|
439
|
+
session: cloneSession(healed),
|
|
440
|
+
});
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
362
443
|
|
|
363
444
|
const wasRemote = session.isRemote;
|
|
364
445
|
const updated = { ...session, lastHeartbeat: now(), status: "active" };
|
|
@@ -492,6 +573,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
492
573
|
return Object.freeze({
|
|
493
574
|
register,
|
|
494
575
|
unregister,
|
|
576
|
+
pruneExpired,
|
|
495
577
|
heartbeat,
|
|
496
578
|
getActive,
|
|
497
579
|
getAll,
|