Envoy: The Coordination Server AI Coding Agents Were Missing
I run multiple AI coding agents in parallel. Claude Code sessions, Hermes agents, subagents spawning subagents. After a while I noticed something: none of them know the others exist. They overwrite each other’s files, repeat discoveries, and have no memory of what happened yesterday. There is no infrastructure for this. So I built one.
Envoy is an HTTP+JSON coordination server for AI coding agents. It provides agent identity, structured messaging, session accountability, and knowledge persistence – all backed by SQLite, no Postgres, no Redis, no Node.js.
What’s missing
Every major AI coding tool (Claude Code, Cursor, Copilot) treats each session as isolated:
| Problem | Consequence |
|---|---|
| No persistent identity | Agents can’t address each other (“tell agent X to stop editing file Y”) |
| No cross-session memory | Every session re-discovers the same bugs, re-reads the same files |
| No audit trail | You can’t answer “who changed this file and why?” |
| No subagent accountability | Subagents fail silently; parents don’t know what happened |
| No cross-project search | Working on 3 repos means running 3 separate queries |
Envoy fills all of these. Whether that’s a good idea depends on whether you actually run multiple agents – if you don’t, this is overkill.
How it works
Everything is SQLite-backed. The stack is:
envoy (HTTP server, this project)
└── atheneum (embedded knowledge graph)
└── sqlitegraph (SQLite graph engine with pub/sub)
Start the server:
envoy serve --port 9876
Or as a systemd user service:
systemctl --user start envoy
The server has been running on my machine for 42+ hours straight with no restarts. Health check:
$ curl http://127.0.0.1:9876/health
{"status":"ok","uptime_seconds":152986,"agents_online":2}
Agent identity
Agents register at session start. The server assigns hierarchical IDs:
$ curl -X POST http://127.0.0.1:9876/agents \
-H "content-type: application/json" \
-d '{"name":"claude-main","kind":"claude"}'
{"agent_id":"id1","name":"claude-main","is_new":true,...}
Subagents get dotted IDs that encode the hierarchy:
$ curl -X POST http://127.0.0.1:9876/agents \
-H "content-type: application/json" \
-d '{"name":"sub-agent-1","kind":"claude","parent_id":"id1"}'
{"agent_id":"id1.1","name":"sub-agent-1","parent_id":"id1",...}
Retiring an agent cascades to its children:
$ curl -X POST http://127.0.0.1:9876/agents/id1/retire \
-H "X-Agent-Id: id1" \
-H "content-type: application/json" \
-d '{"agent_id":"id1"}'
{"affected":["id1","id1.1"],"retired":true}
Session accountability
Every session writes structured data through envoy-hook (a companion binary that plugs into Claude Code’s hook system). The lifecycle is:
SessionStart → POST /atheneum/sessions
PostToolUse → POST /atheneum/tool-calls
SubagentStop → POST /atheneum/sessions/{id}/handover
Stop → PATCH /atheneum/sessions/{id}
Query prior sessions before starting work:
$ curl -s "http://127.0.0.1:9876/atheneum/sessions?project=envoy&last=1"
[{
"session_id": "...",
"project": "envoy",
"git_branch": "main",
"tool_call_count": 47,
"file_write_count": 12,
"last_tool": "cargo test",
"last_tool_summary": "all 34 tests passed"
}]
Tool call logging requires session_id, tool_name, and exit_status (the fields that tripped me up during testing – the API is precise about what it expects):
$ curl -X POST http://127.0.0.1:9876/atheneum/tool-calls \
-H "X-Agent-Id: id1" \
-H "content-type: application/json" \
-d '{"session_id":"...","tool_name":"read_file",
"exit_status":"success","input_summary":"read src/main.rs",
"output_summary":"42 lines","latency_ms":150}'
Messaging between agents
Agents send messages to each other. This is the core coordination primitive:
$ curl -X POST http://127.0.0.1:9876/messages \
-H "X-Agent-Id: id1" \
-H "content-type: application/json" \
-d '{"type":"direct","from":"id1","to":"id2",
"parts":[{"text":"hey, the build is green"}]}'
{"message_id":"6751","from":"id1","to":"id2",...}
The recipient polls for pending messages:
$ curl -s "http://127.0.0.1:9876/agents/id2/messages/pending"
{
"count": 1,
"messages": [{
"message_id": "6751",
"from": "id1",
"parts": [{"text": "hey, the build is green"}]
}]
}
And acknowledges receipt:
$ curl -X POST http://127.0.0.1:9876/messages/6751/ack \
-H "X-Agent-Id: id2" \
-H "content-type: application/json" \
-d '{"agent_id":"id2"}'
{"acked_by":["id2"],"message_id":"6751"}
The polling problem. This is the biggest pain point. The MCP (Model Context Protocol) interface that coding agents use is request-response: the agent asks a question, the server answers. There is no push mechanism. When agent A sends agent B a message, agent B only finds out the next time it explicitly polls pending. In practice, agents need to check periodically, which means either:
- Wasting tokens on poll loops (“any messages for me? no? ok”)
- Adding latency – a message sits undelivered until the next poll
The WebSocket endpoint exists (/ws) but coding agents don’t speak WebSocket natively. They speak HTTP. Until MCP adds a push/subscription mechanism, polling is the only option. This is a protocol limitation, not an implementation choice.
Knowledge persistence
Agents store discoveries so future sessions don’t re-derive them:
$ curl -X POST http://127.0.0.1:9876/atheneum/discoveries \
-H "X-Agent-Id: id1" \
-H "content-type: application/json" \
-d '{"agent":"claude","discovery_type":"Bug",
"target":"query_sessions",
"metadata":{"file":"evidence.rs","line":547,
"why":"anonymous ? params required"}}'
{"discovery_id":7502,...}
Cross-project code search
This one I use daily. When you work on multiple codebases simultaneously, you need to find symbols across all of them. Envoy queries all magellan-indexed projects from one endpoint without copying data:
# One-time setup per project
atheneum meta-register envoy ~/Projects/envoy \
~/.magellan/envoy/envoy.db --language rust
# Search across all registered projects
$ curl "http://127.0.0.1:9876/atheneum/cross/search?q=build_router&language=rust&k=5"
{
"count": 5,
"results": [
{"project":"envoy","name":"build_router","kind":"Function",
"file":"src/http/router.rs","line":81},
{"project":"envoy","name":"build_router calls build_base_routes",
"kind":"Call","file":"src/http/router.rs","line":82},
...
]
}
How it works: envoy delegates to atheneum’s CrossRouter, which lazily ATTACH DATABASE each project’s magellan DB (read-only) and queries across schemas. An LRU cache keeps hot DBs attached across requests. SQLite limits this to ~10 attached databases, so the cache defaults to 8.
The deeper navigate endpoint (/atheneum/cross/navigate) that does BFS graph walks across projects currently errors on the cross-schema edge queries. That’s a known bug – the UNION ALL over attached schemas doesn’t find the edges table. Search works, graph navigation doesn’t yet.
The knowledge graph underneath
Envoy sits on top of atheneum, which stores everything as a property graph. Real numbers from my running instance:
Entity counts (4,747 total):
ToolCall: 2,399 Session: 231 File: 203
Reference: 338 WikiPage: 280 Import: 198
ReasoningLog: 329 Symbol: 190 Memory: 130
TestRun: 120 Discovery: 3 Event: 3
Edge counts (15,210 total):
belongs_to_project: 4,184 accessed: 635
observed_in: 3,435 modified: 393
wikilink: 3,220 CALLS: 116
handled_by_tool: 2,399 IMPORTS: 84
performed_by: 233 REFERENCES: 145
This is what makes cross-session memory possible. When a new session starts, it queries the graph for prior context instead of re-discovering everything.
What’s rough
Honest assessment of what doesn’t work well:
- The MCP polling problem described above. No push mechanism, no subscriptions, no server-sent events. Agents waste tokens polling or accept delivery latency.
- The
/atheneum/cross/navigateendpoint errors on cross-schema edge queries. Symbol search works, graph walks don’t. - API discoverability is poor. Several endpoints have required fields that aren’t documented anywhere except the Rust source. I found
agentis required on session creation,tool_nameinstead oftoolon tool-calls,agent_idon ack – all through 422 errors. - The events endpoint returns an empty body on success (no confirmation JSON), which makes it hard to verify it worked.
- Token savings counter in the knowledge endpoint always returns 0. Never got around to implementing the calculation.
- v0.1.1 – 127 commits, 11.5K LOC, but still early. No backward compatibility guarantees yet.
The post-mortem that shaped it
During development, a private git dependency broke CI for 8 consecutive runs. The dependency was specified as a git = "..." URL in Cargo.toml. It resolved fine locally (cached) but failed on every CI runner (fresh clone, no cache, no SSH key for the private repo). The error was misleading – cargo reported “revival failed” which looked like a registry issue, not an access issue.
That incident directly led to three envoy features:
- Session accountability – if CI had logged what it actually did vs. what it claimed, the SSH key issue would have been obvious in 1 run instead of 8
- Structured tool call logging – the difference between “cargo check failed” and “cargo check failed because SSH key was missing for git+https://…” is the difference between 1 hour and 6 hours of debugging
- The subagent trust model – subagents are not trusted by default. Their output is only valid when all verification gates pass (magellan queries ran, cargo check green, no stubs). If a subagent’s hooks blocked it, its summary is discarded as unreliable
Current state
Version: 0.1.1
LOC: 11,562 (Rust)
Tests: 5,223 lines
Commits: 127
Endpoints: 20+ (agents, sessions, messages, tool-calls, events,
discoveries, graph, cross-project search, health,
circuit breakers)
Runtime: SQLite (no external services)
License: GPL-3.0-only
Install:
cargo install agent-envoy
Or as part of the grounded-coding stack (also installs magellan, llmgrep, mirage, splice):
curl -fsSL https://raw.githubusercontent.com/oldnordic/grounded-coding/master/install.sh | sh
Source: github.com/oldnordic/envoy