# Console v2 — Morning Read (2026-05-11)

You went to bed at ~2:30am asking me to walk through it, audit, consult Gemini + Opus, and iterate. This is the synthesis. Companion files in this folder:
- `walkthrough.md` — what I saw clicking around
- `gemini-r1-raw.md` — Gemini's round 1 (raw)
- `opus-r2-raw.md` — Opus reviewing Gemini's R1 (raw)

## The headline

**Opus is right, Gemini overshot.** Gemini's answer ("you must persist CP-7 Takes to disk before anything else works") was directionally good but framed as a big rewrite. Opus pushed back: you already have most of the persistence you need (`run_overnight.py`, `EpisodeRunner`, `BudgetGuard`, scene JSON files, ops_log with crash detection). The actual gap is **one new field** in your existing shot JSON schema: `parent_take_id`.

If you add `parent_take_id` to the shot JSON schema and populate it at generation time, you unblock:
- **Topic 1 (lineage):** the adapter becomes a 20-line change instead of a CP-7 rewrite. Walk parents recursively for the chain. Done.
- **Topic 3 (overnight loop):** take identity survives a FastAPI restart. The remaining work is wiring `EpisodeRunner` to the EventBus and exposing `/api/runs/active` + `/api/runs/{run_id}`. Three concrete plumbing tasks, not greenfield.

**Topic 2 (MCP) is independent** — it doesn't depend on persistence at all. Opus caught another Gemini error here: Gemini proposed injecting `<Ctrl-U>/tool get_selection<Enter>` into the ttyd websocket because "Claude CLI is deaf between prompts." Wrong — Claude Code already handles MCP `notifications/resources/list_changed`, which is exactly how the Workspace MCP server signals viewer state changes today. No keystroke injection needed.

## JT's first move (recommended)

**Half-day:** Add `parent_take_id` to shot JSON.
- Schema: add the field to `Take.to_dict()` and the beats adapter write path.
- Generation: set it at take creation. For i2v takes, parent is the keyframe take. For t2v/t2i, parent is null (root of chain).
- Backfill: write a script that infers parent links from existing receipts where possible; leave null otherwise.
- Update `get_lineage(beat_id, take_id)` to filter to the chain rooted at the given take. Walk via `parent_take_id` recursively.

**In parallel (day-ish):** Embed the Claude MCP iframe in Console v2.
- Opus caught that the ttyd routes exist in FastAPI but the frontend doesn't actually mount them. *(Note: that's wrong per tonight's work — ChatColumn DOES mount TerminalIframe and it works. But there's no MCP server wired into the spawned Claude. So the spawning is done; MCP is the unbuilt piece.)*
- Write `console_mcp_shim.py` — stdio JSON-RPC ↔ HTTP to FastAPI `/api/mcp/...` routes.
- Configure the spawned `claude` to use it via `claude --mcp-server console-shim ...` or per-project `.claude/mcp.json`.
- Wire React selection state (`focusedProjectId` / `focused` / `selectedTakeId`) to POST `/api/selection/current` on change.
- Server emits MCP `notifications/resources/list_changed` to Claude Code; Claude can call `get_current_selection()` whenever it wants context.

**The bigger lift (multi-day):** Overnight run + monitoring surface.
- This is the closest-to-revenue item but it's wiring on top of existing infra, not greenfield.
- Three concrete tasks Opus identified:
  1. `EpisodeRunner` emit lifecycle events to the EventBus (scene/beat/take start/end/fail). ~50 lines.
  2. New API endpoints: `/api/runs/active`, `/api/runs/{run_id}`, `/api/runs/{run_id}/digest`.
  3. Persist `BudgetGuard.spent` to the run record so it survives restart.
- Then build the UI: Queue template currently exists but renders a fixture; needs SSE subscription + digest rendering.

## What today's session actually fixed

For context — the previous session was a UI integration bug parade. Eleven bugs, all squashed:

1. `chat.css` never imported anywhere → chat panel had no styles. Fixed.
2. `--fg-3`/`--fg-4` design tokens at sub-WCAG contrast. Fixed (now ~5:1 and ~3:1).
3. ttyd "not installed" 500 — launchd PATH missing `/opt/homebrew/bin`. Fixed in launcher.sh.
4. `TerminalIframe` hardcoded `localhost`. Fixed to `window.location.hostname`.
5. ttyd `--index` flag served only custom HTML, no xterm.js renderer. Flag dropped.
6. ProposalTray + ChatColumn ttyd-start fired with `"—"` placeholder. Gated on real project.
7. App.tsx fired `getLineage` for non-beat focus IDs. Gated on `takes.length > 0`.
8. `/api/system-status` returned 503 by design, Chrome logged every 5s poll. Changed to 200 with degraded payload.
9. Lineage output nodes had no `url`. Backend now stamps `/api/media/{slug}/{path}`.
10. Vite port-bumping (5174/5175/5176 zombies). `strictPort: true` + `/devserver` skill.
11. 71 stale paths in Tartarus shot JSONs. Reconciliation script written + applied.

