agent-relay-server 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -51,15 +51,22 @@ Open a second Claude Code session and say:
51
51
 
52
52
  ## Codex Quick Start
53
53
 
54
- Codex support uses the same relay server, plus a Codex plugin, a SessionStart
55
- hook, and a managed app-server launcher.
54
+ Codex live support uses the same relay server plus:
55
+
56
+ - `codex-relay` launcher
57
+ - a `SessionStart` hook
58
+ - the live sidecar (`codex/live-sidecar.ts`)
59
+
60
+ The installer also adds a Codex plugin skill bundle. That plugin is optional:
61
+ it adds in-session guidance, but live agent registration and incoming-message
62
+ delivery are handled by the hook + sidecar path.
56
63
 
57
64
  ```bash
58
65
  # terminal 1: central relay server
59
66
  bunx agent-relay-server@latest
60
67
 
61
68
  # one-time installer
62
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash
69
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash
63
70
 
64
71
  # start Codex with live Agent Relay support after restarting your shell
65
72
  codex-relay
@@ -87,7 +94,7 @@ codex-relay
87
94
  On Windows PowerShell:
88
95
 
89
96
  ```powershell
90
- irm https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.ps1 | iex
97
+ irm https://unpkg.com/agent-relay-server@latest/codex/install-codex.ps1 | iex
91
98
  codex-relay
92
99
  ```
93
100
 
@@ -102,23 +109,28 @@ bunx -p agent-relay-server@latest codex-relay
102
109
  Non-interactive alias opt-in:
103
110
 
104
111
  ```bash
105
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash -s -- --alias
112
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash -s -- --alias
106
113
  ```
107
114
 
108
115
  ```powershell
109
- $env:AGENT_RELAY_CODEX_ALIAS = "1"; irm https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.ps1 | iex
116
+ $env:AGENT_RELAY_CODEX_ALIAS = "1"; irm https://unpkg.com/agent-relay-server@latest/codex/install-codex.ps1 | iex
110
117
  ```
111
118
 
112
119
  ## What the Agent Sees
113
120
 
114
- When a session starts, the plugin's background monitor registers the agent and outputs its identity and messaging instructions as a notification. The agent is immediately aware of the relay and ready to communicate:
121
+ For Claude Code sessions, the plugin monitor registers the agent and outputs its
122
+ identity and messaging instructions as a notification:
115
123
 
116
124
  ```
117
125
  Agent Relay active. Your agent ID: macmini2-cli-myproject-a1b2c3
118
126
  Relay URL: http://localhost:4850 | Server: 0.3.2 | Plugin: 0.3.2
119
127
  ```
120
128
 
121
- Incoming messages arrive as monitor notifications. The agent sees them without being prompted:
129
+ In Claude, incoming messages arrive as monitor notifications.
130
+
131
+ In Codex, incoming messages are delivered as live turns by the sidecar.
132
+
133
+ Example incoming relay message:
122
134
 
123
135
  ```
124
136
  [msg:42] from backend-agent: Migration looks clean, tests pass. Ready to merge.
@@ -156,6 +168,9 @@ export AGENT_RELAY_URL=http://100.x.y.z:4850
156
168
  | `PORT` | `4850` | Server port |
157
169
  | `HOST` | `127.0.0.1` | Bind address |
158
170
  | `DB_PATH` | `agent-relay.db` | SQLite database path |
171
+ | `STALE_TTL_MS` | `600000` | Mark agents offline after this heartbeat gap |
172
+ | `OFFLINE_PRUNE_MS` | `86400000` | Delete offline agents older than this |
173
+ | `REAP_INTERVAL_MS` | `60000` | Reaper cadence for stale/offline cleanup |
159
174
  | `RETENTION_DAYS` | `30` | Auto-prune messages older than this |
160
175
 
161
176
  ### Plugin environment variables
package/codex/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Codex Live Sidecar
2
2
 
3
- First real Codex integration for Agent Relay.
3
+ Codex integration for Agent Relay.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -10,7 +10,7 @@ This sidecar connects to a Codex app-server session and to Agent Relay, then del
10
10
  - `turn/steer`
