@triflux/remote 10.31.0 → 10.33.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/cto/brief.mjs +93 -0
- package/cto/collect.mjs +846 -0
- package/cto/current.schema.json +181 -0
- package/cto/status.mjs +260 -0
- package/hub/hub-lifecycle.mjs +242 -0
- package/hub/mac-focus.mjs +94 -0
- package/hub/mac-tray.swift +133 -0
- package/hub/promote-penalties.mjs +138 -0
- package/hub/public/tray.html +594 -0
- package/hub/server.mjs +233 -44
- package/hub/team/claude-daemon-control.mjs +46 -46
- package/hub/team/conductor.mjs +18 -0
- package/hub/team/cto-auto-collect.mjs +109 -0
- package/hub/team/cto-pull.mjs +27 -0
- package/hub/team/uds-orchestrator.mjs +607 -0
- package/hub/tray-lifecycle.mjs +131 -0
- package/hub/tray-runtime.mjs +265 -0
- package/hub/tray-state.mjs +448 -0
- package/hub/tray.mjs +93 -4
- package/hub/workers/antigravity-route-worker.mjs +14 -4
- package/hub/workers/delegator-mcp.mjs +2 -5
- package/hub/workers/factory.mjs +1 -1
- package/hub/workers/gemini-worker.mjs +2 -1
- package/package.json +7 -2
- package/scripts/lib/env-probe.mjs +36 -9
package/hub/server.mjs
CHANGED
|
@@ -15,35 +15,45 @@ import { homedir } from "node:os";
|
|
|
15
15
|
import { extname, join, resolve, sep } from "node:path";
|
|
16
16
|
import { performance } from "node:perf_hooks";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
18
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
20
19
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
20
|
import {
|
|
22
21
|
CallToolRequestSchema,
|
|
23
22
|
ListToolsRequestSchema,
|
|
24
23
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
24
|
+
import { runCollect } from "../cto/collect.mjs";
|
|
25
|
+
import { runStatus as runCtoStatus } from "../cto/status.mjs";
|
|
25
26
|
import { createModuleLogger } from "../scripts/lib/logger.mjs";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
inspectRegistry,
|
|
29
|
+
inspectRegistryStatus,
|
|
30
|
+
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
31
|
+
import { broker as brokerInstance, reloadBroker } from "./account-broker.mjs";
|
|
32
|
+
import { createAdaptiveEngine } from "./adaptive.mjs";
|
|
33
|
+
import { createAssignCallbackServer } from "./assign-callbacks.mjs";
|
|
34
|
+
import { DelegatorService } from "./delegator/index.mjs";
|
|
35
|
+
import { createHitlManager } from "./hitl.mjs";
|
|
31
36
|
import {
|
|
32
37
|
cleanupOrphanNodeProcesses,
|
|
33
38
|
cleanupOrphanRuntimeProcesses,
|
|
34
39
|
cleanupStaleFsmonitorDaemons,
|
|
35
|
-
} from "
|
|
36
|
-
import
|
|
40
|
+
} from "./lib/process-utils.mjs";
|
|
41
|
+
import {
|
|
42
|
+
reapExistingHubProcesses,
|
|
43
|
+
resolveHubPortForContext,
|
|
44
|
+
} from "./hub-lifecycle.mjs";
|
|
45
|
+
import * as spawnTrace from "./lib/spawn-trace.mjs";
|
|
37
46
|
import {
|
|
38
47
|
recordRequest,
|
|
39
48
|
recordWorker,
|
|
40
49
|
snapshot as traceSnapshot,
|
|
41
|
-
} from "
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
50
|
+
} from "./lib/trace-recorder.mjs";
|
|
51
|
+
import { focusSessionOnMac } from "./mac-focus.mjs";
|
|
52
|
+
import { logQuotaRefreshFailures } from "./middleware/quota-middleware.mjs";
|
|
53
|
+
import { wrapRequestHandler } from "./middleware/request-logger.mjs";
|
|
44
54
|
import { createPipeServer } from "./pipe.mjs";
|
|
45
|
-
import { createRouter } from "
|
|
46
|
-
import { createAdaptiveFingerprintService } from "
|
|
55
|
+
import { createRouter } from "./router.mjs";
|
|
56
|
+
import { createAdaptiveFingerprintService } from "./session-fingerprint.mjs";
|
|
47
57
|
import {
|
|
48
58
|
acquireLock,
|
|
49
59
|
getVersionHash,
|
|
@@ -51,8 +61,9 @@ import {
|
|
|
51
61
|
readState,
|
|
52
62
|
releaseLock,
|
|
53
63
|
writeState,
|
|
54
|
-
} from "
|
|
64
|
+
} from "./state.mjs";
|
|
55
65
|
import { createStoreAdapter } from "./store-adapter.mjs";
|
|
66
|
+
import { createCtoAutoCollector } from "./team/cto-auto-collect.mjs";
|
|
56
67
|
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
57
68
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
58
69
|
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
@@ -60,8 +71,11 @@ import {
|
|
|
60
71
|
createSynapseRegistry,
|
|
61
72
|
projectPeer,
|
|
62
73
|
} from "./team/synapse-registry.mjs";
|
|
63
|
-
import { registerTeamBridge } from "
|
|
74
|
+
import { registerTeamBridge } from "./team-bridge.mjs";
|
|
64
75
|
import { createTools } from "./tools.mjs";
|
|
76
|
+
import { spawnTrayForHub } from "./tray-lifecycle.mjs";
|
|
77
|
+
import { getRuntimeStatus } from "./tray-runtime.mjs";
|
|
78
|
+
import { buildTrayStatePayload } from "./tray-state.mjs";
|
|
65
79
|
import { createDelegatorMcpWorker } from "./workers/delegator-mcp.mjs";
|
|
66
80
|
|
|
67
81
|
registerTeamBridge(nativeProxy);
|
|
@@ -104,7 +118,7 @@ const SYNAPSE_VALID_OPS = new Set([
|
|
|
104
118
|
"stash-pop",
|
|
105
119
|
"worktree-remove",
|
|
106
120
|
]);
|
|
107
|
-
const HUB_IDLE_TIMEOUT_DEFAULT_MS =
|
|
121
|
+
const HUB_IDLE_TIMEOUT_DEFAULT_MS = 30 * 60 * 1000;
|
|
108
122
|
const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
|
|
109
123
|
const STATIC_CONTENT_TYPES = Object.freeze({
|
|
110
124
|
".html": "text/html",
|
|
@@ -256,10 +270,11 @@ function readHubPidFile(
|
|
|
256
270
|
}
|
|
257
271
|
|
|
258
272
|
export function resolveHubPort(env = process.env, opts = {}) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
return resolveHubPortForContext({
|
|
274
|
+
env,
|
|
275
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
276
|
+
defaultPort: HUB_DEFAULT_PORT,
|
|
277
|
+
});
|
|
263
278
|
}
|
|
264
279
|
|
|
265
280
|
export function detectLivePeer(
|
|
@@ -482,6 +497,7 @@ function isPublicPath(path) {
|
|
|
482
497
|
return (
|
|
483
498
|
PUBLIC_PATHS.has(path) ||
|
|
484
499
|
path === "/dashboard" ||
|
|
500
|
+
path === "/tray.html" ||
|
|
485
501
|
path === "/api/qos-stats" ||
|
|
486
502
|
path.startsWith("/public/")
|
|
487
503
|
);
|
|
@@ -638,6 +654,68 @@ function parsePositiveInt(value, fallback) {
|
|
|
638
654
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
639
655
|
}
|
|
640
656
|
|
|
657
|
+
export function resolveHubIdleTimeoutMs({
|
|
658
|
+
port,
|
|
659
|
+
env = process.env,
|
|
660
|
+
defaultPort = HUB_DEFAULT_PORT,
|
|
661
|
+
} = {}) {
|
|
662
|
+
const parsed = Number.parseInt(String(env?.TFX_HUB_IDLE_TIMEOUT_MS ?? ""), 10);
|
|
663
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
664
|
+
return Number(port) === defaultPort ? 0 : HUB_IDLE_TIMEOUT_DEFAULT_MS;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function cleanupOwnedPidFile({
|
|
668
|
+
pidFilePath = PID_FILE,
|
|
669
|
+
currentPid = process.pid,
|
|
670
|
+
readFile = readFileSync,
|
|
671
|
+
unlink = unlinkSync,
|
|
672
|
+
} = {}) {
|
|
673
|
+
try {
|
|
674
|
+
const info = JSON.parse(readFile(pidFilePath, "utf8"));
|
|
675
|
+
if (Number(info?.pid) !== Number(currentPid)) return false;
|
|
676
|
+
unlink(pidFilePath);
|
|
677
|
+
return true;
|
|
678
|
+
} catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function cleanupOwnedTokenFile({
|
|
684
|
+
tokenFilePath = TOKEN_FILE,
|
|
685
|
+
token,
|
|
686
|
+
port = HUB_DEFAULT_PORT,
|
|
687
|
+
readFile = readFileSync,
|
|
688
|
+
unlink = unlinkSync,
|
|
689
|
+
defaultPort = HUB_DEFAULT_PORT,
|
|
690
|
+
} = {}) {
|
|
691
|
+
if (Number(port) !== Number(defaultPort)) return false;
|
|
692
|
+
const normalizedToken = String(token ?? "").trim();
|
|
693
|
+
if (!normalizedToken) return false;
|
|
694
|
+
try {
|
|
695
|
+
if (String(readFile(tokenFilePath, "utf8")).trim() !== normalizedToken) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
unlink(tokenFilePath);
|
|
699
|
+
return true;
|
|
700
|
+
} catch {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function writeOwnedTokenFile({
|
|
706
|
+
tokenFilePath = TOKEN_FILE,
|
|
707
|
+
tokenDir = join(homedir(), ".claude"),
|
|
708
|
+
token,
|
|
709
|
+
mkdir = mkdirSync,
|
|
710
|
+
writeFile = writeFileSync,
|
|
711
|
+
} = {}) {
|
|
712
|
+
const normalizedToken = String(token ?? "").trim();
|
|
713
|
+
if (!normalizedToken) return false;
|
|
714
|
+
mkdir(tokenDir, { recursive: true });
|
|
715
|
+
writeFile(tokenFilePath, normalizedToken, { mode: 0o600 });
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
|
|
641
719
|
function readRecentAimdEvents(now = Date.now()) {
|
|
642
720
|
try {
|
|
643
721
|
if (!existsSync(BATCH_EVENTS_PATH)) return [];
|
|
@@ -779,10 +857,41 @@ function getBrokerPublicSnapshot(currentBroker = brokerInstance) {
|
|
|
779
857
|
return currentBroker.publicSnapshot();
|
|
780
858
|
}
|
|
781
859
|
|
|
860
|
+
async function getTrayCtoStatus() {
|
|
861
|
+
try {
|
|
862
|
+
return await runCtoStatus(["--json"], {
|
|
863
|
+
rootDir: PROJECT_ROOT,
|
|
864
|
+
stdout: { write() {} },
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
return { error: error?.message || String(error) };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function getTrayMcpStatus() {
|
|
872
|
+
const registryState = inspectRegistry();
|
|
873
|
+
if (!registryState.exists || !registryState.valid) {
|
|
874
|
+
return {
|
|
875
|
+
registry_path: registryState.path,
|
|
876
|
+
server_count: 0,
|
|
877
|
+
rows: [],
|
|
878
|
+
error: registryState.errors?.join("; ") || "MCP registry unavailable",
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const status = inspectRegistryStatus(registryState.registry);
|
|
882
|
+
return {
|
|
883
|
+
registry_path: registryState.path,
|
|
884
|
+
server_count: Object.keys(registryState.registry?.servers || {}).length,
|
|
885
|
+
rows: status.rows,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
782
889
|
function resolvePublicFilePath(path) {
|
|
783
890
|
let relativePath = null;
|
|
784
891
|
if (path === "/dashboard") {
|
|
785
892
|
relativePath = "dashboard.html";
|
|
893
|
+
} else if (path === "/tray.html") {
|
|
894
|
+
relativePath = "tray.html";
|
|
786
895
|
} else if (path.startsWith("/public/")) {
|
|
787
896
|
relativePath = path.slice("/public/".length);
|
|
788
897
|
}
|
|
@@ -878,10 +987,7 @@ export async function startHub({
|
|
|
878
987
|
attachBrokerDiagnostics(brokerInstance);
|
|
879
988
|
syncBrokerAuthCache(brokerInstance);
|
|
880
989
|
|
|
881
|
-
const hubIdleTimeoutMs =
|
|
882
|
-
process.env.TFX_HUB_IDLE_TIMEOUT_MS,
|
|
883
|
-
HUB_IDLE_TIMEOUT_DEFAULT_MS,
|
|
884
|
-
);
|
|
990
|
+
const hubIdleTimeoutMs = resolveHubIdleTimeoutMs({ port, env: process.env });
|
|
885
991
|
const hubIdleSweepMs = parsePositiveInt(
|
|
886
992
|
process.env.TFX_HUB_IDLE_SWEEP_MS,
|
|
887
993
|
Math.min(HUB_IDLE_SWEEP_DEFAULT_MS, hubIdleTimeoutMs),
|
|
@@ -926,12 +1032,9 @@ export async function startHub({
|
|
|
926
1032
|
|
|
927
1033
|
const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
|
|
928
1034
|
if (HUB_TOKEN) {
|
|
929
|
-
|
|
930
|
-
writeFileSync(TOKEN_FILE, HUB_TOKEN, { mode: 0o600 });
|
|
1035
|
+
writeOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
931
1036
|
} else {
|
|
932
|
-
|
|
933
|
-
unlinkSync(TOKEN_FILE);
|
|
934
|
-
} catch {}
|
|
1037
|
+
cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
935
1038
|
}
|
|
936
1039
|
|
|
937
1040
|
const store = await createStoreAdapter(dbPath);
|
|
@@ -970,7 +1073,7 @@ export async function startHub({
|
|
|
970
1073
|
|
|
971
1074
|
// adaptive rule confidence decay (7일 이상 미관측 규칙 -0.1 감소)
|
|
972
1075
|
try {
|
|
973
|
-
const { decayRules } = await import("
|
|
1076
|
+
const { decayRules } = await import("./reflexion.mjs");
|
|
974
1077
|
const decay = decayRules(
|
|
975
1078
|
store,
|
|
976
1079
|
adaptiveEngine.sessionCount?.() || 1,
|
|
@@ -1009,10 +1112,16 @@ export async function startHub({
|
|
|
1009
1112
|
registry: synapseRegistry,
|
|
1010
1113
|
locks: swarmLocks,
|
|
1011
1114
|
});
|
|
1115
|
+
const ctoAutoCollector = createCtoAutoCollector({
|
|
1116
|
+
registry: synapseRegistry,
|
|
1117
|
+
runCollect,
|
|
1118
|
+
logger: hubLog,
|
|
1119
|
+
});
|
|
1012
1120
|
|
|
1013
1121
|
// Synapse Layer 5: emitter subscribers — bridge events to hub logging
|
|
1014
|
-
synapseEmitter.on("synapse.session.started", ({ sessionId }) => {
|
|
1122
|
+
synapseEmitter.on("synapse.session.started", ({ sessionId, session }) => {
|
|
1015
1123
|
hubLog.info({ sessionId }, "synapse.session.started");
|
|
1124
|
+
ctoAutoCollector.handleSessionStarted({ sessionId, session });
|
|
1016
1125
|
});
|
|
1017
1126
|
synapseEmitter.on("synapse.session.heartbeat", ({ sessionId }) => {
|
|
1018
1127
|
hubLog.debug({ sessionId }, "synapse.session.heartbeat");
|
|
@@ -1323,6 +1432,66 @@ export async function startHub({
|
|
|
1323
1432
|
});
|
|
1324
1433
|
}
|
|
1325
1434
|
|
|
1435
|
+
if (path === "/api/tray-state" && req.method === "GET") {
|
|
1436
|
+
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
1437
|
+
return writeJson(res, 403, { ok: false, error: "Loopback only" });
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const qos = getQosStatsPayload();
|
|
1441
|
+
const synapseSnapshot = synapseRegistry.snapshot();
|
|
1442
|
+
const broker = getBrokerPublicSnapshot(brokerInstance);
|
|
1443
|
+
const runtimeStatus = getRuntimeStatus({
|
|
1444
|
+
projectRoots: [PROJECT_ROOT],
|
|
1445
|
+
});
|
|
1446
|
+
const [ctoStatus, mcpStatus] = await Promise.all([
|
|
1447
|
+
getTrayCtoStatus(),
|
|
1448
|
+
Promise.resolve(getTrayMcpStatus()),
|
|
1449
|
+
]);
|
|
1450
|
+
|
|
1451
|
+
return writeJson(
|
|
1452
|
+
res,
|
|
1453
|
+
200,
|
|
1454
|
+
buildTrayStatePayload({
|
|
1455
|
+
hub: {
|
|
1456
|
+
id: String(sessionId),
|
|
1457
|
+
pid: process.pid,
|
|
1458
|
+
port,
|
|
1459
|
+
url: buildHubUrl(host, port),
|
|
1460
|
+
projectRoot: PROJECT_ROOT,
|
|
1461
|
+
},
|
|
1462
|
+
qos,
|
|
1463
|
+
synapseSnapshot,
|
|
1464
|
+
broker,
|
|
1465
|
+
ctoStatus,
|
|
1466
|
+
mcpStatus,
|
|
1467
|
+
runtimeStatus,
|
|
1468
|
+
}),
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (path === "/api/focus-session" && req.method === "POST") {
|
|
1473
|
+
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
1474
|
+
return writeJson(res, 403, { ok: false, error: "Loopback only" });
|
|
1475
|
+
}
|
|
1476
|
+
let body;
|
|
1477
|
+
try {
|
|
1478
|
+
body = await parseBody(req);
|
|
1479
|
+
} catch (_e) {
|
|
1480
|
+
return writeJson(res, 400, {
|
|
1481
|
+
error: "Invalid JSON or Body too large",
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const { sessionId, udsId } = body || {};
|
|
1486
|
+
const result = await focusSessionOnMac({
|
|
1487
|
+
sessionId,
|
|
1488
|
+
udsId,
|
|
1489
|
+
pid: body?.pid,
|
|
1490
|
+
agentId: body?.agentId ?? body?.agent_id,
|
|
1491
|
+
});
|
|
1492
|
+
return writeJson(res, 200, { ok: result.ok !== false, focus: result });
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1326
1495
|
// Redacted peer-discovery surface. Returns co-located live peers (same
|
|
1327
1496
|
// cwd / worktree) with raw cwd/pid stripped — only label/hash + booleans.
|
|
1328
1497
|
if (path === "/synapse/peers" && req.method === "GET") {
|
|
@@ -2145,9 +2314,7 @@ export async function startHub({
|
|
|
2145
2314
|
store.close();
|
|
2146
2315
|
} catch {}
|
|
2147
2316
|
if (!preserveTokenFile) {
|
|
2148
|
-
|
|
2149
|
-
unlinkSync(TOKEN_FILE);
|
|
2150
|
-
} catch {}
|
|
2317
|
+
cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
2151
2318
|
}
|
|
2152
2319
|
releaseStartupLock();
|
|
2153
2320
|
};
|
|
@@ -2271,6 +2438,22 @@ export async function startHub({
|
|
|
2271
2438
|
},
|
|
2272
2439
|
"hub.started",
|
|
2273
2440
|
);
|
|
2441
|
+
if (port === HUB_DEFAULT_PORT) {
|
|
2442
|
+
void reapExistingHubProcesses({ currentPid: process.pid }).then(
|
|
2443
|
+
({ reaped }) => {
|
|
2444
|
+
if (reaped.length > 0) {
|
|
2445
|
+
hubLog.info(
|
|
2446
|
+
{
|
|
2447
|
+
killed: reaped.length,
|
|
2448
|
+
processes: reaped,
|
|
2449
|
+
caller: "startup",
|
|
2450
|
+
},
|
|
2451
|
+
"hub.startup_reaper",
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
},
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2274
2457
|
hubLog.debug(
|
|
2275
2458
|
{
|
|
2276
2459
|
publicDir: PUBLIC_DIR,
|
|
@@ -2315,12 +2498,8 @@ export async function startHub({
|
|
|
2315
2498
|
synapseRegistry.destroy();
|
|
2316
2499
|
} catch {}
|
|
2317
2500
|
store.close();
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
} catch {}
|
|
2321
|
-
try {
|
|
2322
|
-
unlinkSync(TOKEN_FILE);
|
|
2323
|
-
} catch {}
|
|
2501
|
+
cleanupOwnedPidFile();
|
|
2502
|
+
cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
2324
2503
|
httpServer.closeAllConnections();
|
|
2325
2504
|
await new Promise((resolveClose) =>
|
|
2326
2505
|
httpServer.close(resolveClose),
|
|
@@ -2730,13 +2909,14 @@ refresh();setInterval(refresh,10000);
|
|
|
2730
2909
|
|
|
2731
2910
|
const selfRun = process.argv[1]?.replace(/\\/g, "/").endsWith("hub/server.mjs");
|
|
2732
2911
|
if (selfRun) {
|
|
2733
|
-
const port =
|
|
2912
|
+
const port = resolveHubPortForContext({
|
|
2913
|
+
env: process.env,
|
|
2914
|
+
cwd: process.cwd(),
|
|
2915
|
+
});
|
|
2734
2916
|
const dbPath = process.env.TFX_HUB_DB || undefined;
|
|
2735
2917
|
|
|
2736
2918
|
const cleanupPidFile = () => {
|
|
2737
|
-
|
|
2738
|
-
unlinkSync(PID_FILE);
|
|
2739
|
-
} catch {}
|
|
2919
|
+
cleanupOwnedPidFile();
|
|
2740
2920
|
};
|
|
2741
2921
|
|
|
2742
2922
|
process.on("unhandledRejection", (err) => {
|
|
@@ -2760,6 +2940,15 @@ if (selfRun) {
|
|
|
2760
2940
|
process.exit(0);
|
|
2761
2941
|
return;
|
|
2762
2942
|
}
|
|
2943
|
+
try {
|
|
2944
|
+
const trayPath = fileURLToPath(new URL("./tray.mjs", import.meta.url));
|
|
2945
|
+
const tray = spawnTrayForHub({ trayPath });
|
|
2946
|
+
if (tray.status === "started") {
|
|
2947
|
+
hubLog.info({ pid: tray.pid }, "tray.auto_started");
|
|
2948
|
+
}
|
|
2949
|
+
} catch (error) {
|
|
2950
|
+
hubLog.warn({ err: error }, "tray.auto_start_failed");
|
|
2951
|
+
}
|
|
2763
2952
|
const shutdown = async (signal) => {
|
|
2764
2953
|
hubLog.info({ signal }, "hub.stopping");
|
|
2765
2954
|
try {
|
|
@@ -10,52 +10,6 @@ import {
|
|
|
10
10
|
writeClaudeSessionProjection,
|
|
11
11
|
} from "./claude-session-projection.mjs";
|
|
12
12
|
|
|
13
|
-
export function buildClaudePromptDispatchPayload({
|
|
14
|
-
short = crypto.randomBytes(4).toString("hex"),
|
|
15
|
-
sessionId,
|
|
16
|
-
cwd = process.cwd(),
|
|
17
|
-
prompt,
|
|
18
|
-
name = "tfx uds claude prompt",
|
|
19
|
-
createdAt = Date.now(),
|
|
20
|
-
cols = 120,
|
|
21
|
-
rows = 40,
|
|
22
|
-
} = {}) {
|
|
23
|
-
if (!prompt) throw new Error("prompt is required");
|
|
24
|
-
const uuid = crypto.randomUUID();
|
|
25
|
-
const resolvedSessionId = sessionId || `${short}${uuid.slice(8)}`;
|
|
26
|
-
return {
|
|
27
|
-
proto: 1,
|
|
28
|
-
short,
|
|
29
|
-
sessionId: resolvedSessionId,
|
|
30
|
-
createdAt,
|
|
31
|
-
source: "shell",
|
|
32
|
-
cwd,
|
|
33
|
-
agent: "claude",
|
|
34
|
-
launch: {
|
|
35
|
-
mode: "prompt",
|
|
36
|
-
args: [
|
|
37
|
-
"--session-id",
|
|
38
|
-
resolvedSessionId,
|
|
39
|
-
"--agent",
|
|
40
|
-
"claude",
|
|
41
|
-
"--permission-mode",
|
|
42
|
-
"auto",
|
|
43
|
-
"--",
|
|
44
|
-
prompt,
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
env: {},
|
|
48
|
-
isolation: "none",
|
|
49
|
-
respawnFlags: [],
|
|
50
|
-
seed: {
|
|
51
|
-
intent: "[redacted uds orchestration prompt]",
|
|
52
|
-
name,
|
|
53
|
-
},
|
|
54
|
-
cols,
|
|
55
|
-
rows,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
13
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
60
14
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
61
15
|
return path.join(os.homedir(), ".claude");
|
|
@@ -1054,6 +1008,52 @@ export function buildDaemonExecDispatchPayload({
|
|
|
1054
1008
|
};
|
|
1055
1009
|
}
|
|
1056
1010
|
|
|
1011
|
+
export function buildClaudePromptDispatchPayload({
|
|
1012
|
+
short = crypto.randomBytes(4).toString("hex"),
|
|
1013
|
+
sessionId,
|
|
1014
|
+
cwd = process.cwd(),
|
|
1015
|
+
prompt,
|
|
1016
|
+
name = "tfx uds claude prompt",
|
|
1017
|
+
createdAt = Date.now(),
|
|
1018
|
+
cols = 120,
|
|
1019
|
+
rows = 40,
|
|
1020
|
+
} = {}) {
|
|
1021
|
+
if (!prompt) throw new Error("prompt is required");
|
|
1022
|
+
const uuid = crypto.randomUUID();
|
|
1023
|
+
const resolvedSessionId = sessionId || `${short}${uuid.slice(8)}`;
|
|
1024
|
+
return {
|
|
1025
|
+
proto: 1,
|
|
1026
|
+
short,
|
|
1027
|
+
sessionId: resolvedSessionId,
|
|
1028
|
+
createdAt,
|
|
1029
|
+
source: "shell",
|
|
1030
|
+
cwd,
|
|
1031
|
+
agent: "claude",
|
|
1032
|
+
launch: {
|
|
1033
|
+
mode: "prompt",
|
|
1034
|
+
args: [
|
|
1035
|
+
"--session-id",
|
|
1036
|
+
resolvedSessionId,
|
|
1037
|
+
"--agent",
|
|
1038
|
+
"claude",
|
|
1039
|
+
"--permission-mode",
|
|
1040
|
+
"auto",
|
|
1041
|
+
"--",
|
|
1042
|
+
prompt,
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
env: {},
|
|
1046
|
+
isolation: "none",
|
|
1047
|
+
respawnFlags: [],
|
|
1048
|
+
seed: {
|
|
1049
|
+
intent: "[redacted uds orchestration prompt]",
|
|
1050
|
+
name,
|
|
1051
|
+
},
|
|
1052
|
+
cols,
|
|
1053
|
+
rows,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
1057
|
export function findDaemonJobByShort(listResponse, short) {
|
|
1058
1058
|
if (!Array.isArray(listResponse?.jobs)) return null;
|
|
1059
1059
|
return listResponse.jobs.find((job) => job?.short === short) || null;
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
ensureConductorRegistry,
|
|
40
40
|
getConductorRegistry,
|
|
41
41
|
} from "./conductor-registry.mjs";
|
|
42
|
+
import { readCtoSnapshot } from "./cto-pull.mjs";
|
|
42
43
|
import { createEventLog } from "./event-log.mjs";
|
|
43
44
|
import { buildSpawnSpecForMode, MODES } from "./execution-mode.mjs";
|
|
44
45
|
import { extractCompletionPayload } from "./extract-completion-payload.mjs";
|
|
@@ -219,6 +220,22 @@ export function createConductor(opts = {}) {
|
|
|
219
220
|
// 공유 event log (모든 세션 이벤트를 하나의 JSONL에)
|
|
220
221
|
const eventLog = createEventLog(join(logsDir, "conductor-events.jsonl"));
|
|
221
222
|
|
|
223
|
+
function logCtoContext(sessionId) {
|
|
224
|
+
const snapshot = readCtoSnapshot({ lakeRoot: opts.ctoLakeRoot });
|
|
225
|
+
if (!snapshot) return;
|
|
226
|
+
eventLog.append("cto_context", {
|
|
227
|
+
session: sessionId,
|
|
228
|
+
snapshot: {
|
|
229
|
+
schema_version: snapshot.schema_version,
|
|
230
|
+
repo: snapshot.repo,
|
|
231
|
+
sources: snapshot.sources,
|
|
232
|
+
ledger_tail: snapshot.ledger_tail,
|
|
233
|
+
live_sessions: snapshot.live_sessions,
|
|
234
|
+
active_shards: snapshot.active_shards,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
222
239
|
/**
|
|
223
240
|
* 세션 상태 전이.
|
|
224
241
|
* @param {object} session
|
|
@@ -1172,6 +1189,7 @@ export function createConductor(opts = {}) {
|
|
|
1172
1189
|
|
|
1173
1190
|
sessions.set(resolvedConfig.id, session);
|
|
1174
1191
|
getConductorRegistry()?.register?.(resolvedConfig.id, publicApi);
|
|
1192
|
+
logCtoContext(resolvedConfig.id);
|
|
1175
1193
|
|
|
1176
1194
|
if (resolvedConfig.remote) {
|
|
1177
1195
|
startRemoteSession(session);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DEBOUNCE_MS = 120_000;
|
|
5
|
+
const OFF_VALUES = new Set(["0", "false", "off", "no"]);
|
|
6
|
+
|
|
7
|
+
export function isCtoAutoCollectDisabled(env = process.env) {
|
|
8
|
+
return OFF_VALUES.has(
|
|
9
|
+
String(env?.TFX_CTO_AUTO_COLLECT ?? "")
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase(),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function freshCurrentMd(projectRoot, now, debounceMs) {
|
|
16
|
+
try {
|
|
17
|
+
const currentMd = join(projectRoot, ".triflux", "lake", "current.md");
|
|
18
|
+
if (!existsSync(currentMd)) return false;
|
|
19
|
+
return now - statSync(currentMd).mtimeMs < debounceMs;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createCtoAutoCollector(opts = {}) {
|
|
26
|
+
const registry = opts.registry;
|
|
27
|
+
const runCollect = opts.runCollect;
|
|
28
|
+
const logger = opts.logger || null;
|
|
29
|
+
const debounceMs = Number(opts.debounceMs || DEFAULT_DEBOUNCE_MS);
|
|
30
|
+
const now = opts.now || (() => Date.now());
|
|
31
|
+
const env = opts.env || process.env;
|
|
32
|
+
const lastCollectMs = opts.lastCollectMs || new Map();
|
|
33
|
+
const inFlight = new Set();
|
|
34
|
+
|
|
35
|
+
function handleSessionStarted({ sessionId, session } = {}) {
|
|
36
|
+
if (isCtoAutoCollectDisabled(env))
|
|
37
|
+
return { triggered: false, reason: "disabled" };
|
|
38
|
+
if (!registry || typeof registry.querySessions !== "function") {
|
|
39
|
+
return { triggered: false, reason: "missing-registry" };
|
|
40
|
+
}
|
|
41
|
+
if (typeof runCollect !== "function") {
|
|
42
|
+
return { triggered: false, reason: "missing-runCollect" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cwd = typeof session?.cwd === "string" ? session.cwd : "";
|
|
46
|
+
const worktree =
|
|
47
|
+
typeof session?.worktreePath === "string" ? session.worktreePath : "";
|
|
48
|
+
const projectRoot = worktree || cwd;
|
|
49
|
+
if (!projectRoot)
|
|
50
|
+
return { triggered: false, reason: "missing-project-root" };
|
|
51
|
+
|
|
52
|
+
let peers = [];
|
|
53
|
+
try {
|
|
54
|
+
peers = registry.querySessions({
|
|
55
|
+
cwd,
|
|
56
|
+
worktree,
|
|
57
|
+
excludeSessionId: sessionId,
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger?.warn?.(
|
|
61
|
+
{ sessionId, err: String(error?.message || error) },
|
|
62
|
+
"cto.auto_collect.query_failed",
|
|
63
|
+
);
|
|
64
|
+
return { triggered: false, reason: "query-failed" };
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(peers) || peers.length < 1) {
|
|
67
|
+
return { triggered: false, reason: "no-peers" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const currentTime = now();
|
|
71
|
+
const last = lastCollectMs.get(projectRoot) || 0;
|
|
72
|
+
if (currentTime - last < debounceMs) {
|
|
73
|
+
return { triggered: false, reason: "debounced" };
|
|
74
|
+
}
|
|
75
|
+
if (freshCurrentMd(projectRoot, currentTime, debounceMs)) {
|
|
76
|
+
lastCollectMs.set(projectRoot, currentTime);
|
|
77
|
+
return { triggered: false, reason: "fresh-lake" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lastCollectMs.set(projectRoot, currentTime);
|
|
81
|
+
const promise = Promise.resolve()
|
|
82
|
+
.then(() => runCollect([], { rootDir: projectRoot }))
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
logger?.warn?.(
|
|
85
|
+
{ projectRoot, err: String(error?.message || error) },
|
|
86
|
+
"cto.auto_collect.failed",
|
|
87
|
+
);
|
|
88
|
+
})
|
|
89
|
+
.finally(() => {
|
|
90
|
+
inFlight.delete(promise);
|
|
91
|
+
});
|
|
92
|
+
inFlight.add(promise);
|
|
93
|
+
return {
|
|
94
|
+
triggered: true,
|
|
95
|
+
reason: "triggered",
|
|
96
|
+
projectRoot,
|
|
97
|
+
peerCount: peers.length,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function drain() {
|
|
102
|
+
await Promise.allSettled([...inFlight]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
handleSessionStarted,
|
|
107
|
+
drain,
|
|
108
|
+
});
|
|
109
|
+
}
|