@triflux/remote 10.51.0 → 10.53.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.
Files changed (62) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/claude-transcript.mjs +19 -0
  13. package/hub/team/cli/commands/debug.mjs +1 -2
  14. package/hub/team/cli/commands/start/index.mjs +2 -1
  15. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  16. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  17. package/hub/team/cli/services/state-store.mjs +5 -20
  18. package/hub/team/headless.mjs +132 -551
  19. package/hub/team/psmux.mjs +3 -33
  20. package/hub/team/results-index.mjs +92 -0
  21. package/hub/team/session-context.mjs +1 -1
  22. package/hub/team/tui-viewer.mjs +9 -56
  23. package/hub/team/tui.mjs +0 -37
  24. package/hub/team/uds-orchestrator.mjs +20 -599
  25. package/hub/workers/factory.mjs +6 -51
  26. package/hub/workers/interface.mjs +1 -7
  27. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  28. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  29. package/package.json +3 -8
  30. package/scripts/lib/agent-route-policy.mjs +0 -67
  31. package/scripts/lib/cli-claude.mjs +0 -12
  32. package/scripts/lib/env-probe.mjs +0 -70
  33. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  34. package/scripts/lib/mcp-guard-engine.mjs +42 -101
  35. package/hub/hub-lifecycle.mjs +0 -248
  36. package/hub/pipe.mjs +0 -825
  37. package/hub/promote-penalties.mjs +0 -138
  38. package/hub/public/dashboard.html +0 -416
  39. package/hub/schema.sql +0 -209
  40. package/hub/server.mjs +0 -3048
  41. package/hub/store-adapter.mjs +0 -182
  42. package/hub/store.mjs +0 -1736
  43. package/hub/team/cli/services/hub-client.mjs +0 -178
  44. package/hub/team/conductor-registry.mjs +0 -121
  45. package/hub/team/cto-auto-collect.mjs +0 -115
  46. package/hub/team/lead-control.mjs +0 -191
  47. package/hub/team/session-sync.mjs +0 -316
  48. package/hub/team/swarm-intent.mjs +0 -18
  49. package/hub/team/synapse-cli.mjs +0 -458
  50. package/hub/team/synapse-http.mjs +0 -311
  51. package/hub/team/synapse-registry.mjs +0 -618
  52. package/hub/team/tui-synapse.mjs +0 -223
  53. package/hub/team/worker-sandbox.mjs +0 -125
  54. package/hub/tools.mjs +0 -900
  55. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  56. package/hub/workers/delegator-mcp.mjs +0 -1098
  57. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  58. package/scripts/lib/claudemd-scanner.mjs +0 -26
  59. package/scripts/lib/context.mjs +0 -51
  60. package/scripts/lib/handoff.mjs +0 -187
  61. package/scripts/lib/logger.mjs +0 -122
  62. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,178 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- unlinkSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { join } from "node:path";
