← RIFF docs · design doc

Session Debug Workspace — TDD implementation plan

Eight bite-sized tasks, each a failing-test-first cycle ending in its own commit. Phase A ships user value with read-side changes only; Phase B makes the turn record canonical.

2026-07-07 · execution source (full test + implementation code per step): docs/superpowers/plans/2026-07-07-session-debug-workspace.md · run tests with .venv/bin/pytest -q

This page is the overview; the .md is the executable plan. Every task in the md contains the actual failing test code, the exact run command with expected failure output, the minimal implementation, and the commit — per the writing-plans discipline. Workers execute the md task-by-task; this page exists so reviewers can hold the shape in one screen.

Global constraints (bind every task)

The ten tasks

TaskCreates / modifiesTest seamProduces (contract for later tasks)
A1Durable slot_write + transition_verdictstate_trace_writer.py filter setinstall_writer(tmp) → emit → assert persisted; noisy type still excludedstate_trace.jsonl carries attribution events as they happen (the durable file alone doesn't change atlas output — Task 1b closes that)
A1bLossless teardown dumpscripts/call_replay.py:300 (dump_session_bus_events)seeded durable trace + rolled ring → merged dump, deduped by (seq, ts, type), other sessions excludedatlas SLOTS/VERDICTS lanes stop truncating on long calls; signature gains optional trace_path, phone-server call site unchanged
A2Pure merge modulenew riff/session_debug.pyplain-dict fixtures, no I/O: turn-window grouping (by turn_index, else turn_complete boundaries; pre-2b live sessions fall back to first turn — stated behavior), B-016 zero-value guard, session isolation, capsbuild_session_debug(turn_rows, trace_events, required) → {turns: [...]} incl. required_slots_in_state; required_by_state(flow)
A2bLive turn_complete emissionriff/events.py helper + one call in live/session.py _record_live_turn_resultsubscribe → emit_turn_complete("s", 4) → assert session_id/turn_index/source on the buslive calls get real turn boundaries for the window grouping (cascade + locked-choice paths already emit; live path didn't)
A3HTTP endpointweb_server.py (+ extracting shared readers from the two existing endpoints)HTTPServer+RiffHandler fixture (pattern: test_state_trace_api.py): 400 / 404 / merged-shape contract; regression guard on refactored endpointsGET /api/debug/session_facts?session_id=X; sources: turns.jsonl + state_trace.jsonl + bus buffer + bus_events.jsonl, deduped by seq
A4flow-studio inspectornew web/session-facts-view.js (pure model); flow-studio.js/.html, flow-studio-replay.js (render only)node-driven contract test on the pure model (incl. volunteered-extra-slot exclusion); Playwright smoke with screenshot on the PRstateFactsModel(stateId, facts, turnIndex) → triad + guards + verdict; triad built ONLY from required_slots_in_state; panel renders from the model only
B5Turn-scoped event collectornew riff/turn_events.pyprivate EventBus: only target session + relevant types collected; unsubscribes on exit; explicit start/close idempotent across async handlersTurnEventCollector(session_id) — explicit start()/close() primary (live opens and closes in different handlers), context-manager sugar for sync callers
B6Dual-source slot diffriff/turn_events.pypure fn: attributed pass-through; direct mutation ⇒ unattributed_diff (redacted); dict-valued slots NOT skipped; same-slot direct mutation AFTER an attributed write caught via final-shape reconciliationreconcile_slot_diff(before, after, events)
B7TurnRecord v2 builderturn_logger.py (extract builder); call sites in live/session.py, web_server.pySimpleNamespace fakes: v1 fields intact + schema_version: 2; enrichments only when inputs supplied; caps; then full suitebuild_turn_record(result, ctx, *, state_snapshot, turn_events, slots_before, kind, run_id); log_turn stays backward compatible. Collector opens at user-turn start — before _on_tool_call can mutate slots mid-stream (both _record_live_turn_result and top-of-_on_turn_complete are too late) — and closes on every completion path incl. locked-choice short circuits
B8Eval sink + tolerant readernew riff/turns_reader.py; flow_eval/driver.py; web_server.pyv1/v2/garbage fixture file; unit test on the extracted _write_eval_turn_record seam ⇒ run-scoped rows, global log untouched, per-turn events not cumulativeread_turn_rows(path, session_id); run_conversation(..., turn_log_dir) slices events[event_mark:] per turn and writes kind="eval" + run_id via the seam

Key decisions baked into the tasks (from the codex consult)

Execution order

Tasks 1→1b→2→2b→3→4 ship Phase A on their own (1b and 2b are the two write-side touches: teardown-dump merge, live boundary emit). Tasks 5→6→7→8 are strictly ordered among themselves but independent of Task 4. After Task 4: use the inspector on real calls for a few days before Phase B — /api/debug/session_facts is the contract TurnRecord v2 must serve.