@triflux/remote 10.31.0 → 10.32.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 +23 -1
- package/hub/team/cto-auto-collect.mjs +109 -0
- package/package.json +1 -1
package/hub/server.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import { createStoreAdapter } from "./store-adapter.mjs";
|
|
|
56
56
|
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
57
57
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
58
58
|
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
59
|
+
import { createCtoAutoCollector } from "./team/cto-auto-collect.mjs";
|
|
59
60
|
import {
|
|
60
61
|
createSynapseRegistry,
|
|
61
62
|
projectPeer,
|
|
@@ -69,6 +70,21 @@ registerTeamBridge(nativeProxy);
|
|
|
69
70
|
const hubLog = createModuleLogger("hub");
|
|
70
71
|
const brokerDiagnosticsAttached = new WeakSet();
|
|
71
72
|
|
|
73
|
+
async function runCtoCollect(args, opts) {
|
|
74
|
+
try {
|
|
75
|
+
const mod = await import("triflux/cto/collect.mjs");
|
|
76
|
+
if (typeof mod?.runCollect === "function") {
|
|
77
|
+
return await mod.runCollect(args, opts);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
hubLog.warn(
|
|
81
|
+
{ err: String(error?.message || error) },
|
|
82
|
+
"cto.auto_collect.remote_collect_unavailable",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
73
89
|
const PUBLIC_PATHS = new Set(["/", "/status", "/health", "/healthz"]);
|
|
74
90
|
const RATE_LIMIT_MAX = 100; // requests per window
|
|
@@ -1009,10 +1025,16 @@ export async function startHub({
|
|
|
1009
1025
|
registry: synapseRegistry,
|
|
1010
1026
|
locks: swarmLocks,
|
|
1011
1027
|
});
|
|
1028
|
+
const ctoAutoCollector = createCtoAutoCollector({
|
|
1029
|
+
registry: synapseRegistry,
|
|
1030
|
+
runCollect: runCtoCollect,
|
|
1031
|
+
logger: hubLog,
|
|
1032
|
+
});
|
|
1012
1033
|
|
|
1013
1034
|
// Synapse Layer 5: emitter subscribers — bridge events to hub logging
|
|
1014
|
-
synapseEmitter.on("synapse.session.started", ({ sessionId }) => {
|
|
1035
|
+
synapseEmitter.on("synapse.session.started", ({ sessionId, session }) => {
|
|
1015
1036
|
hubLog.info({ sessionId }, "synapse.session.started");
|
|
1037
|
+
ctoAutoCollector.handleSessionStarted({ sessionId, session });
|
|
1016
1038
|
});
|
|
1017
1039
|
synapseEmitter.on("synapse.session.heartbeat", ({ sessionId }) => {
|
|
1018
1040
|
hubLog.debug({ sessionId }, "synapse.session.heartbeat");
|
|
@@ -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
|
+
}
|