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 +23 -8
- package/codex/README.md +4 -4
- package/codex/hooks/session-start.ts +56 -16
- package/codex/install-codex.sh +3 -3
- package/codex/plugin/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/config.ts +11 -2
- package/src/db.ts +27 -0
- package/src/index.ts +15 -3
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
|
|
55
|
-
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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,
|
package/codex/install-codex.sh
CHANGED
|
@@ -9,9 +9,9 @@ usage() {
|
|
|
9
9
|
Install Agent Relay for Codex.
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
curl -fsSL https://
|
|
13
|
-
curl -fsSL https://
|
|
14
|
-
curl -fsSL https://
|
|
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.
|
package/package.json
CHANGED
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 {
|
|
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
|