agent-relay-server 0.4.9 → 0.4.11
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 +123 -20
- package/codex/README.md +5 -5
- package/codex/live-sidecar.test.ts +20 -0
- package/codex/live-sidecar.ts +31 -24
- package/codex/plugin/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,126 @@ You're running three Claude Code sessions: one debugging a backend, another writ
|
|
|
23
23
|
- **Closed-loop tasks**: external systems can create deduped work items agents
|
|
24
24
|
claim, progress, resolve, and report back through callbacks
|
|
25
25
|
|
|
26
|
+
## Mental Model
|
|
27
|
+
|
|
28
|
+
Agent Relay is a small trusted-network message bus. Agents register themselves,
|
|
29
|
+
the relay stores their presence and messages in SQLite, and clients route work
|
|
30
|
+
by id, label, tag, capability, channel, or claimable task.
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart LR
|
|
34
|
+
subgraph Agents["Claude / Codex Sessions"]
|
|
35
|
+
A1["Agent registers"]
|
|
36
|
+
A2["Polls / SSE / sidecar"]
|
|
37
|
+
A3["Replies"]
|
|
38
|
+
A4["Claims work"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subgraph Relay["Agent Relay"]
|
|
42
|
+
R1["Agents table"]
|
|
43
|
+
R2["Messages"]
|
|
44
|
+
R3["Threads"]
|
|
45
|
+
R4["Tasks"]
|
|
46
|
+
R5["Events & callbacks"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph Tools["External Tools"]
|
|
50
|
+
T1["Scripts"]
|
|
51
|
+
T2["CI"]
|
|
52
|
+
T3["Monitoring"]
|
|
53
|
+
T4["Support desks"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
A1 -- "POST /api/agents" --> R1
|
|
57
|
+
A2 <-- "notifications / polling" --> R2
|
|
58
|
+
A3 -- "POST /api/messages" --> R3
|
|
59
|
+
A4 -- "POST claim" --> R4
|
|
60
|
+
T1 -- "integration event" --> R4
|
|
61
|
+
T2 -- "integration event" --> R4
|
|
62
|
+
T3 -- "alerts" --> R4
|
|
63
|
+
T4 -- "tickets" --> R4
|
|
64
|
+
R4 -- "task lifecycle" --> R5
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The relay does not decide what an agent should do. It gives agents a shared
|
|
68
|
+
address book, inbox, task queue, and dashboard.
|
|
69
|
+
|
|
70
|
+
### Agent Identity
|
|
71
|
+
|
|
72
|
+
Every running Claude or Codex session registers as an agent. The generated agent
|
|
73
|
+
id is the stable address for that session:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
macmini2-codex-live-agent-relay-019f4c2a
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Treat ids as machine-friendly session addresses. For human workflows, use
|
|
80
|
+
labels, tags, and capabilities:
|
|
81
|
+
|
|
82
|
+
| Field | Example | Use it for | Target syntax |
|
|
83
|
+
|-------|---------|------------|---------------|
|
|
84
|
+
| `id` | `macmini2-codex-live-agent-relay-019f4c2a` | One exact running session | `macmini2-codex-live-agent-relay-019f4c2a` |
|
|
85
|
+
| `label` | `backend fixer` | A human-friendly name you can change from the dashboard | `label:backend fixer` |
|
|
86
|
+
| `tag` | `backend`, `macmini2`, `release` | Grouping sessions by project, machine, role, or temporary work | `tag:backend` |
|
|
87
|
+
| `capability` | `review`, `ops`, `support` | Routing work to agents that can do a kind of job | `cap:review` |
|
|
88
|
+
| `channel` | `alerts`, `support`, `deploy` | Scoping message/task streams so unrelated agents ignore noise | `channel=alerts` |
|
|
89
|
+
|
|
90
|
+
Labels are for people. Tags are for grouping. Capabilities are for routing work.
|
|
91
|
+
When in doubt, route tasks by capability and use tags/channels to narrow the
|
|
92
|
+
audience.
|
|
93
|
+
|
|
94
|
+
### Routing Examples
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "to": "macmini2-codex-live-agent-relay-019f4c2a", "body": "Can you check this branch?" }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Direct messages go to one exact session.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "to": "tag:backend", "body": "Who owns the migration failure?" }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tags fan out to every matching agent.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "to": "cap:review", "claimable": true, "body": "Review PR #42" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Claimable capability-routed messages act like a tiny work queue: many agents may
|
|
113
|
+
see the work, but one agent claims it before acting.
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"target": "cap:ops",
|
|
118
|
+
"channel": "alerts",
|
|
119
|
+
"dedupeKey": "prod-api:5xx-rate",
|
|
120
|
+
"title": "prod-api 5xx rate high"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Integration events create durable tasks. The dedupe key prevents alert storms
|
|
125
|
+
from spamming agents with duplicate work.
|
|
126
|
+
|
|
127
|
+
### Message And Task Lifecycle
|
|
128
|
+
|
|
129
|
+
Messages and tasks are persisted in SQLite, so server restarts do not erase the
|
|
130
|
+
inbox. Agents reconnect, heartbeat, and resume polling from the latest known
|
|
131
|
+
message cursor.
|
|
132
|
+
|
|
133
|
+
| Concept | What it means |
|
|
134
|
+
|---------|---------------|
|
|
135
|
+
| Claimable task | A message or integration task that exactly one agent should own. Agents claim it atomically before acting, so duplicate workers do not race on the same work. |
|
|
136
|
+
| Read state | Read markers are stored per agent. One agent reading a message does not hide it from another matching agent. |
|
|
137
|
+
| Threading | Replies set `replyTo`; the relay keeps the thread chain so the dashboard/API can show the conversation instead of isolated messages. |
|
|
138
|
+
| Restart | The server reloads agents, messages, tasks, events, callbacks, and read markers from SQLite. Connected clients reconnect through polling/SSE/sidecars. |
|
|
139
|
+
| Offline agents | Agents that stop heartbeating become `offline` after `STALE_TTL_MS`. Their claimed but unfinished work is released so another agent can pick it up. |
|
|
140
|
+
| Pruning | Old messages are removed after `RETENTION_DAYS`. Long-offline agents are removed after `OFFLINE_PRUNE_MS`. Built-in/system agents are kept. |
|
|
141
|
+
|
|
142
|
+
For task-like work, prefer `claimable: true` or integration tasks over plain
|
|
143
|
+
broadcasts. For long-running work, update task status/progress so humans and
|
|
144
|
+
tools can see whether it is claimed, blocked, done, failed, or canceled.
|
|
145
|
+
|
|
26
146
|
## Quick Start
|
|
27
147
|
|
|
28
148
|
### 1. Start the relay server
|
|
@@ -104,6 +224,9 @@ time, it starts a new thread by default to avoid surprising cwd-based attachment
|
|
|
104
224
|
to an unrelated loaded session. Use `codex-relay --thread-mode auto` or
|
|
105
225
|
`AGENT_RELAY_CODEX_FALLBACK_THREAD_MODE=auto` when you deliberately want the
|
|
106
226
|
fallback sidecar to attach to the newest loaded thread for the current cwd.
|
|
227
|
+
The lower-level sidecar also defaults to `CODEX_THREAD_MODE=start`; `auto` is an
|
|
228
|
+
explicit opt-in because it may deliver relay messages into an already-open Codex
|
|
229
|
+
session for the same directory.
|
|
107
230
|
|
|
108
231
|
### Codex approval mode
|
|
109
232
|
|
|
@@ -223,26 +346,6 @@ Example incoming relay message:
|
|
|
223
346
|
| Label | `"label:test writer"` | All agents with that human-set label |
|
|
224
347
|
| Broadcast | `"broadcast"` | Everyone |
|
|
225
348
|
|
|
226
|
-
## Mental Model
|
|
227
|
-
|
|
228
|
-
Agent Relay is a small trusted-network message bus:
|
|
229
|
-
|
|
230
|
-
- **Server**: one Bun process with SQLite state and an HTTP API.
|
|
231
|
-
- **Agent registration**: each Claude/Codex session registers an agent card with
|
|
232
|
-
id, labels, tags, capabilities, status, and readiness.
|
|
233
|
-
- **Delivery**: Claude receives monitor notifications; Codex receives live turns
|
|
234
|
-
through the app-server sidecar.
|
|
235
|
-
- **Routing**: direct ids target one session; tags/capabilities/labels fan out;
|
|
236
|
-
claimable messages are tasks where one matching agent claims before acting.
|
|
237
|
-
- **Threads**: replies set `replyTo`; the server keeps a thread id so the
|
|
238
|
-
dashboard/API can show the conversation chain.
|
|
239
|
-
- **Read state**: read markers are per agent. Deleting a message removes it from
|
|
240
|
-
history, so prefer retention settings over manual deletion for cleanup.
|
|
241
|
-
- **Tasks**: integrations create durable work items. New open tasks also create
|
|
242
|
-
claimable system messages so one agent can pick up the work.
|
|
243
|
-
- **Callbacks**: integrations can receive task lifecycle webhooks for closed-loop
|
|
244
|
-
automation.
|
|
245
|
-
|
|
246
349
|
## Integrations And Tasks
|
|
247
350
|
|
|
248
351
|
Agent Relay can act as a secure ingress layer for scripts, monitoring systems,
|
package/codex/README.md
CHANGED
|
@@ -12,9 +12,9 @@ This sidecar connects to a Codex app-server session and to Agent Relay, then del
|
|
|
12
12
|
|
|
13
13
|
## Current behavior
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
15
|
+
- starts a new thread by default
|
|
16
|
+
- resumes the actual launched thread when the SessionStart hook provides a thread id
|
|
17
|
+
- only attaches to loaded/latest same-cwd threads when `CODEX_THREAD_MODE=auto`
|
|
18
18
|
- registers a relay agent with `client: codex-live`
|
|
19
19
|
- marks the relay agent `ready=true` once app-server + thread are attached
|
|
20
20
|
- polls relay inbox and delivers messages into the live thread
|
|
@@ -131,6 +131,6 @@ startup in time.
|
|
|
131
131
|
|
|
132
132
|
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.
|
|
133
133
|
|
|
134
|
-
- `CODEX_THREAD_MODE=
|
|
135
|
-
-
|
|
134
|
+
- `CODEX_THREAD_MODE=start` is the safe default: the sidecar creates its own thread unless the hook supplied an explicit thread id.
|
|
135
|
+
- `CODEX_THREAD_MODE=auto` will attach to an already loaded thread for the same `cwd`. That can be useful for advanced live control, but it also means relay messages can enter your current interactive Codex session if one is already open.
|
|
136
136
|
- A brand-new thread is not materialized for `includeTurns` reads until the first turn starts. That is an app-server behavior, not a relay bug.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { loadConfig, parseThreadMode } from "./live-sidecar";
|
|
3
|
+
|
|
4
|
+
describe("codex live sidecar config", () => {
|
|
5
|
+
it("defaults to starting an isolated thread", () => {
|
|
6
|
+
const config = loadConfig({
|
|
7
|
+
CODEX_LIVE_CWD: "/tmp/agent-relay-test",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
expect(config.threadMode).toBe("start");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("only accepts known thread attachment modes", () => {
|
|
14
|
+
expect(parseThreadMode("auto")).toBe("auto");
|
|
15
|
+
expect(parseThreadMode("resume")).toBe("resume");
|
|
16
|
+
expect(parseThreadMode("start")).toBe("start");
|
|
17
|
+
expect(parseThreadMode("latest")).toBe("start");
|
|
18
|
+
expect(parseThreadMode(undefined)).toBe("start");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/codex/live-sidecar.ts
CHANGED
|
@@ -572,38 +572,43 @@ function describeError(error: unknown): string {
|
|
|
572
572
|
return error instanceof Error ? error.message : String(error);
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
function envNumber(name: string, fallback: number): number {
|
|
576
|
-
const raw =
|
|
575
|
+
function envNumber(env: NodeJS.ProcessEnv, name: string, fallback: number): number {
|
|
576
|
+
const raw = env[name];
|
|
577
577
|
if (!raw) return fallback;
|
|
578
578
|
const parsed = Number(raw);
|
|
579
579
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
export function parseThreadMode(raw: string | undefined): Config["threadMode"] {
|
|
583
|
+
if (raw === "auto" || raw === "resume" || raw === "start") return raw;
|
|
584
|
+
return "start";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
|
|
588
|
+
const cwd = env.CODEX_LIVE_CWD || process.cwd();
|
|
589
|
+
const capabilities = (env.AGENT_RELAY_CAPS || "chat")
|
|
585
590
|
.split(",")
|
|
586
591
|
.map((value) => value.trim())
|
|
587
592
|
.filter(Boolean);
|
|
588
593
|
|
|
589
594
|
return {
|
|
590
|
-
relayUrl:
|
|
591
|
-
appServerUrl:
|
|
595
|
+
relayUrl: env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
|
|
596
|
+
appServerUrl: env.CODEX_APP_SERVER_URL || "ws://127.0.0.1:4501",
|
|
592
597
|
cwd,
|
|
593
|
-
rig:
|
|
598
|
+
rig: env.CODEX_LIVE_RIG || "codex-live",
|
|
594
599
|
capabilities,
|
|
595
|
-
tags: ["codex",
|
|
596
|
-
statePath:
|
|
597
|
-
pollIntervalMs: envNumber("CODEX_LIVE_POLL_INTERVAL_MS", 2000),
|
|
598
|
-
heartbeatIntervalMs: envNumber("CODEX_LIVE_HEARTBEAT_INTERVAL_MS", 30000),
|
|
599
|
-
coalesceWindowMs: envNumber("CODEX_LIVE_COALESCE_WINDOW_MS", 600),
|
|
600
|
-
reconnectInitialDelayMs: envNumber("CODEX_LIVE_RECONNECT_INITIAL_MS", 1000),
|
|
601
|
-
reconnectMaxDelayMs: envNumber("CODEX_LIVE_RECONNECT_MAX_MS", 10000),
|
|
602
|
-
threadMode: (
|
|
603
|
-
threadId:
|
|
604
|
-
model:
|
|
605
|
-
approvalPolicy:
|
|
606
|
-
sandbox:
|
|
600
|
+
tags: ["codex", env.CODEX_LIVE_RIG || "codex-live", cwd.split("/").filter(Boolean).at(-1) || "unknown"],
|
|
601
|
+
statePath: env.CODEX_LIVE_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
|
|
602
|
+
pollIntervalMs: envNumber(env, "CODEX_LIVE_POLL_INTERVAL_MS", 2000),
|
|
603
|
+
heartbeatIntervalMs: envNumber(env, "CODEX_LIVE_HEARTBEAT_INTERVAL_MS", 30000),
|
|
604
|
+
coalesceWindowMs: envNumber(env, "CODEX_LIVE_COALESCE_WINDOW_MS", 600),
|
|
605
|
+
reconnectInitialDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_INITIAL_MS", 1000),
|
|
606
|
+
reconnectMaxDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_MAX_MS", 10000),
|
|
607
|
+
threadMode: parseThreadMode(env.CODEX_THREAD_MODE),
|
|
608
|
+
threadId: env.CODEX_THREAD_ID || undefined,
|
|
609
|
+
model: env.CODEX_MODEL || undefined,
|
|
610
|
+
approvalPolicy: env.CODEX_LIVE_APPROVAL_POLICY || undefined,
|
|
611
|
+
sandbox: env.CODEX_LIVE_SANDBOX || undefined,
|
|
607
612
|
};
|
|
608
613
|
}
|
|
609
614
|
|
|
@@ -613,7 +618,9 @@ async function main(): Promise<void> {
|
|
|
613
618
|
await sidecar.run();
|
|
614
619
|
}
|
|
615
620
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
if (import.meta.main) {
|
|
622
|
+
main().catch((error) => {
|
|
623
|
+
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
});
|
|
626
|
+
}
|