11
11
  - `turn/interrupt`
12
12
 
13
- ## Current MVP behavior
13
+ ## Current behavior
14
14
 
15
15
  - attaches to a loaded thread for the current `cwd` when one exists
16
16
  - otherwise resumes the newest thread for the current `cwd`
@@ -39,7 +39,7 @@ The packaged Codex path is:
39
39
 
40
40
  ```bash
41
41
  bunx agent-relay-server@latest
42
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash
42
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash
43
43
  # after restarting your shell
44
44
  codex-relay
45
45
  ```
@@ -73,7 +73,7 @@ Useful environment variables:
73
73
 
74
74
  ## Notes
75
75
 
76
- This is still an early sidecar cut. It now handles reconnects and basic coalescing, but it still lacks richer policies such as batching by sender, message prioritization queues, and more nuanced retry/backoff behavior.
76
+ Current sidecar behavior is stable for live delivery. Remaining gaps are advanced policies such as batching by sender, message prioritization queues, and more nuanced retry/backoff behavior.
77
77
 
78
78
  - `CODEX_THREAD_MODE=auto` will attach to an already loaded thread for the same `cwd`. That is what you want for real live control, but it also means the sidecar can attach to your current interactive Codex session if one is already open.
79
79
  - For isolated testing, set `CODEX_THREAD_MODE=start` so the sidecar always creates its own thread.
@@ -6,6 +6,11 @@ import { buildAgentIdentity } from "../relay.ts";
6
6
 
7
7
  type HookInput = {
8
8
  session_id?: string;
9
+ sessionId?: string;
10
+ thread_id?: string;
11
+ threadId?: string;
12
+ session?: { id?: string };
13
+ thread?: { id?: string };
9
14
  cwd?: string;
10
15
  model?: string;
11
16
  };
@@ -40,32 +45,56 @@ function outputContext(context: string): never {
40
45
  process.exit(0);
41
46
  }
42
47
 
48
+ function pickThreadId(input: HookInput): string {
49
+ const candidates = [
50
+ input.session_id,
51
+ input.sessionId,
52
+ input.thread_id,
53
+ input.threadId,
54
+ input.session?.id,
55
+ input.thread?.id,
56
+ ];
57
+ for (const value of candidates) {
58
+ if (typeof value === "string" && value.trim()) return value.trim();
59
+ }
60
+ return "";
61
+ }
62
+
63
+ function existingAlivePid(pidPath: string): number | null {
64
+ if (!existsSync(pidPath)) return null;
65
+ const existingPid = Number(readFileSync(pidPath, "utf8").trim());
66
+ if (!Number.isFinite(existingPid) || !isAlive(existingPid)) return null;
67
+ return existingPid;
68
+ }
69
+
43
70
  const input = JSON.parse(readStdin() || "{}") as HookInput;
44
71
  const packageRoot =
45
72
  process.env.AGENT_RELAY_CODEX_PACKAGE_ROOT || resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
46
73
  const appServerUrl = process.env.CODEX_APP_SERVER_URL;
47
74
  const runId = process.env.AGENT_RELAY_CODEX_RUN_ID;
48
75
  const cwd = input.cwd || process.cwd();
49
- const threadId = input.session_id || "";
76
+ const threadId = pickThreadId(input);
50
77
  const relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
51
78
  const rig = process.env.CODEX_LIVE_RIG || "codex-live";
52
79
 
