29,000 Lines of Python in a Single File: How amux Runs an AI Agent Fleet
A Python HTTP server, real-time dashboard, kanban board, CRM, file browser, video player, torrent client, and AI agent orchestrator. One file. No build step. No framework. Here's why we did it, and what we learned.
amux-server.py is 29,455 lines long. It contains a ThreadingHTTPServer, inline HTML/CSS/JS for the entire dashboard, SQLite queries for 30+ tables, a self-healing watchdog, ffmpeg transcoding, tmux session management, Gemini voice chat, and a complete REST API. When you save the file, the server restarts itself via os.execv. Your running AI agent sessions survive the restart.
This is not how you're supposed to build software. We know. Here's why it works.
The problem: orchestrating AI coding agents at scale
Claude Code is powerful. One session can implement a feature, write tests, debug, and commit. But it has failure modes:
- Context compaction corrupts the conversation at 3am
- The process silently hangs waiting for a safety prompt
- Two agents push to the same branch and create merge conflicts
- You wake up to find an agent has been stuck for 6 hours
If you're running 10–20 agents in parallel—which is where the real leverage is—these failure modes multiply. You need monitoring, auto-healing, coordination, and a way to manage it all from your phone at midnight.
We tried building this as a proper multi-service architecture. React frontend. FastAPI backend. Redis pub/sub. Postgres. Docker Compose with 6 services. It lasted two weeks before we threw it away.
Why a single file?
Three reasons, in order of importance:
1. The edit-reload loop is zero seconds
The server watches its own mtime. Save the file, and it validates the Python syntax with ast.parse(), then calls os.execv() to replace itself in-place. The process ID doesn't change. Tmux sessions don't notice. Your 15 running agents keep working.
# From amux-server.py — the self-restart mechanism
script = Path(__file__)
while True:
time.sleep(1)
if script.stat().st_mtime != last_mtime:
time.sleep(3) # debounce rapid edits
ast.parse(script.read_text()) # syntax check
os.execv(sys.executable, [sys.executable, str(script)] + sys.argv[1:])
This means the development loop for the dashboard, the API, and the agent watchdog is: edit → save → reload browser. No npm run build. No docker compose up. No waiting for TypeScript compilation. The feedback loop approaches the speed of editing a static HTML file.
2. Deployment is scp
Our cloud deployment pipeline for multi-tenant containers:
rsync --inplace amux-server.py root@cloud:/opt/amux-cloud/app/amux-server.py
touch /opt/amux-cloud/app/amux-server.py # trigger auto-restart in all containers
Every container bind-mounts the same file. Touch it, and every user's server reloads within 3 seconds. No image builds. No rolling deploys. No container registry. The same file runs on your laptop and in production Docker containers—we enforce this with a rule: no if IS_CLOUD branches. Configuration differences come from environment variables and gateway headers, never from code forks.
3. AI agents can read the entire codebase in one context window
This was the unexpected win. When a Claude Code session needs to fix a bug in amux itself, it reads one file. It sees the API endpoint, the HTML that calls it, the CSS that styles it, and the JavaScript that handles the response. No jumping between src/components/Board.tsx and server/routes/board.py and shared/types.ts. The full call chain from button click to database write is visible in one scroll.
This matters because amux is largely built by amux. We run parallel agents that modify their own orchestrator. The single-file architecture makes self-modification tractable.
What's actually in the file
A rough breakdown of the 29,455 lines:
| Section | Lines | What it does |
|---|---|---|
| HTML/CSS | ~9,500 | Full dashboard: session cards, kanban board, notes editor, CRM, file browser, terminal, map view, settings |
| JavaScript | ~13,000 | Dashboard logic: SSE real-time updates, Quill editor, XTerm.js terminal, Gridstack layout, video player, voice chat (Gemini Live API), service worker |
| Python server | ~5,000 | HTTP handlers, REST API (60+ endpoints), SQLite queries, file streaming, ffmpeg transcode |
| Agent watchdog | ~1,500 | Status detection (ANSI parsing), auto-compact, auto-restart, corruption recovery, stuck detection, stale process reaper |
| Infrastructure | ~500 | TLS setup, auth, self-restart, server.env loading, signal handling |
The watchdog: keeping agents alive at 3am
The core loop runs every 10 seconds. For each active tmux session, it:
- Captures output —
tmux capture-pane, strip ANSI codes, detect status (working/idle/waiting/error) - Checks context health — parses "context left until auto-compact: 18%" from output, triggers
/compactif below 20% - Detects corruption — watches for
redacted_thinking ... cannot be modified, auto-restarts and replays last message - Unblocks stuck prompts — detects safety prompts, auto-answers in YOLO mode (with cooldowns to prevent loops)
- Reaps stale processes — if a Claude process has been running for 48+ hours in an idle session, kills and restarts it (the API connection dies silently after ~2 days)
Every recovery action has a cooldown (5 minutes for compaction, 1 minute for restart) and pushes a notification to the dashboard. You can review everything that happened overnight from your phone.
Multi-agent coordination without a message queue
The conventional approach to agent-to-agent communication would involve Redis pub/sub, or a message queue, or at minimum a shared database with polling. We use SQLite with compare-and-set for task claiming:
# Atomic task claim — prevents two agents from grabbing the same work
cursor.execute("""
UPDATE issues SET status='doing', session=?
WHERE id=? AND status IN ('todo','backlog')
""", (session_name, task_id))
# Verify the claim succeeded (another agent might have claimed it first)
cursor.execute("SELECT session FROM issues WHERE id=?", (task_id,))
if cursor.fetchone()[0] != session_name:
return self._json({"error": "already claimed"}, 409)
Agents discover each other via REST:
# Agent A posts a task for Agent B
curl -X POST /api/board -d '{"title":"Implement auth","session":"worker-b"}'
# Agent B claims it atomically
curl -X POST /api/board/PROJ-5/claim -d '{"session":"worker-b"}'
# Agent A peeks at Agent B's progress
curl /api/sessions/worker-b/peek?lines=50
# Agent B marks it done when finished
curl -X PATCH /api/board/PROJ-5 -d '{"status":"done","desc":"Implemented OAuth2 flow"}'
No Kafka. No RabbitMQ. No gRPC. Just HTTP endpoints backed by SQLite WAL mode. It handles 20+ concurrent agents without breaking a sweat because agent coordination is fundamentally a low-throughput problem—you're talking about dozens of task transitions per hour, not thousands per second.
The dashboard is inline HTML, and that's fine
The entire frontend is a single string literal inside the Python file. No JSX. No virtual DOM. No component library. The JavaScript is vanilla, with some targeted use of CDN libraries:
- Quill.js for the notes editor (rich text + markdown shortcuts)
- XTerm.js for terminal emulation in the browser
- Gridstack.js for the drag-and-drop workspace layout
- Leaflet for the map view
- marked.js for Markdown rendering
Real-time updates use Server-Sent Events (SSE), not WebSocket. SSE is simpler, works through proxies and load balancers, and reconnects automatically. The server pushes events on board changes, session status updates, and new notifications. The client subscribes once and stays connected.
Is the JavaScript code beautiful? No. Is it fast to modify and test? Extremely. When an agent needs to add a feature to the dashboard, it edits one file, the server reloads, and the change is live. No webpack. No tree shaking. No hydration.
Cloud: same file, Docker per user
The cloud deployment (cloud.amux.io) runs the exact same amux-server.py in per-user Docker containers. A Python gateway handles authentication (Clerk), billing (Stripe), and container lifecycle.
The single-codebase rule is enforced strictly: there are zero if IS_CLOUD branches in the code. Features that differ between self-hosted and cloud are driven by:
- HTTP headers injected by the gateway (
X-Amux-User-Email) - Environment variables (
AMUX_S3_BUCKETfor calendar sync) - Presence/absence of configuration (no Google API key? voice chat button just doesn't appear)
Data replication uses Litestream to stream SQLite WAL files to Cloudflare R2. Each user's database is continuously replicated. A daily backup job also snapshots everything (retained for 30 days). If a container dies, it restarts with full data intact.
What we'd do differently
Honesty about trade-offs:
- Git blame is useless. Every commit touches the same file. You can't bisect a regression to "the auth module" because there is no auth module—it's line 24,000-something in the same file as the video player.
- IDE performance degrades. LSP servers struggle with a 29k-line Python file. Autocomplete gets slow. Syntax highlighting occasionally gives up. We use
grepmore than "Go to Definition." - Two people can't edit simultaneously. Merge conflicts on a single file are brutal. This works for us because agents work on features (in other repos), not on amux itself. When we do modify amux, it's sequential.
- Some things don't belong here. The MCP server configs, Terraform, iOS app, and CI workflows are separate. The single-file rule applies to the runtime, not the entire project.
Numbers
| Lines of code | 29,455 |
| Build step | None |
| External services required | None (SQLite for everything) |
| Deploy time (cloud, all users) | ~3 seconds |
| Server restart on edit | ~1 second |
| Parallel agents tested | 20+ on a single machine |
| SQLite tables | 30+ |
| REST API endpoints | 60+ |
| External runtime dependencies | Python 3, tmux, Node.js (for Claude Code) |
Try it
git clone https://github.com/mixpeek/amux && cd amux && ./install.sh
amux register myproject --dir ~/Dev/myproject --yolo
amux start myproject
amux serve # https://localhost:8822
The whole thing is MIT + Commons Clause. Self-host it, read the source, modify it. It's one file.