# Host Architecture Spec — Local-Interactive, Remote-Daemon

Synthesized from one-round consults with Gemini 3.1 Pro and Opus 4.6 on whether to move Console v2's interactive dev surface to the MacBook (raw responses in `host-arch-gemini.md` and `host-arch-opus.md`).

## Verdict

**Shape A: Local-interactive, Remote-daemon.** Both consultants agree, strongly. MacBook runs the Vite + FastAPI dev surface against the Dropbox-synced tree. Studio keeps ttyd Claude sessions, overnight generation, anything that needs to survive a closed laptop.

Latency math: a review session fires ~40 GETs/min. Tailscale RTT on wifi adds 10-30ms per call → 400-1200ms/min of pure network tax for files that already exist locally. Local SSD reads are sub-ms.

## The one disagreement and the call

**Gemini:** symmetric writes — both APIs can mutate, single-user-as-mutex via "MacBook owns the day, Studio owns the night."
**Opus:** read-local / write-remote — MacBook reads locally, proxies all non-GET requests to Studio via a 30-line middleware. "Do not allow symmetric writes."

**Calling it for Opus.** Reasons:

1. Single-user-as-mutex breaks at the day/night boundary. Overnight generation on Studio finishing as JT clicks around on MacBook = exactly the kind of race Gemini's convention can't catch.
2. Dropbox conflict files (`shots/X (1).json`) are silent — the API never serves the conflict variant. JT's mutation disappears. That's a worse failure mode than 30ms on a rare write.
3. The proxy is genuinely simple. ~30 lines of FastAPI middleware, no branching in route handlers.
4. Centralizes "who writes" in code, not human discipline. JT's stated preference ([feedback-architect-preferences.md]) is infrastructure-over-convention.

## Architecture

```
┌────────────────────────────────────────┐         ┌──────────────────────────────────────┐
│ MacBook (Tailscale 100.114.133.97)     │         │ Studio (Tailscale 100.105.59.118)    │
│                                        │         │                                      │
│  Vite :5173                            │         │  FastAPI :8431  ── writes land here ─┤
│       ↓ proxies /api → localhost:8431  │         │     ↑                                │
│  FastAPI :8431  (READ MODE)            │   ─────►│     ttyd :7681+ (Claude sessions)    │
│   ├─ GETs: serve from ~/Dropbox/...    │   write │     └─ launchd-managed, always up    │
│   ├─ POST/PUT/DELETE: proxy to Studio  │   proxy │                                      │
│   └─ Dropbox-watches files locally     │         │     run_overnight.py / EpisodeRunner │
│                                        │         │                                      │
│  Chrome: http://localhost:5173         │         │  (No Vite — retired or kept as      │
│                                        │         │   fallback access point)             │
└────────────────────────────────────────┘         └──────────────────────────────────────┘
                                ▲
                                │
                Dropbox sync (both directions)
                ~/Dropbox/CLAUDE_PROJECTS/recoil/
```

## What changes

### Backend (FastAPI)

1. **New endpoint: `GET /api/config`** — returns `{ttydHost, schemaVersion}` (and any other runtime config the frontend needs). Defaults: `ttydHost = "localhost"` on Studio, `"100.105.59.118"` on MacBook (env-driven).

2. **New middleware: write-proxy.** When `RECOIL_WRITE_UPSTREAM` env var is set, all non-GET requests get forwarded to that URL preserving method/path/body/headers. When unset (Studio's own process), requests fall through to local handlers. Implemented as a Starlette `BaseHTTPMiddleware` in `recoil/api/main.py`. Both consultants estimated ~30 lines.

3. **Backend respects no other change.** Studio's existing LaunchAgent keeps doing its thing. The middleware is opt-in via env var.

### Frontend (Vite + http-adapter)

1. **`TerminalIframe.tsx`** stops using `window.location.hostname` directly. Instead, it reads `ttydHost` from a runtime config provider that fetched `/api/config` on app mount.

2. **Config provider** — a tiny React context or just a `useEffect`-driven state in `App.tsx`. Fetch once, cache in memory, no SWR needed.

3. **No frontend env vars.** Per Opus + Gemini both: ttyd host is runtime, not build-time. Same `pnpm build` artifact works in both modes.

### Local dev startup (MacBook)

1. **`.env.local` on MacBook** in `recoil/`:
   ```
   RECOIL_WRITE_UPSTREAM=http://100.105.59.118:8431
   TTYD_HOST=100.105.59.118
   ```

2. **`scripts/dev-local.sh`** (new) — starts local Vite + local FastAPI with the env vars sourced. Doesn't touch Studio.

3. **No LaunchAgent on MacBook.** Per Opus: "it'll fight you when you want to iterate on the code." Plain `pnpm dev` + `uvicorn` in two terminal panes.

## Gotchas flagged by both consultants

| Gotcha | Mitigation |
|---|---|
| **Partial-sync media** — Studio writes 40MB MP4, MacBook serves before Dropbox finishes | Phase 2: media-route fallback — if local file mtime < 30s old AND size suspicious, proxy to Studio. Phase 1: accept 30s viewability delay. |
| **Dropbox dotfiles** in `projects/` listing | Verify `iterdir()` filters dotfiles (probably already does, but check). |
| **Vite HMR thrashing** from overnight Dropbox syncs | Exclude `projects/` from Vite's watch tree (most likely already excluded, verify). |
| **Python env drift** between MacBook venv and Studio venv | Sync `requirements.txt` religiously. Already in use, low risk. |
| **Absolute paths poisoning** state files | Audit shot JSON serialization for absolute paths (Opus flagged; Gemini flagged). Likely already relative, verify. |

## Phasing

**Phase 1 (today, ~45 min):** Backend `/api/config` endpoint + write-proxy middleware + frontend config provider. Studio behavior unchanged. MacBook can boot a local pair pointing at Studio for writes/ttyd.

**Phase 2 (later, if needed):** Media-route fallback for partial-sync race. Probably only matters during active generation.

**Phase 3 (much later):** Decide whether Studio's Vite/API roles get retired or kept as a fallback access point.

## Concrete first step (Opus's 20-min version, refined)

1. On Studio: nothing changes. Keep API on `:8431`, ttyd on `:7681+`, etc.
2. Add `/api/config` endpoint to FastAPI (~10 lines).
3. Add write-proxy middleware to FastAPI (~30 lines), gated by `RECOIL_WRITE_UPSTREAM`.
4. Frontend: tiny config-fetch in `App.tsx` (or a context), pass `ttydHost` to `TerminalIframe`.
5. On MacBook: set env vars, `cd recoil/console-v2 && pnpm dev`, separately start local FastAPI with `RECOIL_WRITE_UPSTREAM` set.
6. Open `http://localhost:5173`. Reads are local. Writes hit Studio. ttyd iframe still loads from Studio.

Ship it.

## What I'm doing now

Implementing Phase 1. Order:

1. Backend `/api/config` endpoint
2. Backend write-proxy middleware
3. Tests for both (unit + integration)
4. Frontend config provider
5. `TerminalIframe` uses config
6. Local dev script
7. Verify on MacBook
