Han Solo
Architecture & Flows
Click any node or flow to trace the path
Claude (any MCP client) · /mcp Streamable HTTP Web Browser Scott / Ted · Workspace UI Reflection/Jottings Scheduler scripts/reflection_tick.py · launchd, every 20 min Render health check / deploy BearerAuthMiddleware auth.py · raw ASGI (SSE-safe) FastMCP Server server.py · /mcp endpoint · 32 tool groups config.py TOKEN_REGISTRY · provider env vars validation.py write-contract enforcement chat_api.py REST routes · api_send · capture buffer Memory & Sessions memory · memory_work · memory_sessions memory_bundle · memory_meaning memory_identity · memory_delete conversations Builds & Project State phase · project_state · gates risk_registry · risk_score factory_docs · agent_skill register_role Signals, Notecards & Portraits signals · notecards portraits Framework & Skills framework · skills Jottings, Sketch & Graph jottings · sketch · graph Reflection & QA reflection (flag / dry_wells / consume) qa (insert_readback / insert_qa_evidence) Logbook, History, Bridge, Codebase & Health logbook · history · bridge codebase · health run_turn / loop.py provider-agnostic tool-calling loop step cap 20 agent_tools.py tool roster assembly HELD_BACK_TOOLS (10) Provider Adapters Anthropic · Google/Gemini · OpenAI providers/__init__.py registry context_assembler.py 80%-window ceiling rolling-summary fold Anthropic API api.anthropic.com/v1/messages runtime provider + direct calls (image analysis, handoff synthesis) Google/Gemini API generativelanguage.googleapis.com Ren's live provider (gemini-2.5-flash) OpenAI API api.openai.com GPT-5.5 provider + text-embedding-3-small ElevenLabs api.elevenlabs.io · TTS GitHub API codebase webhook / indexer Doppler + Render provisioning.py Create-Agent wizard secrets + redeploy db.py two asyncpg pools · memory_pool + framework_pool PostgreSQL — han-solo-data chat_transcripts · agents session/build/lesson/touchstone schema project_state · jottings · notecards signals · risk_registry PostgreSQL — Framework DB framework_entries / projects skills / state reflection_tick.py launchd · every 20 min POSTs both tick endpoints verify.py launchd · every 30 min full-stack health check parse_transcripts.py launchd · every 30 min Claude Code transcript ingestion Deploy — Dockerfile.mcp python:3.11-slim · COPY han_solo/ + docs/ · uvicorn han_solo.server:app ⚠ separate, unused root Dockerfile still starts FROM letta/letta:0.16.8 — dead, not fixed in this build
Live call
Async / scheduled
Architecture

How the pieces fit together

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.

The request lifecycle — chat and MCP

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.


What the runtime is, and why it replaced Letta

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.


The tool-roster distinction — Ren's held-back set vs. Claude Code's direct access

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:

CategoryToolsWhy
Irreversible deletesdelete_core_block, delete_archival_passage, delete_framework_entry, delete_conversation, delete_conversation_entry, delete_sessionNo runtime agent may self-invoke a permanent delete of identity, memory, or project-artifact data.
Self-re-entrant bridge callssend_to_renThe agent on the runtime IS the agent — calling this would re-enter its own turn loop.
Approval / authority-executionapprove_gate, advance_phaseAn agent may advise, but the final approval authority rests with Scott.
Granted-to-nobody-by-defaultinsert_qa_evidenceReserved exclusively for the Claude Code QA-swarm write path until a deliberate future decision opts a runtime agent in.

The two background-job families

Reflection & Jottings ticksscripts/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.


External services — what each is used for

ServiceUsed for
Anthropic APIOne 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 APIRen's live provider today — gemini-2.5-flash, resolved per-turn from her agents-registry row.
OpenAI APIA supported runtime provider (GPT-5.5) and the source of text-embedding-3-small embeddings used elsewhere in the codebase (codebase indexing).
ElevenLabsText-to-speech for the chat UI's voice playback, called directly from chat_api.py's /api/tts route.
GitHub APIThe 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 + Renderprovisioning.py writes a new per-agent secret to Doppler and triggers a Render redeploy — the mechanism behind the Create Agent wizard.

The two-pool database split

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.


Deployment — one honest callout

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.

Known issue, not fixed here: a separate, unused file at the repo root — plain 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.

Bands and flows, at a glance

33 nodes across 8 section-labeled bands; 8 clickable flows, each tracing one real request path end to end.

BandNodes
Clients4 — Claude, Web Browser, Reflection/Jottings Scheduler, Render
Auth / Server5 — BearerAuthMiddleware, FastMCP Server, config.py, validation.py, chat_api.py
Tool Layer7 grouped boxes covering all 32 registered tool modules
Runtime4 — run_turn/loop.py, agent_tools.py, Provider Adapters, context_assembler.py
External Services6 — Anthropic, Google/Gemini, OpenAI, ElevenLabs, GitHub, Doppler + Render
Database3 — db.py, PostgreSQL han-solo-data, PostgreSQL Framework DB
Background Jobs3 — reflection_tick.py, verify.py, parse_transcripts.py
Deploy1 — Dockerfile.mcp (+ the dead-root-Dockerfile callout)