53
- if (!appServerUrl || !runId || !threadId) {
80
+ if (!appServerUrl || !runId) {
54
81
  outputContext(
55
82
  "Agent Relay for Codex is installed. For live incoming relay messages, start Codex with `agent-relay-codex start` so a managed app-server and sidecar can attach to this session.",
56
83
  );
57
84
  }
58
85
 
59
86
  const runtimeDir = process.env.AGENT_RELAY_CODEX_RUNTIME_DIR || join(process.env.HOME || ".", ".agent-relay", "codex", "runtime", runId);
60
- const sessionDir = join(runtimeDir, sanitize(threadId));
87
+ const sessionKey = sanitize(threadId || "auto");
88
+ const sessionDir = join(runtimeDir, sessionKey);
61
89
  const pidPath = join(sessionDir, "sidecar.pid");
62
90
  const statePath = join(sessionDir, "live-state.json");
63
91
  const logPath = join(sessionDir, "sidecar.log");
64
92
  mkdirSync(sessionDir, { recursive: true });
65
93
 
66
- if (existsSync(pidPath)) {
67
- const existingPid = Number(readFileSync(pidPath, "utf8").trim());
68
- if (Number.isFinite(existingPid) && isAlive(existingPid)) {
94
+ const autoPidPath = join(runtimeDir, "auto", "sidecar.pid");
95
+ const activePid = existingAlivePid(pidPath) ?? (threadId ? existingAlivePid(autoPidPath) : null);
96
+ if (activePid !== null) {
97
+ if (threadId) {
69
98
  const identity = buildAgentIdentity({
70
99
  relayUrl,
71
100
  cwd,
@@ -78,20 +107,27 @@ if (existsSync(pidPath)) {
78
107
  });
79
108
  outputContext(`Agent Relay active for this Codex session. Agent ID: ${identity.id}. Relay URL: ${relayUrl}.`);
80
109
  }
110
+ outputContext(`Agent Relay sidecar already running (pid ${activePid}). Relay URL: ${relayUrl}.`);
111
+ }
112
+
113
+ const spawnEnv: Record<string, string | undefined> = {
114
+ ...process.env,
115
+ AGENT_RELAY_URL: relayUrl,
116
+ CODEX_APP_SERVER_URL: appServerUrl,
117
+ CODEX_THREAD_MODE: threadId ? "resume" : "auto",
118
+ CODEX_LIVE_CWD: cwd,
119
+ CODEX_LIVE_STATE_PATH: statePath,
120
+ CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
121
+ };
122
+ if (threadId) {
123
+ spawnEnv.CODEX_THREAD_ID = threadId;
124
+ } else {
125
+ delete spawnEnv.CODEX_THREAD_ID;
81
126
  }
82
127
 
83
128
  const logFile = Bun.file(logPath);
84
129
  const sidecar = Bun.spawn(["bun", "run", join(packageRoot, "codex", "live-sidecar.ts")], {
85
- env: {
86
- ...process.env,
87
- AGENT_RELAY_URL: relayUrl,
88
- CODEX_APP_SERVER_URL: appServerUrl,
89
- CODEX_THREAD_ID: threadId,
90
- CODEX_THREAD_MODE: "resume",
91
- CODEX_LIVE_CWD: cwd,
92
- CODEX_LIVE_STATE_PATH: statePath,
93
- CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
94
- },
130
+ env: spawnEnv,
95
131
  stdout: logFile,
96
132
  stderr: logFile,
97
133
  });
@@ -100,6 +136,10 @@ sidecar.unref();
100
136
  writeFileSync(pidPath, String(sidecar.pid));
101
137
  appendFileSync(join(runtimeDir, "sidecar-pids.txt"), `${sidecar.pid}\n`);
102
138
 
139
+ if (!threadId) {
140
+ outputContext(`Agent Relay sidecar started in auto-thread mode. Relay URL: ${relayUrl}. Incoming messages will arrive as live user turns once the sidecar resolves the active thread.`);
141
+ }
142
+
103
143
  const identity = buildAgentIdentity({
104
144
  relayUrl,
105
145
  cwd,
@@ -9,9 +9,9 @@ usage() {
9
9
  Install Agent Relay for Codex.
10
10
 
11
11
  Usage:
12
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash
13
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash -s -- --alias
14
- curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash -s -- --no-alias
12
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash
13
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash -s -- --alias
14
+ curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash -s -- --no-alias
15
15
 
16
16
  Options:
17
17
  --alias Install a PATH shim so plain `codex` starts with Agent Relay.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -8,9 +8,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
9
9
  export const VERSION: string = pkg.version;
10
10
 
11
- export const STALE_TTL_MS = 600_000; // 10min without heartbeat → offline
12
- export const REAP_INTERVAL_MS = 60_000; // reaper cadence
13
11
  export const DAY_MS = 86_400_000;
12
+ function envPositiveInt(name: string, fallback: number): number {
13
+ const raw = process.env[name];
14
+ if (!raw) return fallback;
15
+ const parsed = Number(raw);
16
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
17
+ return Math.floor(parsed);
18
+ }
19
+
20
+ export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 600_000); // 10min without heartbeat → offline
21
+ export const OFFLINE_PRUNE_MS = envPositiveInt("OFFLINE_PRUNE_MS", DAY_MS); // 24h offline → delete
22
+ export const REAP_INTERVAL_MS = envPositiveInt("REAP_INTERVAL_MS", 60_000); // reaper cadence
14
23
 
15
24
  // Max body size for any POST/PATCH request (64 KiB).
16
25
  export const MAX_BODY_BYTES = 64 * 1024;
package/src/db.ts CHANGED
@@ -289,6 +289,33 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
289
289
  return rows.map((r: any) => r.id);
290
290
  }
291
291
 
292
+ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
293
+ const cutoff = Date.now() - maxOfflineMs;
294
+ return db.transaction(() => {
295
+ const rows = db
296
+ .prepare(
297
+ "SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
298
+ )
299
+ .all(cutoff) as any[];
300
+ if (rows.length === 0) return [];
301
+
302
+ // Release claims held by pruned agents so work becomes claimable again.
303
+ db
304
+ .prepare(
305
+ "UPDATE messages SET claimed_by = NULL, claimed_at = NULL WHERE claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system'))"
306
+ )
307
+ .run(cutoff);
308
+
309
+ db
310
+ .prepare(
311
+ "DELETE FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
312
+ )
313
+ .run(cutoff);
314
+
315
+ return rows.map((r: any) => r.id);
316
+ })();
317
+ }
318
+
292
319
  export function findAgentsByCapability(capability: string, onlineOnly = true): AgentCard[] {
293
320
  let sql = `SELECT * FROM agents WHERE EXISTS (SELECT 1 FROM json_each(capabilities) WHERE value = ?)`;
294
321
  const params: any[] = [capability];
package/src/index.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  #!/usr/bin/env bun
2
- import { initDb, reapStaleAgents, pruneOldMessages } from "./db";
2
+ import { initDb, reapStaleAgents, pruneOfflineAgents, pruneOldMessages } from "./db";
3
3
  import { matchRoute } from "./routes";
4
- import { emitAgentStatus } from "./sse";
4
+ import { emitAgentStatus, emitAgentRemoved } from "./sse";
5
5
  import { resolve, sep } from "path";
6
- import { REAP_INTERVAL_MS, STALE_TTL_MS, MAX_BODY_BYTES, DAY_MS, VERSION } from "./config";
6
+ import {
7
+ REAP_INTERVAL_MS,
8
+ STALE_TTL_MS,
9
+ OFFLINE_PRUNE_MS,
10
+ MAX_BODY_BYTES,
11
+ DAY_MS,
12
+ VERSION,
13
+ } from "./config";
7
14
 
8
15
  const PORT = Number(process.env.PORT) || 4850;
9
16
  const HOST = process.env.HOST || "127.0.0.1";
@@ -19,6 +26,11 @@ setInterval(() => {
19
26
  console.log(`reaped ${reaped.length} stale agent(s)`);
20
27
  for (const id of reaped) emitAgentStatus(id);
21
28
  }
29
+ const pruned = pruneOfflineAgents(OFFLINE_PRUNE_MS);
30
+ if (pruned.length > 0) {
31
+ console.log(`pruned ${pruned.length} offline agent(s)`);
32
+ for (const id of pruned) emitAgentRemoved(id);
33
+ }
22
34
  }, REAP_INTERVAL_MS);
23
35
 
24
36
  // Daily message prune