9
- import { publishLeadControl as publishLeadControlBridge } from "../../lead-control.mjs";
10
- import {
11
- getTeamStatus as fetchTeamStatus,
12
- subscribeToLeadCommands as pullLeadCommands,
13
- } from "../../session-sync.mjs";
14
- import { HUB_PID_DIR } from "./state-store.mjs";
15
-
16
- const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
17
- const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
18
-
19
- export function formatHostForUrl(host) {
20
- return host.includes(":") ? `[${host}]` : host;
21
- }
22
-
23
- export function buildHubBaseUrl(host, port) {
24
- return `http://${formatHostForUrl(host)}:${port}`;
25
- }
26
-
27
- export function getDefaultHubPort() {
28
- const envPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
29
- return Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 27888;
30
- }
31
-
32
- export function getDefaultHubUrl() {
33
- return `${buildHubBaseUrl("127.0.0.1", getDefaultHubPort())}/mcp`;
34
- }
35
-
36
- function normalizeLoopbackHost(host) {
37
- if (typeof host !== "string") return "127.0.0.1";
38
- const candidate = host.trim();
39
- return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
40
- }
41
-
42
- async function probeHubStatus(host, port, timeoutMs = 1500) {
43
- try {
44
- const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
45
- signal: AbortSignal.timeout(timeoutMs),
46
- });
47
- if (!res.ok) return null;
48
- const data = await res.json();
49
- return data?.hub ? data : null;
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- export async function getHubInfo() {
56
- const probePort = getDefaultHubPort();
57
-
58
- if (existsSync(HUB_PID_FILE)) {
59
- try {
60
- const raw = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
61
- const pid = Number(raw?.pid);
62
- if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
63
- process.kill(pid, 0);
64
- const host = normalizeLoopbackHost(raw?.host);
65
- const port = Number(raw?.port) || probePort;
66
- const status = await probeHubStatus(host, port, 1200);
67
- if (!status) throw new Error("pid file process is not a healthy hub");
68
- return {
69
- ...raw,
70
- pid,
71
- host,
72
- port,
73
- url: `${buildHubBaseUrl(host, port)}/mcp`,
74
- };
75
- } catch {
76
- try {
77
- unlinkSync(HUB_PID_FILE);
78
- } catch {}
79
- }
80
- }
81
-
82
- for (const portCandidate of Array.from(new Set([probePort, 27888]))) {
83
- const data = await probeHubStatus("127.0.0.1", portCandidate, 1200);
84
- if (!data) continue;
85
- const port = Number(data.port) || portCandidate;
86
- const pid = Number(data.pid);
87
- const recovered = {
88
- pid: Number.isFinite(pid) ? pid : null,
89
- host: "127.0.0.1",
90
- port,
91
- url: `${buildHubBaseUrl("127.0.0.1", port)}/mcp`,
92
- discovered: true,
93
- };
94
- if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
95
- try {
96
- mkdirSync(HUB_PID_DIR, { recursive: true });
97
- writeFileSync(
98
- HUB_PID_FILE,
99
- JSON.stringify({ ...recovered, started: Date.now() }),
100
- );
101
- } catch {}
102
- }
103
- return recovered;
104
- }
105
- return null;
106
- }
107
-
108
- export async function fetchHubTaskList(state) {
109
- const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
110
- const teamName = state?.sessionName || null;
111
- if (!teamName) return [];
112
-
113
- try {
114
- const res = await fetch(`${hubBase}/bridge/team/task-list`, {
115
- method: "POST",
116
- headers: { "Content-Type": "application/json" },
117
- body: JSON.stringify({ team_name: teamName }),
118
- signal: AbortSignal.timeout(2000),
119
- });
120
- const data = await res.json();
121
- return data?.ok ? data.data?.tasks || [] : [];
122
- } catch {
123
- return [];
124
- }
125
- }
126
-
127
- export async function publishLeadControl(
128
- state,
129
- targetMember,
130
- command,
131
- reason = "",
132
- ) {
133
- const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
134
- const leadAgent =
135
- (state?.members || []).find((member) => member.role === "lead")?.agentId ||
136
- "lead";
137
- const targetAgent =
138
- typeof targetMember === "string" ? targetMember : targetMember?.agentId;
139
-
140
- const result = await publishLeadControlBridge({
141
- hubUrl: hubBase,
142
- fromAgent: leadAgent,
143
- toAgent: targetAgent,
144
- command,
145
- reason,
146
- payload: {
147
- issued_by: leadAgent,
148
- issued_at: Date.now(),
149
- },
150
- });
151
-
152
- return !!result?.ok;
153
- }
154
-
155
- export async function subscribeToLeadCommands(state, member, options = {}) {
156
- const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
157
- const fallbackAgentId =
158
- (state?.members || []).find((candidate) => candidate.role === "lead")
159
- ?.agentId || null;
160
- const agentId =
161
- typeof member === "string"
162
- ? member
163
- : member?.agentId || options?.agentId || fallbackAgentId;
164
-
165
- return await pullLeadCommands({
166
- hubUrl: hubBase,
167
- agentId,
168
- ...options,
169
- });
170
- }
171
-
172
- export async function getTeamStatus(state, options = {}) {
173
- const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
174
- return await fetchTeamStatus({
175
- hubUrl: hubBase,
176
- ...options,
177
- });
178
- }
@@ -1,121 +0,0 @@
1
- function normalizeSessionId(sessionId) {
2
- if (sessionId == null) return "";
3
- return String(sessionId).trim();
4
- }
5
-
6
- export function createConductorRegistry() {
7
- const sessions = new Map();
8
-
9
- return Object.freeze({
10
- register(sessionId, conductor) {
11
- const normalizedSessionId = normalizeSessionId(sessionId);
12
- if (
13
- !normalizedSessionId ||
14
- !conductor ||
15
- typeof conductor.sendInput !== "function"
16
- ) {
17
- return false;
18
- }
19
- sessions.set(normalizedSessionId, conductor);
20
- return true;
21
- },
22
-
23
- unregister(sessionId, conductor = null) {
24
- const normalizedSessionId = normalizeSessionId(sessionId);
25
- if (!normalizedSessionId || !sessions.has(normalizedSessionId))
26
- return false;
27
- if (conductor && sessions.get(normalizedSessionId) !== conductor)
28
- return false;
29
- return sessions.delete(normalizedSessionId);
30
- },
31
-
32
- get(sessionId) {
33
- const normalizedSessionId = normalizeSessionId(sessionId);
34
- if (!normalizedSessionId) return null;
35
- return sessions.get(normalizedSessionId) || null;
36
- },
37
-
38
- clear() {
39
- sessions.clear();
40
- },
41
-
42
- list() {
43
- return [...sessions.keys()];
44
- },
45
- });
46
- }
47
-
48
- let conductorRegistry = null;
49
-
50
- export function getConductorRegistry() {
51
- return conductorRegistry;
52
- }
53
-
54
- export function setConductorRegistry(nextRegistry) {
55
- const previousRegistry = conductorRegistry;
56
- conductorRegistry = nextRegistry ?? null;
57
- return previousRegistry;
58
- }
59
-
60
- export function ensureConductorRegistry() {
61
- if (!conductorRegistry) {
62
- conductorRegistry = createConductorRegistry();
63
- }
64
- return conductorRegistry;
65
- }
66
-
67
- export function sendInputToConductorSession(sessionId, text) {
68
- const normalizedSessionId = normalizeSessionId(sessionId);
69
- const normalizedText = typeof text === "string" ? text : "";
70
-
71
- if (!normalizedSessionId || !normalizedText) {
72
- return {
73
- ok: false,
74
- error: {
75
- code: "INVALID_SEND_INPUT",
76
- message: "session_id, text 필수",
77
- },
78
- };
79
- }
80
-
81
- const registry = getConductorRegistry();
82
- if (!registry) {
83
- return {
84
- ok: false,
85
- error: {
86
- code: "CONDUCTOR_REGISTRY_NOT_AVAILABLE",
87
- message: "Conductor registry가 초기화되지 않았습니다",
88
- },
89
- };
90
- }
91
-
92
- const conductor = registry.get(normalizedSessionId);
93
- if (!conductor) {
94
- return {
95
- ok: false,
96
- error: {
97
- code: "CONDUCTOR_SESSION_NOT_FOUND",
98
- message: `Conductor session not found: ${normalizedSessionId}`,
99
- },
100
- };
101
- }
102
-
103
- const sent = conductor.sendInput(normalizedSessionId, normalizedText);
104
- if (!sent) {
105
- return {
106
- ok: false,
107
- error: {
108
- code: "SEND_INPUT_FAILED",
109
- message: `입력 전송 실패: ${normalizedSessionId}`,
110
- },
111
- };
112
- }
113
-
114
- return {
115
- ok: true,
116
- data: {
117
- session_id: normalizedSessionId,
118
- sent: true,
119
- },
120
- };
121
- }
@@ -1,115 +0,0 @@
1
- import { existsSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import { resolveLakeRootDir } from "../../cto/lake-root.mjs";
5
- import { resolveRoleControlSnapshot } from "../lib/cto-env.mjs";
6
-
7
- const DEFAULT_DEBOUNCE_MS = 120_000;
8
-
9
- export function isCtoAutoCollectDisabled(env = process.env) {
10
- return !resolveRoleControlSnapshot(env).auto_collect_enabled;
11
- }
12
-
13
- function freshCurrentMd(projectRoot, now, debounceMs) {
14
- try {
15
- const currentMd = join(projectRoot, ".triflux", "lake", "current.md");
16
- if (!existsSync(currentMd)) return false;
17
- return now - statSync(currentMd).mtimeMs < debounceMs;
18
- } catch {
19
- return false;
20
- }
21
- }
22
-
23
- export function createCtoAutoCollector(opts = {}) {
24
- const registry = opts.registry;
25
- const runCollect = opts.runCollect;
26
- const logger = opts.logger || null;
27
- const debounceMs = Number(opts.debounceMs || DEFAULT_DEBOUNCE_MS);
28
- const now = opts.now || (() => Date.now());
29
- const env = opts.env || process.env;
30
- const lastCollectMs = opts.lastCollectMs || new Map();
31
- const inFlight = new Set();
32
-
33
- function handleSessionStarted({ sessionId, session } = {}) {
34
- if (isCtoAutoCollectDisabled(env))
35
- return { triggered: false, reason: "disabled" };
36
- if (!registry || typeof registry.querySessions !== "function") {
37
- return { triggered: false, reason: "missing-registry" };
38
- }
39
- if (typeof runCollect !== "function") {
40
- return { triggered: false, reason: "missing-runCollect" };
41
- }
42
-
43
- const cwd = typeof session?.cwd === "string" ? session.cwd : "";
44
- const worktree =
45
- typeof session?.worktreePath === "string" ? session.worktreePath : "";
46
- const projectRoot = resolveLakeRootDir(worktree || cwd);
47
- if (!projectRoot)
48
- return { triggered: false, reason: "missing-project-root" };
49
-
50
- let peers = [];
51
- try {
52
- peers = registry.querySessions({
53
- cwd,
54
- worktree,
55
- excludeSessionId: sessionId,
56
- });
57
- } catch (error) {
58
- logger?.warn?.(
59
- { sessionId, err: String(error?.message || error) },
60
- "cto.auto_collect.query_failed",
61
- );
62
- // peer 조회 실패는 collect 를 막지 않는다 — peer 정보는 이제 라벨 용도뿐이라
63
- // registry 일시 장애에도 solo 로 진행해 collect 회복력을 유지한다.
64
- peers = [];
65
- }
66
- // 단일 세션(peer 0개)도 허용한다 — 사용자 선택. peer 유무와 무관하게
67
- // 아래 debounce + fresh-lake 게이트가 collect 빈도를 제어한다.
68
- //
69
- // 주의: peerCount 는 best-effort 라벨이다. querySessions 는 raw
70
- // cwd/worktreePath 로 매칭하지만 projectRoot 는 resolved git toplevel 이라,
71
- // 같은 repo 의 다른 하위 폴더 세션은 peer 로 안 잡혀 triggered-solo 로 보고될
72
- // 수 있다. 즉 라벨은 repo-scoped 가 아니라 exact-cwd-scoped 다 — 향후 peer
73
- // 기반 로직을 이 라벨 위에 다시 올릴 때 주의한다.
74
- const peerCount = Array.isArray(peers) ? peers.length : 0;
75
-
76
- const currentTime = now();
77
- const last = lastCollectMs.get(projectRoot) || 0;
78
- if (currentTime - last < debounceMs) {
79
- return { triggered: false, reason: "debounced" };
80
- }
81
- if (freshCurrentMd(projectRoot, currentTime, debounceMs)) {
82
- lastCollectMs.set(projectRoot, currentTime);
83
- return { triggered: false, reason: "fresh-lake" };
84
- }
85
-
86
- lastCollectMs.set(projectRoot, currentTime);
87
- const promise = Promise.resolve()
88
- .then(() => runCollect([], { rootDir: projectRoot }))
89
- .catch((error) => {
90
- logger?.warn?.(
91
- { projectRoot, err: String(error?.message || error) },
92
- "cto.auto_collect.failed",
93
- );
94
- })
95
- .finally(() => {
96
- inFlight.delete(promise);
97
- });
98
- inFlight.add(promise);
99
- return {
100
- triggered: true,
101
- reason: peerCount > 0 ? "triggered" : "triggered-solo",
102
- projectRoot,
103
- peerCount,
104
- };
105
- }
106
-
107
- async function drain() {
108
- await Promise.allSettled([...inFlight]);
109
- }
110
-
111
- return Object.freeze({
112
- handleSessionStarted,
113
- drain,
114
- });
115
- }
@@ -1,191 +0,0 @@
1
- import { readState } from "@triflux/core/hub/state.mjs";
2
-
3
- const DEFAULT_TIMEOUT_MS = 2500;
4
- const CONTROL_COMMAND_ALIASES = Object.freeze({
5
- stop: "abort",
6
- interrupt: "abort",
7
- });
8
-
9
- export const LEAD_CONTROL_COMMANDS = Object.freeze([
10
- "pause",
11
- "resume",
12
- "abort",
13
- "reassign",
14
- ]);
15
-
16
- function resolveFetch(fetchImpl) {
17
- if (typeof fetchImpl === "function") return fetchImpl;
18
- if (typeof globalThis.fetch === "function")
19
- return globalThis.fetch.bind(globalThis);
20
- return null;
21
- }
22
-
23
- function normalizeHubBaseUrl(hubUrl) {
24
- return String(hubUrl || "")
25
- .replace(/\/+$/, "")
26
- .replace(/\/mcp$/, "");
27
- }
28
-
29
- function normalizeCommand(command) {
30
- const raw = String(command || "")
31
- .trim()
32
- .toLowerCase();
33
- if (!raw) return "";
34
- return CONTROL_COMMAND_ALIASES[raw] || raw;
35
- }
36
-
37
- function safeAbortSignal(timeoutMs) {
38
- if (typeof AbortSignal?.timeout !== "function") return undefined;
39
- return AbortSignal.timeout(timeoutMs);
40
- }
41
-
42
- function isPidAlive(pid) {
43
- const resolvedPid = Number(pid);
44
- if (!Number.isFinite(resolvedPid) || resolvedPid <= 0) return false;
45
- try {
46
- process.kill(resolvedPid, 0);
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function resolveHeadlessHubUrl() {
54
- const stateUrl = normalizeHubBaseUrl(readState()?.url);
55
- if (stateUrl) return stateUrl;
56
-
57
- const envHubUrl = normalizeHubBaseUrl(process.env.TFX_HUB_URL);
58
- if (envHubUrl) return envHubUrl;
59
-
60
- const envPort = Number(process.env.TFX_HUB_PORT || "27888");
61
- const port = Number.isFinite(envPort) && envPort > 0 ? envPort : 27888;
62
- return `http://127.0.0.1:${port}`;
63
- }
64
-
65
- function hasLiveHubState() {
66
- const state = readState();
67
- return !!normalizeHubBaseUrl(state?.url) && isPidAlive(state?.pid);
68
- }
69
-
70
- function toHeadlessSessionAgentId(sessionName) {
71
- const normalizedSessionName = String(sessionName || "").trim();
72
- return normalizedSessionName ? `session:${normalizedSessionName}` : "";
73
- }
74
-
75
- function safeJson(res) {
76
- return res.json().catch(() => ({}));
77
- }
78
-
79
- export async function publishLeadControl({
80
- hubUrl,
81
- fromAgent = "lead",
82
- toAgent,
83
- command,
84
- reason = "",
85
- payload = {},
86
- traceId,
87
- correlationId,
88
- ttlMs = 3600000,
89
- timeoutMs = DEFAULT_TIMEOUT_MS,
90
- fetchImpl,
91
- } = {}) {
92
- const requestFetch = resolveFetch(fetchImpl);
93
- if (!requestFetch) {
94
- return { ok: false, error: "FETCH_UNAVAILABLE" };
95
- }
96
-
97
- const hubBase = normalizeHubBaseUrl(hubUrl);
98
- if (!hubBase) {
99
- return { ok: false, error: "HUB_URL_REQUIRED" };
100
- }
101
-
102
- const targetAgent = String(toAgent || "").trim();
103
- if (!targetAgent) {
104
- return { ok: false, error: "TARGET_AGENT_REQUIRED" };
105
- }
106
-
107
- const normalizedCommand = normalizeCommand(command);
108
- if (!LEAD_CONTROL_COMMANDS.includes(normalizedCommand)) {
109
- return {
110
- ok: false,
111
- error: "INVALID_COMMAND",
112
- allowed: LEAD_CONTROL_COMMANDS,
113
- command: normalizedCommand,
114
- };
115
- }
116
-
117
- try {
118
- const res = await requestFetch(`${hubBase}/bridge/control`, {
119
- method: "POST",
120
- headers: { "Content-Type": "application/json" },
121
- body: JSON.stringify({
122
- from_agent: String(fromAgent || "lead"),
123
- to_agent: targetAgent,
124
- command: normalizedCommand,
125
- reason: String(reason || ""),
126
- payload: payload && typeof payload === "object" ? payload : {},
127
- ttl_ms: ttlMs,
128
- trace_id: traceId,
129
- correlation_id: correlationId,
130
- }),
131
- signal: safeAbortSignal(timeoutMs),
132
- });
133
-
134
- const body = await safeJson(res);
135
- return {
136
- ok: res.ok && body?.ok !== false,
137
- status: res.status,
138
- body,
139
- command: normalizedCommand,
140
- };
141
- } catch (error) {
142
- return {
143
- ok: false,
144
- error: "CONTROL_PUBLISH_FAILED",
145
- message: error?.message || "control publish failed",
146
- command: normalizedCommand,
147
- };
148
- }
149
- }
150
-
151
- export async function publishHeadlessControl(
152
- sessionName,
153
- command,
154
- targetWorker = "*",
155
- ) {
156
- const sessionAgentId = toHeadlessSessionAgentId(sessionName);
157
- if (!sessionAgentId) {
158
- return {
159
- ok: false,
160
- noop: true,
161
- error: "SESSION_NAME_REQUIRED",
162
- command: String(command || "")
163
- .trim()
164
- .toLowerCase(),
165
- };
166
- }
167
-
168
- if (!normalizeHubBaseUrl(process.env.TFX_HUB_URL) && !hasLiveHubState()) {
169
- return {
170
- ok: false,
171
- noop: true,
172
- error: "HUB_UNAVAILABLE",
173
- command: String(command || "")
174
- .trim()
175
- .toLowerCase(),
176
- };
177
- }
178
-
179
- return await publishLeadControl({
180
- hubUrl: resolveHeadlessHubUrl(),
181
- fromAgent: "lead",
182
- toAgent: sessionAgentId,
183
- command,
184
- payload: {
185
- session_name: String(sessionName || "").trim(),
186
- target_worker: String(targetWorker || "*").trim() || "*",
187
- issued_by: "lead",
188
- issued_at: Date.now(),
189
- },
190
- });
191
- }