Architecture

How it works

Five pieces working together — each with a distinct role. Understanding the stack means you know what breaks and why, and what it costs.

The stack

ServiceWhat it doesCost
han-solo-db PostgreSQL 16 + pgvector on Render. Stores everything — Letta agents, memory blocks, archival passages, conversation history. The source of truth for all of Ren's memory. $7/mo
han-solo-letta Letta v0.16.8 on Render. The AI memory runtime — manages Ren as a MemGPT agent, handles tool calls, conversation storage, and memory compression. Ren's brain lives here. $7/mo
han-solo-mcp FastMCP server on Render. The bridge between Claude Code and Letta — 67 tools covering memory, portraits, signals, T4 project data, notecards, search, and session briefs. Also serves the chat UI, workspace, and REST API. Deployed from GitHub on every push. $7/mo
Google AI API Gemini 2.5 Flash (BYOK via Google AI) — the model powering Ren's responses. Every message you send hits this API. Pay-per-token, billed separately from the Claude Pro subscription. Variable
Voyage AI Embedding model (voyage-3, 1024-dim) for archival memory search. When Ren searches her memory, Voyage converts the query to a vector and finds semantically similar passages. Minimal
Total fixed cost: ~$21/month for Render (3 services). Anthropic API is variable — $50 in credits lasts months at normal usage. The Claude Pro/Max subscription is separate and covers claude.ai and Claude Code only.

What happens when you send a message

Every message in the chat UI goes through this chain:

  1. Chat UI (browser) — you type a message and hit Send.
  2. han-solo-mcp — receives the message via POST /api/send, authenticates your bearer token, forwards to Letta.
  3. Letta — receives the message, loads Ren's core memory blocks into context (always_loaded_core, pending_thoughts, portraits), and sends everything to Anthropic.
  4. Google AI API — Gemini 2.5 Flash generates Ren's response, potentially calling tools (archival search, memory write, web fetch).
  5. Letta — stores the exchange in PostgreSQL, returns the response.
  6. han-solo-mcp — passes the response back to the UI.
  7. Chat UI — renders Ren's reply.

Tool calls (when Ren searches memory, writes to a block, or fetches a page) add extra round-trips to Anthropic. Each tool call is a separate LLM inference — this is why sessions with heavy archival searching or page fetching cost more.


Ren's memory layers

Core memory blocks (always loaded)

Ten or more blocks loaded into every prompt. This is the baseline context cost that Ren carries into every message whether she uses it or not. Portrait blocks grow as signals accumulate — forming layers promote to trusted when patterns are confirmed across multiple sessions.

BlockWhat it holdsLimit
always_loaded_coreFramework context, working norms, Scott's profile summary, memory use instructions, session close-out ritual10,000 chars
pending_thoughtsSession brief — what happened last session, what's open, what's next. Written nightly by the dream script.8,000 chars
project_stateCurrent in-flight project context (JSON). Phase, active slice, project metadata.10,000 chars
scott_portrait_formingRen's evolving interpretation of Scott — specific, dated observations about how he thinks and what he values20,000 chars
scott_portrait_trustedPatterns confirmed across multiple sessions — promoted from forming when a signal recurs20,000 chars
ted_portrait_formingRen's evolving interpretation of Ted — same signal model as Scott's portrait20,000 chars
ted_portrait_trustedConfirmed patterns about Ted20,000 chars
ren_portrait_formingRen's self-portrait — what she got right, what she missed, what she wants to develop20,000 chars
ren_portrait_trustedConfirmed self-observations20,000 chars
seed_signalsEarly-session observations, dated signals, relational notes not yet moved to archival20,000 chars

Archival memory (searchable)

214+ passages stored as vector embeddings in pgvector (Voyage AI, voyage-3, 1024 dimensions). Ren searches this with archival_memory_search when she needs context that isn't in her core blocks. Every search is logged to memory_access_log — the query, what was returned, and whether it was used. This is the Memory MRI: a diagnostic record of whether her memory is actually being accessed and whether the right things surface.

Passages are written by Ren and Claude Code during sessions — specific, real captures written in the moment they matter. Session close-outs write structured content directly to archival without an intermediate processing step.

Conversation history

Every message stored in PostgreSQL. Letta loads recent messages into context on each send. This is what fills the context window over time and causes the crash if not managed.

Context window: 1,000,000 tokens (Gemini 2.5 Flash's limit). Core blocks use ~3,000 tokens. Each message exchange uses 500–2,000 tokens. Heavy page fetches can use 10,000+ tokens in a single exchange. A warning banner appears at 60 messages; automatic rollover fires at 150 to stay well clear of the limit.

Session rollover

The context window crash that happened on 2026-05-13 (the day this docs site was built) exposed a gap: no way to reset a Letta conversation without losing everything. The fix is now live.

When the session rolls over — either automatically at 150 messages, or manually via the "New session" button:

  1. A handoff summary is synthesized from the current conversation history.
  2. The summary is written to pending_thoughts on the same agent so Ren has immediate context.
  3. Message history is cleared on the existing Letta agent — same agent, core blocks and archival memory remain untouched.
  4. The chat UI clears and shows a "New session — memory intact" divider.

Ren's memory is never lost in a rollover — only the raw conversation thread resets. The nightly dream captures session content before that happens.


Automated background jobs

dream.py — nightly reflection (2am, Mac launchd)

Every night at 2:00am, a script on Scott's Mac sends Ren a structured reflection prompt via Letta's REST API. Ren uses her own built-in tools to reflect and write a fresh session brief. She:

  1. Searches her conversation history for the day's exchanges
  2. Searches archival memory for relevant context
  3. Writes a fresh session brief to pending_thoughts
  4. Adds portrait signals for Scott and herself if anything worth noting happened
  5. Checks the Letta GitHub releases page for updates newer than the current version

Runs via launchd (com.scotth.rendream.plist). No external dependencies — stdlib Python only.

How Mac-to-cloud works: The script makes a single HTTP POST to han-solo-letta.onrender.com with the dream prompt. Scott's Mac just fires that request — everything after that (Letta processing, Ren calling her own tools, memory writes) happens entirely on Render's servers. The Mac doesn't need to stay connected or wait. If the Mac is asleep at 2am and wakes up later, launchd runs the job when it can. If it misses a night entirely, Ren skips that reflection — no retry.

parse_transcripts.py — Claude Code session capture (every 30 min, Mac launchd)

Every 30 minutes, a script on Scott's Mac reads Claude Code session JSONL files from ~/.claude/projects/, parses them into structured entries, and pushes them to the Han Solo database. Only sessions from the last 45 days are kept. Ren can search these via the search_transcripts MCP tool — raw session history without any Anthropic API calls.

dream.py respects a jobs_paused flag in the han_solo_config Postgres table. Toggle it from the Memory panel in the chat UI.


Claude Code integration

Claude Code connects to Han Solo via the MCP server at han-solo-mcp.onrender.com/mcp. This gives Claude Code 67 tools:

The Solo Hook (~/.claude/hooks/framework-skill-inject.sh) fires before every Claude Code session. It calls get_session_brief, pulls Ren's pending thoughts and always-loaded context, and injects them into the session — including the current date and time. Claude Code starts every session already oriented from where Ren left off, with no manual briefing required.


Deployment

All three Render services deploy from github.com/scoots31/han-solo. A push to main triggers automatic redeploy of han-solo-mcp (the only service with code that changes). Letta and the database are stable services that rarely need touching.

Detailed deployment notes — 16 challenges logged and resolved — are in ~/Developer/han-solo/DEPLOYMENT.md. Read that before touching the stack.