Architecture
Yuna is pull-based, stateless, and fire-and-forget. The orchestrator never opens a connection to your device. Communication flows through Redis Streams as a broker and long-poll HTTPS as pseudo-push. This is the same pattern GitHub Actions runners and Celery workers use — it just happens to be wrapped around a Claude agent loop.
The end-to-end flow
Telegram Vercel Redis Device
──────── ────── ───── ──────
user msg ─── webhook ───▶ orchestrator
│
├─▶ Claude API (tool_use)
│
│◀── tool_use: run_on_{dev}
│
├─── risk gate ─── risky? ─── Telegram ⚠️
│ (await 👍/❌)
│
└─── XADD ───────▶ stream:{dev}
│
│◀─── XREADGROUP (long-poll, 25s)
│
device
executes
bash
┌────── POST /respond ◀─────────────────────────── output
│
├─▶ Claude API (tool_result)
│
│◀── final text
│
reply ◀── sendMessage ─── ┘Components
Server (Next.js on Vercel)
lib/orchestrator.ts— the Claude agent loop. Holds the conversation, builds tools dynamically, dispatches commands, resumes on tool_result.lib/tools.ts— generatesrun_on_{device},read_file,write_file, and (conditionally)transfer_filetool definitions from the device registry on every Claude call.lib/system-prompt.ts— builds a dynamic system prompt with one section per registered device (status, OS, description, capabilities). Tells the model that<tool_output>content is untrusted.lib/risk.ts— regex classifier for destructive commands (rm -rf, dd, sudo, force-push, mkfs, eval, etc.). Flags go through the confirmation gate.lib/redis.ts— typed helpers over Upstash Redis for streams, conversation, orchestration tasks, audit log, pending confirmations.lib/devices.ts— device registry CRUD with online status (heartbeat window 60s).lib/auth.ts— per-device UUID tokens, one-time setup codes, hashed master secret, Telegram owner lock.- API routes:
/api/telegram/webhook,/api/relay/poll,/api/relay/respond,/api/relay/register,/api/devices,/api/health.
Device agent
agent.ts— polling loop. Long-polls/api/relay/pollwith a per-fetch AbortController to avoid leaking listeners on the shutdown signal. Exponential backoff on network failure (1s → 30s cap).executor.ts— bash / read_file / write_file / transfer_file. Per-command timeout (default 60s, max configurable viatimeout_secondsin the tool input). Output truncated at 8 KB with head+tail preservation.- Runs on any Linux or macOS box with Node 18+. Only needs outbound HTTPS to your Vercel URL — no inbound ports, no Tailscale required.
Redis keys
| Key | Type | Purpose |
|---|---|---|
yuna:devices | SET | Registered device names |
yuna:device:{name} | HASH | Device metadata (os, description, capabilities, ssh) |
yuna:token:{token} | STRING | Per-device auth token → device identity |
yuna:device-token:{name} | STRING | Reverse index for token revocation |
yuna:setup-code:{code} | STRING | One-time device setup code (10-min TTL) |
yuna:lastseen:{name} | STRING | Device heartbeat timestamp |
yuna:stream:{name} | STREAM | Per-device command queue, consumer group agent |
yuna:conversation:messages | STRING | Shared conversation history (JSON) |
yuna:orchestration:{taskId} | STRING | In-flight agentic task state (5-min TTL) |
yuna:pending-confirm:{msgId} | STRING | Risky command awaiting 👍/❌ reaction (5-min TTL) |
yuna:log | LIST | Audit log, capped at 1000 entries |
yuna:master | STRING | Hashed master secret |
Wire protocol
The orchestrator serializes a command to the device stream as a flat field map (Redis Streams don't support nesting):
{
"type": "command",
"taskId": "uuid",
"tool": "run_on_uconsole",
"input": "{\"command\":\"df -h\",\"timeout_seconds\":60}",
"chatId": "123456",
"messageId": "42",
"payload": "...",
"timestamp": "2026-04-13T03:30:00.000Z"
}The device responds via POST:
POST /api/relay/respond
Authorization: Bearer <device-token>
{
"taskId": "uuid",
"output": "<bash stdout+stderr>",
"exitCode": 0,
"streamId": "1234567890-0"
}The orchestrator then ACKs the stream entry, appends the output to the orchestration task (wrapped in <tool_output> delimiters), and feeds the batch back to Claude as tool_result blocks.
Why this architecture
- Devices are stateless and firewall-friendly. They only make outbound HTTPS. No VPN, no port forwarding, no Tailscale required. Works from coffee shops and cell data without any extra setup.
- The server is stateless too. Every request to Vercel loads conversation + orchestration state from Redis, does its work, and returns. Vercel can scale to zero between invocations — you only pay for actual traffic.
- Redis Streams give guaranteed delivery. Consumer groups with XACK mean a command is either processed or reclaimed if the device crashes mid-execution. No silent drops.
- Dynamic tools mean zero code changes per device. Adding a device is a runtime operation. Claude's tool list is regenerated from the registry on every call, and the system prompt describes each device so the model can route intelligently.