agent-relay-server 0.4.8 → 0.4.10

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
@@ -223,26 +343,6 @@ Example incoming relay message:
223
343
  | Label | `"label:test writer"` | All agents with that human-set label |
224
344
  | Broadcast | `"broadcast"` | Everyone |
225
345
 
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
346
  ## Integrations And Tasks
247
347
 
248
348
  Agent Relay can act as a secure ingress layer for scripts, monitoring systems,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
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.8",
3
+ "version": "0.4.10",
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/index.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  REAP_INTERVAL_MS,
8
8
  STALE_TTL_MS,
9
9
  OFFLINE_PRUNE_MS,
10
- MAX_BODY_BYTES,
11
10
  DAY_MS,
12
11
  VERSION,
13
12
  } from "./config";
@@ -57,68 +56,66 @@ function startServer(): void {
57
56
  if (pruned > 0) console.log(`pruned ${pruned} old message(s)`);
58
57
  }, DAY_MS);
59
58
 
60
- const publicDir = resolve(import.meta.dir, "../public");
61
- const publicDirPrefix = publicDir + sep;
62
-
63
59
  Bun.serve({
64
60
  port: PORT,
65
61
  hostname: HOST,
66
- async fetch(req) {
67
- const url = new URL(req.url);
62
+ fetch: createFetchHandler({ logRequests: LOG_REQUESTS }),
63
+ });
68
64
 
69
- if (req.method === "OPTIONS") {
70
- return corsPreflight(req);
71
- }
72
- if (!isOriginAllowed(req)) {
73
- return Response.json({ error: "origin not allowed" }, { status: 403 });
74
- }
65
+ console.log(`agent-relay ${VERSION} running on http://${HOST}:${PORT}`);
66
+ }
75
67
 
76
- // Body size guard for write methods
77
- if (req.method === "POST" || req.method === "PATCH" || req.method === "PUT") {
78
- const len = Number(req.headers.get("content-length") ?? 0);
79
- if (len > MAX_BODY_BYTES) {
80
- return Response.json(
81
- { error: `request body exceeds ${MAX_BODY_BYTES} bytes` },
82
- { status: 413 },
83
- );
84
- }
85
- }
68
+ export function createFetchHandler(
69
+ opts: { publicDir?: string; logRequests?: boolean } = {},
70
+ ): (req: Request) => Promise<Response> {
71
+ const publicDir = opts.publicDir ?? resolve(import.meta.dir, "../public");
72
+ const publicDirPrefix = publicDir + sep;
73
+ const logRequests = opts.logRequests ?? false;
86
74
 
87
- // API routes
88
- const matched = matchRoute(req.method, url.pathname);
89
- if (matched) {
90
- const integrationAuth = getIntegrationAuth(req);
91
- if (!isAuthorized(req)) {
92
- if (!integrationAuth || !url.pathname.startsWith("/api/integrations/")) {
93
- return unauthorized(req);
94
- }
95
- }
96
- const response = await matched.handler(req, matched.params);
97
- applyCors(req, response);
98
- if (LOG_REQUESTS && url.pathname.startsWith("/api/")) {
99
- console.log(`${req.method} ${url.pathname} → ${response.status}`);
75
+ return async function fetch(req: Request): Promise<Response> {
76
+ const url = new URL(req.url);
77
+
78
+ if (req.method === "OPTIONS") {
79
+ return corsPreflight(req);
80
+ }
81
+ if (!isOriginAllowed(req)) {
82
+ return Response.json({ error: "origin not allowed" }, { status: 403 });
83
+ }
84
+
85
+ // API routes
86
+ const matched = matchRoute(req.method, url.pathname);
87
+ if (matched) {
88
+ const integrationAuth = getIntegrationAuth(req);
89
+ if (!isAuthorized(req)) {
90
+ if (!integrationAuth || !url.pathname.startsWith("/api/integrations/")) {
91
+ return unauthorized(req);
100
92
  }
101
- return response;
102
93
  }
103
-
104
- // Dashboard — serve static files, rejecting path traversal and directory requests
105
- let requested = url.pathname === "/" ? "/index.html" : url.pathname;
106
- if (requested.endsWith("/")) requested += "index.html";
107
- const resolved = resolve(publicDir, `.${requested}`);
108
- if (!resolved.startsWith(publicDirPrefix)) {
109
- return Response.json({ error: "not found" }, { status: 404 });
94
+ const response = await matched.handler(req, matched.params);
95
+ applyCors(req, response);
96
+ if (logRequests && url.pathname.startsWith("/api/")) {
97
+ console.log(`${req.method} ${url.pathname} ${response.status}`);
110
98
  }
111
- const file = Bun.file(resolved);
112
- if (await file.exists()) return new Response(file);
99
+ return response;
100
+ }
113
101
 
102
+ // Dashboard — serve static files, rejecting path traversal and directory requests
103
+ let requested = url.pathname === "/" ? "/index.html" : url.pathname;
104
+ if (requested.endsWith("/")) requested += "index.html";
105
+ const resolved = resolve(publicDir, `.${requested}`);
106
+ if (!resolved.startsWith(publicDirPrefix)) {
114
107
  return Response.json({ error: "not found" }, { status: 404 });
115
- },
116
- });
108
+ }
109
+ const file = Bun.file(resolved);
110
+ if (await file.exists()) return new Response(file);
117
111
 
118
- console.log(`agent-relay ${VERSION} running on http://${HOST}:${PORT}`);
112
+ return Response.json({ error: "not found" }, { status: 404 });
113
+ };
119
114
  }
120
115
 
121
- main().catch((error) => {
122
- console.error(error instanceof Error ? error.message : String(error));
123
- process.exit(1);
124
- });
116
+ if (import.meta.main) {
117
+ main().catch((error) => {
118
+ console.error(error instanceof Error ? error.message : String(error));
119
+ process.exit(1);
120
+ });
121
+ }