agent-relay-server 0.3.3 → 0.3.5
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/bin/agent-relay-codex.ts +59 -1
- package/codex/README.md +2 -0
- package/codex/live-sidecar.ts +5 -1
- package/codex/plugin/.codex-plugin/plugin.json +10 -3
- package/codex/relay.ts +4 -0
- package/codex/smoke/fallback.ts +125 -0
- package/package.json +3 -2
- package/public/index.html +5 -5
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
@@ -149,6 +149,15 @@ function samePath(left: string, right: string): boolean {
|
|
|
149
149
|
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function isAlive(pid: number): boolean {
|
|
153
|
+
try {
|
|
154
|
+
process.kill(pid, 0);
|
|
155
|
+
return true;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
function candidateNames(command: string): string[] {
|
|
153
162
|
if (process.platform !== "win32") return [command];
|
|
154
163
|
const extensions = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
|
|
@@ -278,6 +287,48 @@ async function waitForPort(url: string, child: ReturnType<typeof Bun.spawn>): Pr
|
|
|
278
287
|
throw new Error(`timed out waiting for ${url}`);
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
async function waitForSidecarPid(runDir: string, timeoutMs: number): Promise<boolean> {
|
|
291
|
+
const pidsPath = join(runDir, "sidecar-pids.txt");
|
|
292
|
+
const deadline = Date.now() + timeoutMs;
|
|
293
|
+
|
|
294
|
+
while (Date.now() < deadline) {
|
|
295
|
+
if (existsSync(pidsPath)) {
|
|
296
|
+
for (const line of readFileSync(pidsPath, "utf8").split("\n")) {
|
|
297
|
+
const pid = Number(line.trim());
|
|
298
|
+
if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await Bun.sleep(100);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function spawnFallbackSidecar(runDir: string, env: Record<string, string | undefined>): number {
|
|
308
|
+
const autoDir = join(runDir, "auto");
|
|
309
|
+
mkdirSync(autoDir, { recursive: true });
|
|
310
|
+
|
|
311
|
+
const sidecarEnv: Record<string, string | undefined> = {
|
|
312
|
+
...env,
|
|
313
|
+
CODEX_THREAD_MODE: "auto",
|
|
314
|
+
CODEX_LIVE_CWD: process.cwd(),
|
|
315
|
+
CODEX_LIVE_STATE_PATH: join(autoDir, "live-state.json"),
|
|
316
|
+
};
|
|
317
|
+
delete sidecarEnv.CODEX_THREAD_ID;
|
|
318
|
+
|
|
319
|
+
const sidecar = Bun.spawn(["bun", "run", join(activePackageRoot(), "codex", "live-sidecar.ts")], {
|
|
320
|
+
env: sidecarEnv,
|
|
321
|
+
stdout: Bun.file(join(autoDir, "sidecar.log")),
|
|
322
|
+
stderr: Bun.file(join(autoDir, "sidecar.log")),
|
|
323
|
+
});
|
|
324
|
+
sidecar.unref();
|
|
325
|
+
|
|
326
|
+
writeFileSync(join(autoDir, "sidecar.pid"), String(sidecar.pid));
|
|
327
|
+
appendFileSync(join(runDir, "sidecar-pids.txt"), `${sidecar.pid}\n`);
|
|
328
|
+
|
|
329
|
+
return sidecar.pid;
|
|
330
|
+
}
|
|
331
|
+
|
|
281
332
|
function cleanupRun(runDir: string, appServer: ReturnType<typeof Bun.spawn> | null): void {
|
|
282
333
|
if (existsSync(runDir)) {
|
|
283
334
|
const pidsPath = join(runDir, "sidecar-pids.txt");
|
|
@@ -482,6 +533,13 @@ async function start(args: string[]): Promise<void> {
|
|
|
482
533
|
stdout: "inherit",
|
|
483
534
|
stderr: "inherit",
|
|
484
535
|
});
|
|
536
|
+
|
|
537
|
+
const sidecarDetected = await waitForSidecarPid(runDir, 2500);
|
|
538
|
+
if (!sidecarDetected && codex.exitCode === null) {
|
|
539
|
+
const pid = spawnFallbackSidecar(runDir, env);
|
|
540
|
+
console.error(`Agent Relay: SessionStart hook did not start sidecar; started fallback sidecar pid ${pid}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
485
543
|
const exitCode = await codex.exited;
|
|
486
544
|
shutdown();
|
|
487
545
|
process.exit(exitCode);
|
package/codex/README.md
CHANGED
|
@@ -16,6 +16,7 @@ This sidecar connects to a Codex app-server session and to Agent Relay, then del
|
|
|
16
16
|
- otherwise resumes the newest thread for the current `cwd`
|
|
17
17
|
- otherwise creates a new thread
|
|
18
18
|
- registers a relay agent with `client: codex-live`
|
|
19
|
+
- marks the relay agent `ready=true` once app-server + thread are attached
|
|
19
20
|
- polls relay inbox and delivers messages into the live thread
|
|
20
21
|
- coalesces ordinary relay bursts into one delivery turn
|
|
21
22
|
- reconnects to the app-server with exponential backoff after disconnects
|
|
@@ -55,6 +56,7 @@ For local development from this repo:
|
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
58
|
bun run bin/agent-relay-codex.ts
|
|
59
|
+
bun run codex:smoke:fallback
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
Useful environment variables:
|
package/codex/live-sidecar.ts
CHANGED
|
@@ -319,9 +319,13 @@ class CodexLiveSidecar {
|
|
|
319
319
|
: this.activeTurnId || this.threadStatus === "active"
|
|
320
320
|
? "busy"
|
|
321
321
|
: "idle";
|
|
322
|
+
const ready = this.appConnected && Boolean(this.threadId);
|
|
322
323
|
|
|
323
324
|
try {
|
|
324
|
-
await
|
|
325
|
+
await Promise.all([
|
|
326
|
+
this.relay.setStatus(this.agentId, status),
|
|
327
|
+
this.relay.setReady(this.agentId, ready),
|
|
328
|
+
]);
|
|
325
329
|
} catch {
|
|
326
330
|
// Best-effort status sync.
|
|
327
331
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Agent Relay integration for Codex sessions",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Edin Mujkanovic"
|
|
7
7
|
},
|
|
8
8
|
"repository": "https://github.com/edimuj/agent-relay",
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"agent-relay",
|
|
12
|
+
"multi-agent",
|
|
13
|
+
"codex"
|
|
14
|
+
],
|
|
11
15
|
"skills": "./skills/",
|
|
12
16
|
"interface": {
|
|
13
17
|
"displayName": "Agent Relay",
|
|
@@ -15,7 +19,10 @@
|
|
|
15
19
|
"longDescription": "Adds Codex-facing instructions for Agent Relay and pairs with the agent-relay-codex launcher for live incoming messages.",
|
|
16
20
|
"developerName": "Edin Mujkanovic",
|
|
17
21
|
"category": "Productivity",
|
|
18
|
-
"capabilities": [
|
|
22
|
+
"capabilities": [
|
|
23
|
+
"Read",
|
|
24
|
+
"Write"
|
|
25
|
+
],
|
|
19
26
|
"defaultPrompt": [
|
|
20
27
|
"Use Agent Relay to message another coding agent.",
|
|
21
28
|
"Check how this Codex session is connected to Agent Relay."
|
package/codex/relay.ts
CHANGED
|
@@ -68,6 +68,10 @@ export class RelayClient {
|
|
|
68
68
|
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/status`, { status });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
async setReady(agentId: string, ready: boolean): Promise<void> {
|
|
72
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/ready`, { ready });
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
async pollMessages(agentId: string, sinceId: number): Promise<RelayMessage[]> {
|
|
72
76
|
const url = new URL(`/api/messages`, this.baseUrl);
|
|
73
77
|
url.searchParams.set("for", agentId);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const ROOT_DIR = resolve(import.meta.dir, "../..");
|
|
6
|
+
|
|
7
|
+
function log(message: string): void {
|
|
8
|
+
console.error(`[codex-fallback-smoke] ${message}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function waitForExit(process: Bun.Subprocess, timeoutMs: number): Promise<number> {
|
|
12
|
+
const started = Date.now();
|
|
13
|
+
while (Date.now() - started < timeoutMs) {
|
|
14
|
+
if (process.exitCode !== null) return process.exitCode;
|
|
15
|
+
await Bun.sleep(100);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
process.kill();
|
|
19
|
+
} catch {
|
|
20
|
+
// Process already exited.
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`timed out waiting for process to exit after ${timeoutMs}ms`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeFakeCodex(path: string): void {
|
|
26
|
+
const script = `#!/usr/bin/env bash
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
if [[ "\${1:-}" == "plugin" && "\${2:-}" == "marketplace" && "\${3:-}" == "add" ]]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [[ "\${1:-}" == "app-server" && "\${2:-}" == "--listen" ]]; then
|
|
34
|
+
url="\${3:-}"
|
|
35
|
+
port="\${url##*:}"
|
|
36
|
+
exec node -e 'const net=require("node:net"); const port=Number(process.argv[1]); const server=net.createServer((socket)=>socket.destroy()); server.listen(port,"127.0.0.1"); setInterval(()=>{},1000);' "$port"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ "\${1:-}" == "--remote" ]]; then
|
|
40
|
+
sleep 6
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "fake codex: unsupported args: $*" >&2
|
|
45
|
+
exit 64
|
|
46
|
+
`;
|
|
47
|
+
writeFileSync(path, script, "utf8");
|
|
48
|
+
chmodSync(path, 0o755);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseRuntimeDir(stderr: string): string | null {
|
|
52
|
+
const match = stderr.match(/^Runtime:\s+(.+)$/m);
|
|
53
|
+
return match?.[1]?.trim() || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main(): Promise<void> {
|
|
57
|
+
const tempHome = mkdtempSync(join(tmpdir(), "agent-relay-codex-fallback-"));
|
|
58
|
+
const fakeBin = join(tempHome, "fake-bin");
|
|
59
|
+
mkdirSync(fakeBin, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const fakeCodexPath = join(fakeBin, "codex");
|
|
62
|
+
writeFakeCodex(fakeCodexPath);
|
|
63
|
+
|
|
64
|
+
const env = {
|
|
65
|
+
...process.env,
|
|
66
|
+
HOME: tempHome,
|
|
67
|
+
USERPROFILE: tempHome,
|
|
68
|
+
PATH: `${fakeBin}:${process.env.PATH || ""}`,
|
|
69
|
+
AGENT_RELAY_URL: process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
log("starting launcher with fake codex runtime (hooks intentionally not fired)");
|
|
73
|
+
const run = Bun.spawn(["bun", "run", "bin/agent-relay-codex.ts", "start"], {
|
|
74
|
+
cwd: ROOT_DIR,
|
|
75
|
+
env,
|
|
76
|
+
stdout: "pipe",
|
|
77
|
+
stderr: "pipe",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const exitCode = await waitForExit(run, 25_000);
|
|
81
|
+
const stderr = await new Response(run.stderr).text();
|
|
82
|
+
const stdout = await new Response(run.stdout).text();
|
|
83
|
+
|
|
84
|
+
if (exitCode !== 0) {
|
|
85
|
+
throw new Error(`launcher exited ${exitCode}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const runtimeDir = parseRuntimeDir(stderr);
|
|
89
|
+
if (!runtimeDir) {
|
|
90
|
+
throw new Error(`missing runtime directory in launcher output\nstderr:\n${stderr}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!stderr.includes("SessionStart hook did not start sidecar; started fallback sidecar pid")) {
|
|
94
|
+
throw new Error(`fallback start message not found\nstderr:\n${stderr}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fallbackPidPath = join(runtimeDir, "auto", "sidecar.pid");
|
|
98
|
+
const fallbackLogPath = join(runtimeDir, "auto", "sidecar.log");
|
|
99
|
+
const sidecarPidsPath = join(runtimeDir, "sidecar-pids.txt");
|
|
100
|
+
|
|
101
|
+
if (!existsSync(fallbackPidPath)) {
|
|
102
|
+
throw new Error(`fallback pid file missing: ${fallbackPidPath}`);
|
|
103
|
+
}
|
|
104
|
+
if (!existsSync(sidecarPidsPath)) {
|
|
105
|
+
throw new Error(`sidecar-pids missing: ${sidecarPidsPath}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pid = readFileSync(fallbackPidPath, "utf8").trim();
|
|
109
|
+
const pidsFile = readFileSync(sidecarPidsPath, "utf8");
|
|
110
|
+
if (!pid || !pidsFile.includes(pid)) {
|
|
111
|
+
throw new Error(`fallback pid ${pid || "<empty>"} not recorded in sidecar-pids.txt`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
log(`fallback sidecar created pid ${pid}`);
|
|
115
|
+
if (existsSync(fallbackLogPath)) {
|
|
116
|
+
log(`fallback log file present at ${fallbackLogPath}`);
|
|
117
|
+
}
|
|
118
|
+
log("fallback smoke passed");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
log(error instanceof Error ? error.stack || error.message : String(error));
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"codex:live": "bun run codex/live-sidecar.ts",
|
|
27
27
|
"codex:live:start": "bash codex/start-live.sh",
|
|
28
28
|
"codex:install": "bun run bin/agent-relay-codex.ts install",
|
|
29
|
-
"codex:doctor": "bun run bin/agent-relay-codex.ts doctor"
|
|
29
|
+
"codex:doctor": "bun run bin/agent-relay-codex.ts doctor",
|
|
30
|
+
"codex:smoke:fallback": "bun run codex/smoke/fallback.ts"
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
|
32
33
|
"agent",
|
package/public/index.html
CHANGED
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
<div class="card-body">
|
|
135
135
|
<div class="d-flex align-items-center">
|
|
136
136
|
<div>
|
|
137
|
-
<div class="text-secondary small">
|
|
137
|
+
<div class="text-secondary small">Active</div>
|
|
138
138
|
<div class="h1 mb-0 text-success" x-text="onlineCount"></div>
|
|
139
139
|
</div>
|
|
140
140
|
<i class="ti ti-circle-check ms-auto stat-card"></i>
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<div class="card">
|
|
177
177
|
<div class="card-header d-flex align-items-center">
|
|
178
178
|
<h3 class="card-title">Agents</h3>
|
|
179
|
-
<span class="badge bg-success text-white ms-auto" x-text="onlineCount + '
|
|
179
|
+
<span class="badge bg-success text-white ms-auto" x-text="onlineCount + ' active'"></span>
|
|
180
180
|
</div>
|
|
181
181
|
<div class="list-group list-group-flush" style="max-height: 60vh; overflow-y: auto">
|
|
182
182
|
<template x-for="a in sortedAgents.slice(0, 20)" :key="a.id">
|
|
@@ -319,7 +319,7 @@
|
|
|
319
319
|
<div class="card">
|
|
320
320
|
<div class="card-body text-center text-secondary py-5">
|
|
321
321
|
<i class="ti ti-robot-off" style="font-size:48px; opacity:0.3"></i>
|
|
322
|
-
<p class="mt-2" x-text="showOffline ? 'No agents registered' : 'No agents
|
|
322
|
+
<p class="mt-2" x-text="showOffline ? 'No agents registered' : 'No active agents — enable Show Offline'"></p>
|
|
323
323
|
</div>
|
|
324
324
|
</div>
|
|
325
325
|
</template>
|
|
@@ -822,11 +822,11 @@ function relay() {
|
|
|
822
822
|
// ── Computed ──
|
|
823
823
|
|
|
824
824
|
get onlineCount() {
|
|
825
|
-
return this.agents.filter(a => a.status
|
|
825
|
+
return this.agents.filter(a => a.status !== 'offline').length;
|
|
826
826
|
},
|
|
827
827
|
|
|
828
828
|
get sortedAgents() {
|
|
829
|
-
let list = this.showOffline ? [...this.agents] : this.agents.filter(a => a.status
|
|
829
|
+
let list = this.showOffline ? [...this.agents] : this.agents.filter(a => a.status !== 'offline');
|
|
830
830
|
const dir = this.agentSortDir === 'desc' ? -1 : 1;
|
|
831
831
|
return list.sort((a, b) => {
|
|
832
832
|
let cmp = 0;
|