agent-relay-orchestrator 0.65.2 → 0.67.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/package.json +2 -2
- package/src/config.ts +8 -0
- package/src/index.ts +4 -0
- package/src/maintenance.ts +51 -0
- package/src/tmux-socket-sweeper.ts +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test": "bun test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"agent-relay-sdk": "0.2.
|
|
19
|
+
"agent-relay-sdk": "0.2.43"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "latest",
|
package/src/config.ts
CHANGED
|
@@ -62,6 +62,14 @@ export function workspaceDepsMode(): string {
|
|
|
62
62
|
return (process.env.AGENT_RELAY_WORKSPACE_DEPS || "symlink").toLowerCase();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export function tmuxSocketSweepEnabled(): boolean {
|
|
66
|
+
return process.env.AGENT_RELAY_TMUX_SOCKET_SWEEP !== "0";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function tmuxSocketSweepIntervalMs(): number {
|
|
70
|
+
return envNonNegativeMax("AGENT_RELAY_TMUX_SOCKET_SWEEP_INTERVAL_MS", 10 * 60 * 1000, 60_000);
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
export const TERMINAL_FLUSH_MS = envNonNegativeMax("AGENT_RELAY_TERMINAL_FLUSH_MS", 6, 0);
|
|
66
74
|
export const TERMINAL_FLUSH_MAX_BYTES = envNonNegativeMax("AGENT_RELAY_TERMINAL_FLUSH_MAX_BYTES", 65536, 4096);
|
|
67
75
|
export const TERMINAL_BACKPRESSURE_MAX_BYTES = envNonNegativeMax("AGENT_RELAY_TERMINAL_BACKPRESSURE_MAX_BYTES", 8 << 20, 1 << 20);
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { startApiServer } from "./api";
|
|
|
8
8
|
import { recoverManagedAgents } from "./recovery";
|
|
9
9
|
import { ProviderProbeCache } from "./provider-probe";
|
|
10
10
|
import { sweepEmptyWorkspaceContainers, workspacesRoot } from "./workspace-probe";
|
|
11
|
+
import { startOrchestratorMaintenanceScheduler } from "./maintenance";
|
|
11
12
|
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
|
|
@@ -78,6 +79,9 @@ async function startup(): Promise<void> {
|
|
|
78
79
|
// Recover existing tmux sessions
|
|
79
80
|
await recoverManagedAgents(config, control, relay);
|
|
80
81
|
|
|
82
|
+
// Host-local maintenance must run where the agent processes and tmux sockets live.
|
|
83
|
+
startOrchestratorMaintenanceScheduler();
|
|
84
|
+
|
|
81
85
|
// Sweep empty workspace container dirs left behind by prior cleanups (#280).
|
|
82
86
|
const swept = sweepEmptyWorkspaceContainers(workspacesRoot(config.baseDir));
|
|
83
87
|
if (swept.length > 0) console.error(`[orchestrator] Swept ${swept.length} empty workspace container(s)`);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { tmuxSocketSweepEnabled, tmuxSocketSweepIntervalMs } from "./config";
|
|
2
|
+
import { sweepStaleTmuxSockets } from "./tmux-socket-sweeper";
|
|
3
|
+
|
|
4
|
+
interface OrchestratorMaintenanceJobDefinition {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
intervalMs: number;
|
|
8
|
+
runOnStart: boolean;
|
|
9
|
+
handler(): unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let started = false;
|
|
13
|
+
const timers: Timer[] = [];
|
|
14
|
+
|
|
15
|
+
export const definitions: OrchestratorMaintenanceJobDefinition[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "tmux-socket-sweep",
|
|
18
|
+
title: "Tmux socket sweep",
|
|
19
|
+
intervalMs: tmuxSocketSweepIntervalMs(),
|
|
20
|
+
runOnStart: true,
|
|
21
|
+
handler() {
|
|
22
|
+
const result = sweepStaleTmuxSockets();
|
|
23
|
+
if (result.scanned > 0 || result.failed.length > 0) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[orchestrator] Tmux socket sweep: removed=${result.removed} kept=${result.kept} scanned=${result.scanned} dir=${result.dir}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
for (const failure of result.failed) {
|
|
29
|
+
console.error(`[orchestrator] Tmux socket sweep failed for ${failure.socket}: ${failure.error}`);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function startOrchestratorMaintenanceScheduler(): void {
|
|
37
|
+
if (started || !tmuxSocketSweepEnabled()) return;
|
|
38
|
+
started = true;
|
|
39
|
+
for (const definition of definitions) {
|
|
40
|
+
if (definition.runOnStart) void runJob(definition);
|
|
41
|
+
timers.push(setInterval(() => void runJob(definition), definition.intervalMs));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runJob(definition: OrchestratorMaintenanceJobDefinition): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
await Promise.resolve(definition.handler());
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`[orchestrator] Maintenance job ${definition.id} failed: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { tmuxCommand } from "agent-relay-sdk/tmux-utils";
|
|
4
|
+
|
|
5
|
+
const SOCKET_PREFIX = "agent-relay-";
|
|
6
|
+
|
|
7
|
+
interface TmuxSocketSweepResult {
|
|
8
|
+
dir: string;
|
|
9
|
+
scanned: number;
|
|
10
|
+
removed: number;
|
|
11
|
+
kept: number;
|
|
12
|
+
failed: Array<{ socket: string; error: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function tmuxSocketDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
16
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
17
|
+
const tmuxTmp = env.TMUX_TMPDIR?.trim();
|
|
18
|
+
if (!tmuxTmp) return `/tmp/tmux-${uid}`;
|
|
19
|
+
return basename(tmuxTmp) === `tmux-${uid}` ? tmuxTmp : join(tmuxTmp, `tmux-${uid}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sweepStaleTmuxSockets(input: { dir?: string; env?: NodeJS.ProcessEnv } = {}): TmuxSocketSweepResult {
|
|
23
|
+
const dir = input.dir ?? tmuxSocketDir(input.env);
|
|
24
|
+
const result: TmuxSocketSweepResult = { dir, scanned: 0, removed: 0, kept: 0, failed: [] };
|
|
25
|
+
if (!existsSync(dir)) return result;
|
|
26
|
+
|
|
27
|
+
for (const entry of readdirSync(dir)) {
|
|
28
|
+
if (!entry.startsWith(SOCKET_PREFIX)) continue;
|
|
29
|
+
const path = join(dir, entry);
|
|
30
|
+
let removable = false;
|
|
31
|
+
try {
|
|
32
|
+
const stat = lstatSync(path);
|
|
33
|
+
removable = stat.isSocket() || stat.isFile();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
result.failed.push({ socket: entry, error: String(error) });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!removable) continue;
|
|
39
|
+
|
|
40
|
+
result.scanned += 1;
|
|
41
|
+
if (tmuxSocketHasLiveServer(entry, input.env)) {
|
|
42
|
+
result.kept += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
rmSync(path, { force: true });
|
|
48
|
+
result.removed += 1;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
result.failed.push({ socket: entry, error: String(error) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tmuxSocketHasLiveServer(socketName: string, env?: NodeJS.ProcessEnv): boolean {
|
|
58
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "list-sessions"), {
|
|
59
|
+
stdin: "ignore",
|
|
60
|
+
stdout: "ignore",
|
|
61
|
+
stderr: "ignore",
|
|
62
|
+
...(env ? { env } : {}),
|
|
63
|
+
});
|
|
64
|
+
return result.exitCode === 0;
|
|
65
|
+
}
|