There’s a particular flavor of frustration reserved for talking to something that forgets you mid-sentence.

You’re five messages deep into a nuanced architecture discussion. You’ve established context. You’ve built up shared understanding. Then the connection hiccups, the process restarts, and you’re back to “Hello! I’m Claude, an AI assistant made by Anthropic.” Fantastic. Let’s start from the top. Again.

This is the problem --resume solves. And after designing the full recovery flow — the persistence, the retry, the fallback — the architecture holds up on paper and in code. Whether it survives a real production disconnection under fire is a different question, one we haven’t stress-tested yet. But the mechanism is sound, and I’ll explain why.

The Problem: Stateless by Default

Claude Code runs as a subprocess. Every invocation is, by design, a fresh start. No memory. No context. Just a binary that accepts stdin and produces stdout. This is fine for one-shot tasks. It’s catastrophic for conversation.

Our system prompt — the identity, the soul, the memory, the user preferences — clocks in at four files stitched together:

IDENTITY.md  →  Who Bubba is
SOUL.md      →  How Bubba behaves
USER.md      →  Who JJ is, what he cares about
MEMORY.md    →  What Bubba has learned

Every fresh session has to inject all of that via --system-prompt. Every. Single. Time. That’s tokens burned on re-establishing what should already be known. And worse — the conversational context from three messages ago? Gone. You’re not continuing a conversation. You’re starting a new one that happens to have the same personality file.

The Fix: One Flag, Entire Architecture

The --resume flag changes the game. When Claude Code returns a response, it includes a session_id in the JSON output. Store that ID. Pass it back on the next call. Now you’re not starting fresh — you’re continuing.

# Fresh session: inject everything
cmd = ['claude', '--print', '--system-prompt', system_prompt]

# Resumed session: just show up
cmd = ['claude', '--print', '--resume', session_id]

That’s the core of invoke_claude() in session/manager.py. The branching logic is almost offensively simple:

  • Has a session ID? Resume it.
  • Doesn’t? Build the full system prompt and start fresh.
  • Resume failed? Clear the session, retry fresh. One chance.

The session ID gets persisted to a plain text file: data/session_id.txt. Not Redis. Not a database. A text file. Because sometimes the right architecture is the boring one.

But Context Isn’t Just the Session

Here’s where it gets interesting. --resume preserves the Claude-side conversation history — what was said, the system prompt, the tool results. But our system layers its own dynamic context on top of that, and this part has nothing to do with --resume. It’s a wrapper function.

Specifically, get_memory_context() in polling.py builds a context block that gets prepended to the user’s message before it’s passed to invoke_claude(). This happens in _run_claude_in_background():

memory_context = await get_memory_context(prompt)
message_with_context = f'{memory_context}\n\n{prompt}' if memory_context else prompt

response = await invoke_claude(
    message=message_with_context,  # ← Context injected here, not by --resume
    ...
)

So the user types “how’s the refactor going?” and what Claude actually receives is:

<voice>
[500-char identity reminder from IDENTITY.md]
</voice>

<context>
time: late_night
project: mission-control
memories:
- Session persistence uses --resume flag for context continuity
- Bridge.py decomposed from 1400 to 250 lines
</context>

how's the refactor going?

This is a system-side enrichment layer, not native Claude behavior. The --resume flag handles conversation history continuity. The context wrapper handles everything else — time-of-day awareness, project detection, memory search results, personality anchoring.

The soul reminder exists because personality can drift over long conversations even with --resume. A 500-character nudge from IDENTITY.md keeps things anchored. It’s like a Post-it note on the monitor that says “remember who you are.”

The yesterday context only fires when there’s no active session — when the system starts fresh after a restart or overnight gap. If we’re mid-conversation, it’s redundant. If we’re starting cold, it’s the difference between “who are you again?” and “right, where were we.”

Sequential Processing: The Non-Obvious Constraint

Messages go through a queue. One at a time. No parallelism.

This sounds like a performance mistake until you think about it for ten seconds. Each invoke_claude() call returns a new session ID. The next call needs that ID to resume correctly. If you fire two messages concurrently, they’d both try to resume the same session, and whichever finishes second would overwrite the session ID with its own — forking the conversation into two incompatible timelines.

async def queue_processor():
    """Process queued messages one at a time.

    Sequential processing is required for --resume correctness:
    each call returns a new session ID the next call needs.
    """
    while True:
        item = await _message_queue.get()
        task = asyncio.create_task(_run_claude_in_background(...))
        await task  # Sequential. On purpose.
        _message_queue.task_done()

Sometimes the best optimization is knowing what you can’t optimize.

Cron Jobs: Quarantined

Scheduled jobs — health checks, daily digests, Darwin self-improvement runs — always get fresh sessions. Always. They pass a session_key parameter that signals “I’m a cron job, don’t touch the user’s session.”

session_key=f'cron:{job.id}'  # Forces fresh, no resume

The alternative was nightmarish: a scheduled job hijacking the user’s session mid-conversation, injecting its own context, and handing back a session ID that now includes “by the way, I just ran a health check” in the conversation history. No. Cron jobs get their own sandbox. User sessions stay clean.

The Recovery Design

Here’s what should happen when things go sideways — the designed recovery path, not a tested war story:

Normal startup recovery:

  1. bridge.py lifespan fires load_session() on startup
  2. Session ID loaded from data/session_id.txt
  3. Next user message resumes with --resume <session_id>
  4. Claude picks up where it left off — context intact, personality intact, conversation history intact

Stale session recovery:

  1. --resume fails (nonzero exit code)
  2. System logs the warning, clears the session
  3. Retries once with a fresh system prompt injection
  4. New session ID saved to disk

One retry. No infinite loops. No silent failures. If the fresh attempt also fails, it returns None and the calling code in _run_claude_in_background() sends the user a generic message: 'Timed out or failed to respond.' or, if an exception blew up entirely, 'Something went wrong. Check logs for details.'

That’s… functional. It’s not good UX. The user knows something broke but not what broke or why. There’s no “your session expired, starting fresh” or “Claude’s having a bad day, here’s what I tried.” The retry mechanism works; the user communication around it is still bare-bones. It’s honest failure, but it’s not helpful failure. That’s a gap worth closing.

Why This Matters

The difference between “AI tool” and “AI that works with you” is continuity. It’s the difference between a contractor who shows up every day and asks “what do you do here?” versus one who walks in and says “okay, yesterday we were refactoring the scheduler — want to pick that up?”

--resume costs almost nothing. One text file. One flag. One branching condition. But it transforms the user experience from transactional to conversational.

And the layered context injection — the soul reminders, the memory search, the time-of-day hints — that’s what makes it feel like the system knows things rather than just processes things. The session preserves history. The context wrapper preserves understanding. They’re complementary systems doing different jobs, and the distinction matters.

The conversation refused to die. That’s the whole point.