Strategic upshot from the session: you almost pivoted to extending Workspace. You ended up making the case for finishing Console v2 yourself, with the three-item list above. That list still stands. Opus refined the order; Gemini's headline "persist first" was actionable but overstated.

## Bugs I caught this overnight (in priority order)

| # | Bug | Severity | Where | Notes |
|---|-----|----------|-------|-------|
| O-1 | `test_system_status_returns_503_until_workers_wired` failing — I broke it tonight when I changed the endpoint to return 200 | **CRITICAL → FIXED** | `recoil/api/tests/test_system_status.py` | Updated test to match new contract. All 178 backend tests pass now. |
| O-2 | `JADE_FACE_FILTER_PROBE_2026_05_07.json` has `episode_id: ""` → adapter falls back, parses `output/video/ep_001/...` from `output_path`, returns lowercase `ep_001` → phantom episode in tree | REAL | `projects/tartarus/state/visual/shots/JADE_FACE_FILTER_PROBE_2026_05_07.json` | Two options: (a) archive the file (it's a probe per JT's "tombstone retired probes" rule), or (b) normalize case in `_derive_episode_id` so `ep_001` → `EP001`. (a) is cleaner. |
| O-3 | Status pill flickers to `disconnected` on single failed poll | REAL | `console-v2/.../lib/use_system_status.ts` | Add N=2 or 3 consecutive-fail threshold before flipping. |
| O-4 | 15s `_capture_session_id` blocks ttyd port return → "Starting Claude…" lingers even after ttyd binds | REAL (UX) | `recoil/api/ttyd_routes.py` | Return port immediately on bind; capture session_id in background; emit it as an SSE event when found. |
| O-5 | Bottom bay shows `110 fails / 5 warns` — count includes synthesized-state fallbacks dressed as errors | POLISH | `api/eventbus.py` filter | Either demote synthesized fallbacks to `info`, or filter bay display to severity≥warning. |
| O-6 | Project rows render trailing em-dash `▾tartarus—` with no value | POLISH | `console-v2/.../nav/NavRow.tsx` or KeeperGlyph | Hide when `count+total = 0`. |
| O-7 | 9_16 fallback warning fires noisily on initial mount | POLISH | `console-v2/.../App.tsx:330` | Downgrade to `console.debug` or gate on dev flag. |
| O-8 | 5 empty `.catch(() => {})` swallow promise rejections silently | POLISH | App.tsx (3), ChatColumn.tsx (1), ContextBar.tsx (1) | Most are intentional (cleanup, 404=expected). At minimum, log to `console.debug` for diagnosis. |

Baseline at end of overnight: TypeScript clean, 92/92 frontend tests, **178/178** backend tests (was 177 before I fixed O-1). No regressions introduced.

## What I didn't get to

- The Chrome extension dropped mid-walkthrough, so I never tested: the actual Lineage view with a beat selected, the 9:16 vs 16:9 inspector layout, keyboard navigation, the Queue/Memory/Events templates with real data.
- The background "code audit" sub-agent I dispatched stalled at the 30-min watchdog and produced no output. I did the audit manually but it's narrower than the agent would've been.
- Did not run `/engine-check` — you asked about engine correctness at one point. Worth doing once you wake up: `cd recoil && /engine-check`.

## Order of operations if you only have a few hours tomorrow

1. **Decide:** is the `parent_take_id` half-day worth doing alone, or are you batching it with the MCP shim? My read is batch them — they're both small enough that doing them together gives you Topic 1 AND Topic 2 in a day.
2. **Quick wins worth slotting in:** O-2 (archive the JADE probe file → ep_001 phantom disappears) and O-3 (consecutive-fail threshold on status pill → no more red flicker).
3. **The overnight loop / monitoring surface** is the real ship. Once `parent_take_id` and MCP are live, this is the next sustained effort. Plan for it as a 3-5 day push.

Files I touched tonight (verify in `git diff` before deciding anything):
- `recoil/api/tests/test_system_status.py` — updated to match new 200-with-degraded-payload contract (this was a regression I caused earlier; now fixed).

Nothing else was modified. Walkthrough + audit + consults were read-only.
