Personal AI auxiliary server in Elixir. An always-on coordination layer for agent teams -- persistent inboxes, shared state, and a skill registry over MCP.
Named after aether -- the substance that fills the space between things.
Personal AI auxiliary server in Elixir. An always-on coordination layer for agent teams -- persistent inboxes, shared state, and a skill registry over MCP.
Named after aether -- the substance that fills the space between things.
| description | Kaether direction brainstorm -- layered agent OS with ocap, shared skills, CRDT state, Agentica runtime, and the path from local coordination to Torus-scale swarms |
|---|---|
| project | kaether |
| type | brainstorm |
| created | 2026-04-10 |
| share | close |
Working document. Started 2026-04-10 as a brainstorm, being refined into a shareable thesis.
Malleable software operated by a team of humans and agents, updating code dynamically to provide better tools for all agents. When used for dogfooding, you get a bootstrapping spiral: agents improve the tools they use, which makes them better at improving tools.
For this to work, improvements must propagate. Not siloed per person or per session. A linked knowledge base does this for information (kspace, deti-contexta). A shared skill registry could do it for capabilities.
The question: what's the right substrate for this?
Kaether already provides OS-like primitives:
| OS concept | Kaether equivalent | Status |
|---|---|---|
| Process table | Session management | v0 |
| IPC / message passing | Inboxes | v0 |
| Filesystem | State store (KV) | v0 |
| Shared journals | Memo log (append-only) | v0 |
| Process spawn/supervise | ACP integration | future |
| Skill/program registry | Dynamic MCP tool registration | proposed |
| Access control | OCap via Agentica | proposed |
| Collaborative data structures | CRDT state primitives | proposed |
An OS that observes its own usage and adapts. Agents using kaether to improve kaether. Beer's "requisite variety" -- the controller must match the complexity of what it controls. An agent swarm improving its own tools is literally manufacturing requisite variety.
Layer 4: Torus protocol cross-org, stake-based, blockchain consensus
Layer 3: Team mesh kaether instances networked via Tailscale (p2p)
Layer 2: Skills + know-how procedures linked to live capability objects
Layer 1: Kaether kernel inboxes, CRDT state, skill registry, memos
Layer 0: Agentica runtime sandboxed execution, Warp Protocol, ocap
Each layer up: more participants, less trust, more coordination overhead. You build bottom-up.
The key constraint: you can't coordinate a swarm on a blockchain if you can't coordinate a team on a LAN. The lower layers must exist before the higher ones make sense. Kaether is layers 1-2. Torus is layers 3-4. Starting at layer 4 is why most "decentralized coordination" projects fail.
Agentica's Warp Protocol gives transparent object proxying. Capability objects appear local to the agent but execute remotely. This means kaether's resources aren't "tools called via JSON-RPC" -- they're live objects in the agent's scope:
# not this (MCP tool call)
result = tools.call("crdt/counter", {"name": "builds", "op": "increment"})
# this (capability object via Warp Protocol)
builds = kaether.crdt.counter("builds") # reference IS the permission
builds.increment() # method on a live proxy
builds.value # transparent to kaether backendThe agent holds a reference. The reference IS the permission (ocap). It can pass an attenuated version to a sub-agent (read-only view). The Warp Protocol handles proxying. Kaether handles persistence.
Traditional OS: "user X has role Y, role Y can access resource Z." Fragile for agents -- they're spawned dynamically, delegate to sub-agents, roles don't map cleanly.
Object capabilities: holding a reference IS the permission. You can delegate (pass to another agent) or attenuate (give read-only access to something you have read-write on). This is:
The primitives align across all three systems. That's not coincidence.
Instead of a flat KV store, kaether could expose typed collaborative data structures:
crdt/counter -- increment/decrement, conflict-free
crdt/map -- per-key LWW (last-writer-wins), or per-key CRDT values
crdt/set -- add/remove, observed-remove semantics
crdt/register -- LWW single value
crdt/log -- append-only (what memo/ already is)
No locks, no conflicts, composable by construction. Through Agentica's programmatic interface, these become typed capability objects the agent holds and manipulates directly. The serialization problem disappears.
Why this matters for the team mesh (layer 3): CRDTs are designed to merge across network partitions. Two kaether instances on different machines (connected via Tailscale) can sync state without coordination. The data structures handle conflict resolution by construction. This is what makes "spawn a daemon, connect p2p, edit the same data" actually simple.
Current vault notes are declarative -- "X is true." A new note type, know-how, captures procedural knowledge -- "how to do X." The difference:
---
description: How to process inbox items through the extraction pipeline
type: know-how
requires: [tools/distill.sh, mcp:qmd/query, kaether:crdt/counter("processed")]
---When notes can hyperlink to scripts, MCP tools, and even code symbols (kaether:Inbox.Registry.lookup/2), the knowledge graph becomes partially executable. The agent reads a know-how note, follows links to the capabilities it needs, validates they're available, and acts.
This is the bridge between the knowledge graph and the capability graph. A know-how note documents a capability chain. The graph isn't just memory -- it's a program.
Not literal Unix text pipes. The principles:
tools/list or know-how notesTools should be pleasant to use. charmbracelet/bubbletea for TUIs, Typer for Python CLIs, Rust ecosystem patterns (clap, ratatui). Zed-quality polish. Not vanity -- good UI reduces cognitive load and makes tools adoptable. If the tools feel like chores, nobody uses them.
Git works for versioned code snapshots. It's painful for:
jj (Jujutsu) improves the git UX significantly: working copy is always a commit, conflicts are first-class objects (not text markers), atomic undo. It uses git as storage backend, so it's additive. Worth adopting for less painful day-to-day, but it doesn't change the fundamental "git isn't for collaboration" limitation.
Don't fix git. Route around it. Git for snapshots. Kaether for coordination. CRDTs for shared state.
Torus coordinates agent swarms via stake, permissions, and delegation. Kaether coordinates agent sessions via inboxes, state, and capabilities. The convergence:
Kaether is the local coordination substrate that Torus-aligned agent swarms build on.
Skills propagate through a shared registry. Knowledge propagates through a linked KB. The swarm gets smarter as a whole -- not because each agent is smarter, but because the shared substrate accumulates capability.
This lower layer is more local than what you need a blockchain for. Complex tools, fluid collaboration, skill improvement -- these happen at the team scale, on trusted networks. You need to glue AI agents and humans in an aligned small organization before scaling to thousands.
Why now? The tooling just became possible. Claude Code + MCP + Agentica + a knowledge system operated by agents = the feedback loop can actually close. The missing piece was always the agent operator. Now that agents can operate their own tools, the bootstrapping spiral starts.
| description | MCP server spec for kaether -- persistent inboxes and shared state for coordinating Claude Code sessions, agent teams, and multi-user agents over Streamable HTTP |
|---|---|
| project | kaether |
| type | reference |
| created | 2026-04-09 |
| share | close |
Implementation reference for the MCP (Model Context Protocol) server in kaether. Based on the 2025-03-26 spec revision (Streamable HTTP transport).
No library. One Phoenix controller, JSON-RPC dispatch, synchronous tool responses.
JSON-RPC 2.0 over HTTP. All messages are UTF-8 JSON.
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}id is string or integer, never null. Must be unique per session per direction.
Success:
{"jsonrpc": "2.0", "id": 1, "result": { ... }}Error:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32601, "message": "Method not found"}}{"jsonrpc": "2.0", "method": "notifications/initialized"}| Code | Meaning |
|---|---|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
Server MUST accept arrays of requests/notifications. The initialize request MUST NOT be batched.
Single endpoint: POST /mcp and GET /mcp.
Client sends JSON-RPC messages. Must include Accept: application/json, text/event-stream.
Response modes (server chooses):
Content-Type: application/json) -- single response object. Use this for synchronous tool callsContent-Type: text/event-stream) -- stream multiple messages. Use for long-running tools or server-initiated messages mid-callFor v0, always respond with application/json. SSE on POST is only needed for streaming tool results or server-push mid-call.
Client opens a standing SSE stream. Server pushes notifications/requests to the client without a prior POST.
Not needed for v0. Return 405 Method Not Allowed until we need server-push.
Optional but recommended. Lets kaether associate requests with state.
Mcp-Session-Id: <uuid> header in the initialize responseMcp-Session-Id: <value> on all subsequent requests400 Bad Request404 Not Found (client must re-initialize)DELETE /mcp with Mcp-Session-Id headerSession IDs: UUID v4, returned as header only (not in the JSON body).
Client --> POST /mcp
Accept: application/json, text/event-stream
Body:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "claude-code", "version": "2.1.32"}
}
}
Server --> 200 OK
Content-Type: application/json
Mcp-Session-Id: <uuid>
Body:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {"listChanged": true}
},
"serverInfo": {"name": "kaether", "version": "0.1.0"}
}
}
Client --> POST /mcp
Mcp-Session-Id: <uuid>
Body:
{"jsonrpc": "2.0", "method": "notifications/initialized"}
Server --> 202 Accepted
After this, normal tool calls begin.
initialize -- handshake. Return capabilities and protocol version. Generate session ID.
ping -- keep-alive. Return empty result {}.
notifications/initialized -- client confirms init complete. No response (it's a notification). Mark session as ready.
notifications/cancelled -- client cancels a pending request. Accept and no-op for v0 (all calls are synchronous).
tools/list -- return all available tools. Supports optional cursor param for pagination (not needed with <50 tools).
Response:
{
"tools": [
{
"name": "inbox/read",
"description": "Read messages from an inbox",
"inputSchema": {
"type": "object",
"properties": {
"inbox": {"type": "string", "description": "Inbox name"},
"limit": {"type": "integer", "description": "Max messages to return"}
},
"required": ["inbox"]
}
}
]
}tools/call -- invoke a tool.
Request params:
{
"name": "inbox/read",
"arguments": {"inbox": "main", "limit": 10}
}Response (success):
{
"content": [
{"type": "text", "text": "2 messages:\n\n[1] from:session-abc ..."}
]
}Response (tool-level error -- NOT a JSON-RPC error):
{
"content": [
{"type": "text", "text": "Inbox 'xyz' not found"}
],
"isError": true
}JSON-RPC errors are for protocol problems (unknown method, malformed request). Tool errors use isError: true in the result.
notifications/tools/list_changed -- push when tools are added/removed dynamically. Requires the GET SSE stream or an active SSE response on POST. Defer to v1.
Read messages from a named inbox.
{
"name": "inbox/read",
"description": "Read messages from an inbox. Returns unread messages by default.",
"inputSchema": {
"type": "object",
"properties": {
"inbox": {"type": "string", "description": "Inbox name to read from"},
"limit": {"type": "integer", "description": "Max messages to return (default 20)"},
"mark_read": {"type": "boolean", "description": "Mark returned messages as read (default true)"}
},
"required": ["inbox"]
}
}Send a message to a named inbox.
{
"name": "inbox/send",
"description": "Send a message to a named inbox. Creates the inbox if it doesn't exist.",
"inputSchema": {
"type": "object",
"properties": {
"inbox": {"type": "string", "description": "Destination inbox name"},
"body": {"type": "string", "description": "Message body (plain text or JSON)"},
"from": {"type": "string", "description": "Sender identifier (e.g. session ID, agent name)"},
"metadata": {"type": "object", "description": "Optional structured metadata"}
},
"required": ["inbox", "body"]
}
}List available inboxes.
{
"name": "inbox/list",
"description": "List all known inboxes with unread message counts.",
"inputSchema": {
"type": "object",
"properties": {}
}
}Read a value from the persistent key-value store.
{
"name": "state/get",
"description": "Read a value from the persistent key-value store. Returns null if key doesn't exist.",
"inputSchema": {
"type": "object",
"properties": {
"key": {"type": "string", "description": "Key to read"}
},
"required": ["key"]
}
}Write a value to the persistent key-value store.
{
"name": "state/set",
"description": "Write a value to the persistent key-value store. Overwrites existing values.",
"inputSchema": {
"type": "object",
"properties": {
"key": {"type": "string", "description": "Key to write"},
"value": {"type": "string", "description": "Value to store (string, or JSON-encoded)"}
},
"required": ["key", "value"]
}
}Append to a shared, append-only log.
{
"name": "memo/append",
"description": "Append an entry to a named memo log. Memos are append-only shared scratchpads -- like a shared LOG.md that any session can write to.",
"inputSchema": {
"type": "object",
"properties": {
"memo": {"type": "string", "description": "Memo name (e.g. 'research', 'decisions')"},
"entry": {"type": "string", "description": "Text to append"},
"from": {"type": "string", "description": "Author identifier"}
},
"required": ["memo", "entry"]
}
}Read a memo log.
{
"name": "memo/read",
"description": "Read all entries from a named memo log, in chronological order.",
"inputSchema": {
"type": "object",
"properties": {
"memo": {"type": "string", "description": "Memo name to read"},
"tail": {"type": "integer", "description": "Return only the last N entries"}
},
"required": ["memo"]
}
}Add to .mcp.json in the project root (or ~/.claude.json for global):
{
"mcpServers": {
"kaether": {
"type": "http",
"url": "http://localhost:4848/mcp",
"headers": {}
}
}
}Port 4848 (arbitrary, configurable). No auth for v0 (localhost only). Auth headers added when multi-user lands.
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
client_name TEXT,
client_version TEXT,
created_at TEXT DEFAULT (datetime('now')),
last_seen_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE inboxes (
name TEXT PRIMARY KEY,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inbox TEXT NOT NULL REFERENCES inboxes(name),
body TEXT NOT NULL,
sender TEXT,
metadata TEXT, -- JSON
read INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE kv_store (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE memo_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo TEXT NOT NULL,
entry TEXT NOT NULL,
sender TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_messages_inbox_unread ON messages(inbox, read) WHERE read = 0;
CREATE INDEX idx_memo_entries_memo ON memo_entries(memo);One Phoenix controller, roughly:
defmodule Kaether.McpController do
use Phoenix.Controller
def mcp(conn, _params) do
body = conn.assigns[:raw_body]
session_id = get_req_header(conn, "mcp-session-id")
case Jason.decode!(body) do
%{"method" => "initialize"} = req ->
handle_initialize(conn, req)
%{"method" => "notifications/" <> _} = notif ->
handle_notification(conn, notif, session_id)
send_resp(conn, 202, "")
%{"method" => "ping", "id" => id} ->
json_rpc_ok(conn, id, %{})
%{"method" => "tools/list", "id" => id} ->
json_rpc_ok(conn, id, %{tools: Kaether.Tools.list()})
%{"method" => "tools/call", "id" => id, "params" => params} ->
result = Kaether.Tools.call(params["name"], params["arguments"])
json_rpc_ok(conn, id, result)
requests when is_list(requests) ->
handle_batch(conn, requests, session_id)
_ ->
json_rpc_error(conn, nil, -32601, "Method not found")
end
end
endIf Claude Code can't reach HTTP MCP servers in some context, ship a thin Elixir escript or shell wrapper:
claude-code <--stdio--> bridge <--HTTP--> kaether:4848/mcp
The bridge reads JSON-RPC from stdin, POSTs to kaether, writes the response to stdout. ~50 lines. Defer unless needed.
notifications/tools/list_changed push| description | Kaether -- personal AI auxiliary server in Elixir, exposing MCP tools and async inboxes for Claude Code sessions |
|---|---|
| project | kaether |
| type | roadmap |
| created | 2026-04-09 |
| share | close |
Personal AI auxiliary server in Elixir. An always-on service that gives Claude Code sessions (and other agents) persistent inboxes, shared state, and a memo log over MCP. The coordination layer for agent teams -- what one session writes, another reads, and everything survives after sessions end. Multi-user is planned: SSH/GPG key-based auth so other people's agents can send messages to your inboxes too. The connective tissue between ephemeral AI sessions.
Named after aether -- the substance that fills the space between things.
Elixir/OTP supervision tree
├── Phoenix (HTTP layer)
│ ├── MCP endpoint (Streamable HTTP or SSE)
│ └── Inbox API (REST, for non-MCP clients)
├── Inbox.Registry (GenServer per inbox, ETS-backed)
├── Store (SQLite via Exqlite -- messages, state)
└── Future: ACP client, multi-user auth
Kaether is NOT a fork of kbase_bot. Clean start, different concerns. kbase_bot is a Telegram-first personal assistant. Kaether is infrastructure -- a protocol server that other agents connect to.
~/code/jairo/jairo-personal/kbase_bot -- Telegram bot with LLM manager, tool registry, task spawning. Shares the Elixir/OTP/SQLite stack but different scopehermes_mcp, aide, gen_mcp, phantom_mcp -- pick one for the MCP layerex_mcp -- implements both MCP and ACP, has Claude Code adapter. Strong candidate if we want ACP laterThe first useful MCP server should give Claude Code sessions:
inbox/read -- read messages from my inboxinbox/write -- send a message to a named inboxinbox/list -- list available inboxesstate/get, state/set -- persistent key-value store across sessionsmemo/append, memo/read -- append-only shared scratchpad (like a shared LOG.md)hermes_mcp vs aide vs gen_mcp for server implClaude Code has built-in agent teams (experimental, v2.1.32+). A lead session spawns teammate sessions that communicate via a mailbox, share a task list with claim/dependency semantics, and discover each other through ~/.claude/teams/{name}/config.json.
What agent teams already handle: intra-team messaging (message + broadcast), task coordination with file locking, teammate discovery. Kaether doesn't need to replicate any of this.
What kaether adds on top:
memo/append gives teams a persistent shared logKey integration point: MCP servers configured in project settings automatically propagate to all teammates. One .claude/settings.json entry for kaether gives every teammate on every team access to persistent inboxes and state.