@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 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.31.0",
3
+ "version": "10.32.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",