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 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
- - attaches to a loaded thread for the current `cwd` when one exists
16
- - otherwise resumes the newest thread for the current `cwd`
17
- - otherwise creates a new thread
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=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.
135
- - For isolated testing, set `CODEX_THREAD_MODE=start` so the sidecar always creates its own thread.
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
+ });
@@ -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 = process.env[name];
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 loadConfig(): Config {
583
- const cwd = process.env.CODEX_LIVE_CWD || process.cwd();
584
- const capabilities = (process.env.AGENT_RELAY_CAPS || "chat")
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: process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
591
- appServerUrl: process.env.CODEX_APP_SERVER_URL || "ws://127.0.0.1:4501",
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: process.env.CODEX_LIVE_RIG || "codex-live",
598
+ rig: env.CODEX_LIVE_RIG || "codex-live",
594
599
  capabilities,
595
- tags: ["codex", process.env.CODEX_LIVE_RIG || "codex-live", cwd.split("/").filter(Boolean).at(-1) || "unknown"],
596
- statePath: process.env.CODEX_LIVE_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
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: (process.env.CODEX_THREAD_MODE as Config["threadMode"]) || "auto",
603
- threadId: process.env.CODEX_THREAD_ID || undefined,
604
- model: process.env.CODEX_MODEL || undefined,
605
- approvalPolicy: process.env.CODEX_LIVE_APPROVAL_POLICY || undefined,
606
- sandbox: process.env.CODEX_LIVE_SANDBOX || undefined,
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
- main().catch((error) => {
617
- console.error(error instanceof Error ? error.stack || error.message : String(error));
618
- process.exit(1);
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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",