The diagram above is interactive — drag to pan, scroll to zoom, and click any node or any flow in the sidebar to trace the exact path a request takes. Everything below restates the same facts in prose, per the docs site's rule that diagrams supplement text but never replace it.
Two entry points reach the same server. Scott and Ted talk to Ren through the Workspace Chat UI (POST /api/send). Claude Code, and any other MCP client, talks to the same server over the MCP Streamable HTTP transport (POST /mcp). Both paths pass through BearerAuthMiddleware first — a raw ASGI middleware (deliberately not Starlette's BaseHTTPMiddleware, which buffers responses and would break the SSE streaming MCP's transport needs) that validates the Authorization: Bearer header against TOKEN_REGISTRY and stores the resolved identity on a per-request ContextVar for the rest of the request's lifetime.
From there the two paths diverge. On the chat side, chat_api.api_send first gates on _require_room_member (the chat is currently Scott and Ren's room only — a collaborator token gets a plain 403), builds the outbound message (handling an image attachment via a direct Claude call that bypasses the runtime entirely, a file attachment, or open-sketch-room context), snapshots the prior conversation, pushes the new user turn to the in-memory history cache, and fires an async capture task to chat_transcripts before calling the runtime through _ren_runtime_turn — the single call this path makes into the runtime.
On the MCP side, server.py registers exactly 32 tool groups (memory, signals, phase, brief, portraits, notecards, framework, skills, logbook, history, bridge, codebase, health, gates, jottings, sketch, and 17 more) and FastMCP dispatches each JSON-RPC call to the matching @server.tool() handler. Any tool that writes calls into validation.py first — assert_can_write_block, assert_can_write_signal, assert_can_write_registry, or assert_can_write_factory_doc, each enforcing an owner-only or owner-or-Ren-only write set before the call reaches db.py. Every tool now reads and writes through one of exactly two asyncpg pools — there is no third, legacy backend to route to anymore.
run_turn (han_solo/runtime/loop.py) is the provider-agnostic tool-calling loop that replaced self-hosted Letta entirely. One call is one full turn: resolve the calling agent's provider configuration from the agents registry table (db.get_agent_config — a missing or unknown provider fails loud, never silently defaults), assemble context (context_assembler.py loads the agent's core blocks, folds in that thread's carried rolling summary, and applies an 80%-of-context-window ceiling), call the resolved provider (Anthropic, Google, or OpenAI, via the providers/__init__.py routing factory bound to that agent's own key), and loop — dispatching any tool call the model makes back through the same MCP tool registry Claude Code itself uses, up to a step cap of 20. Every outcome — a clean completion, a healed truncation retry, or an honest failure — is written to a turn record via db.write_runtime_turn_record. Nothing is ever silently dropped.
The old Letta-era mechanic — a hard message-count limit that triggered a chat-side "session rollover" (an inline Anthropic synthesis call, then a PATCH to Letta's reset-messages endpoint) — is gone completely. There is no PATCH, no separate rollover step, and no memgpt_agent. Context management now happens automatically and transparently inside every single run_turn call, chat or scheduled tick alike.
agent_tools.py's default_agent_tools(server, agent_id) builds the curated roster any runtime agent (Ren today) gets: the full live FastMCP tool registry minus a 10-tool HELD_BACK_TOOLS set, plus 3 own-block read/write tools (read_own_block, list_own_blocks, write_own_block — full-replace, verified read-back, versioned). Every dispatched call runs under a synthesized per-agent UserIdentity (Role.AGENT, full read clearance) — entirely separate from Claude Code's own bearer-authenticated MCP session, which is role-gated per agent (agent_permissions — Maya, Kate, Recon, and each QA role each see a different, smaller tool set than Claude's own).
The 10 held-back tools fall into four categories, by principle rather than a frozen list — a future tool that fits a category is excluded automatically the moment it's added to HELD_BACK_TOOLS:
| Category | Tools | Why |
|---|---|---|
| Irreversible deletes | delete_core_block, delete_archival_passage, delete_framework_entry, delete_conversation, delete_conversation_entry, delete_session | No runtime agent may self-invoke a permanent delete of identity, memory, or project-artifact data. |
| Self-re-entrant bridge calls | send_to_ren | The agent on the runtime IS the agent — calling this would re-enter its own turn loop. |
| Approval / authority-execution | approve_gate, advance_phase | An agent may advise, but the final approval authority rests with Scott. |
| Granted-to-nobody-by-default | insert_qa_evidence | Reserved exclusively for the Claude Code QA-swarm write path until a deliberate future decision opts a runtime agent in. |
Reflection & Jottings ticks — scripts/reflection_tick.py runs via launchd every 20 minutes and POSTs both /api/reflection/tick and /api/jottings/tick independently (one failing doesn't skip the other). Both endpoints are bearer-exempt — a launchd cron job carries no user identity — and instead self-gate on a shared REFLECTION_TICK_SECRET header, checked first and fail-closed. Reflection is Ren's private inward turn: it evaluates thresholds (an urgent flag, >24 hours since her last reflection, a summary over 750 words, more than 12 turns since, or 3+ queued non-urgent flags) and, if one trips, runs one tool-armed run_turn('ren', ...) so she can write her own memory — touchstones, lessons, portraits. Jottings is her public outward turn on a fully separate runtime thread and a separate advisory lock, engaging the oldest new item in the team's shared writing space and posting through her own post_reply. Both only advance their marker when the turn returns tr.ok == True — a failed turn leaves the marker exactly where it was, so nothing is ever silently marked "handled" that wasn't.
verify.py & parse_transcripts.py — both run via launchd every 30 minutes, independent of the reflection/jottings pair. verify.py runs a full-stack health check (tokens, live MCP tool count, memory-schema integrity, framework access, the docs site, Render cold-start timing) authenticated as the dedicated, default-deny SERVICE role — it may reach only its explicit allowlisted surface, nothing else. parse_transcripts.py reads Claude Code's local JSONL session files from ~/.claude/projects/, extracts structured entries, and POSTs them to /api/transcripts — the durable record of Claude Code work sessions, entirely independent of any Anthropic API call.
| Service | Used for |
|---|---|
| Anthropic API | One of the three runtime providers, plus two calls that bypass the runtime entirely: direct image analysis (_analyze_image) on the chat path, and handoff synthesis. |
| Google/Gemini API | Ren's live provider today — gemini-2.5-flash, resolved per-turn from her agents-registry row. |
| OpenAI API | A supported runtime provider (GPT-5.5) and the source of text-embedding-3-small embeddings used elsewhere in the codebase (codebase indexing). |
| ElevenLabs | Text-to-speech for the chat UI's voice playback, called directly from chat_api.py's /api/tts route. |
| GitHub API | The one external service that calls in — an inbound codebase webhook that feeds the code indexer, the opposite direction from every other external-service edge on the diagram. |
| Doppler + Render | provisioning.py writes a new per-agent secret to Doppler and triggers a Render redeploy — the mechanism behind the Create Agent wizard. |
db.py holds exactly two separate asyncpg connection pools — no third, legacy pool exists anymore. init_memory_pool() connects to han-solo-data (the agent memory schema: sessions, conversations, builds, lessons, touchstones, project_state, jottings, notecards, signals, risk_registry, chat_transcripts, and the agents registry itself) and verifies current_database() == 'han_solo_data' before running any schema DDL — a hard, loud guard against a misconfigured connection string silently applying memory-schema migrations to the wrong instance (the exact failure class a real prior incident caused). init_framework_pool() connects to the separate Framework DB — framework_entries, projects, skills, and phase state, the Solo Builder Framework's own system of record.
The live deploy artifact is Dockerfile.mcp: a python:3.11-slim image that copies han_solo/ and docs/, installs requirements.txt, and runs uvicorn han_solo.server:app. This is what Render actually builds and serves.
Dockerfile (no .mcp suffix) — still opens with FROM letta/letta:0.16.8 and installs psycopg2-binary. It is not part of the deploy pipeline; Render builds only from Dockerfile.mcp. It is dead weight left over from the Letta era, flagged here for cleanup, not fixed as part of this rebuild.
33 nodes across 8 section-labeled bands; 8 clickable flows, each tracing one real request path end to end.
| Band | Nodes |
|---|---|
| Clients | 4 — Claude, Web Browser, Reflection/Jottings Scheduler, Render |
| Auth / Server | 5 — BearerAuthMiddleware, FastMCP Server, config.py, validation.py, chat_api.py |
| Tool Layer | 7 grouped boxes covering all 32 registered tool modules |
| Runtime | 4 — run_turn/loop.py, agent_tools.py, Provider Adapters, context_assembler.py |
| External Services | 6 — Anthropic, Google/Gemini, OpenAI, ElevenLabs, GitHub, Doppler + Render |
| Database | 3 — db.py, PostgreSQL han-solo-data, PostgreSQL Framework DB |
| Background Jobs | 3 — reflection_tick.py, verify.py, parse_transcripts.py |
| Deploy | 1 — Dockerfile.mcp (+ the dead-root-Dockerfile callout) |