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.
@@ -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:
@@ -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 this.relay.setStatus(this.agentId, status);
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",
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": ["agent-relay", "multi-agent", "codex"],
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": ["Read", "Write"],
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",
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">Online</div>
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 + ' online'"></span>
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 online — enable Show Offline'"></p>
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 === 'online').length;
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 === 'online');
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;