State Program Debugger Implementation Plan
Date: 2026-07-10
Depends on: 2026-07-10-state-program-debugger-evidence-design.md revision 2
Status: execution plan for implementation models; no packet implies production
readiness
Purpose
Turn the design contract into small, independently reviewable work packets. This plan is intentionally explicit enough for lower-cost models to implement mechanical packets without deciding architecture as they go.
The first useful milestone is read-only recorded navigation for Codex, Claude, and the browser. Historical-code execution and state simulation come later and require stronger implementation and security review.
Non-Negotiable Rules
- The Markdown evidence design is authoritative when this plan is silent.
- Do not edit generated business flows to implement debugger behavior.
- Do not put raw PII, secrets, or unredacted caller audio into test fixtures.
- Do not add external tool writes, phone calls, texts, ticket mutations, or network access to simulation.
- Do not infer missing values, timestamps, writers, event ownership, or source revisions. Mark them unavailable.
- Keep recorded navigation and simulation separate in code and JSON.
- The browser, CLI, and API must call the same pure projection engine.
- Each packet lands with its focused tests before the next packet starts.
- Existing
/api/debug/state_traceand/api/debug/session_factsbehavior is preserved until the replacement client is proven. - No production integration or OTA test occurs without the design gates and
explicit human
GO. - Every operator-facing Markdown document has a rendered counterpart under
docs/html/;docs/html/index.htmllinks the current canonical pages and removes or relabels obsolete prototype links.
Documentation Lifecycle
Markdown under docs/design/ or docs/guide/ is canonical. After changing an
operator-facing document, render its HTML counterpart with:
cd docs
../.venv/bin/python ../scripts/render_doc.py <source-relative-to-docs> \
html/<output-name>.html
Every packet checks whether its user-visible behavior changes the evidence design, implementation plan, CLI/API guide, or operator workflow. When it does:
- update the canonical Markdown in the same commit;
- regenerate the complete HTML page, not a hand-maintained excerpt;
- add or update its link and summary in
docs/html/index.html; - remove, relabel, or redirect obsolete index entries so there is one obvious current starting point;
- run
tests/test_docs_index.py,tests/test_docs_toc.py, andtests/test_docs_truth.py.
Do not delete mock pages while tests or review packets still depend on them.
WP6 marks them prototype/obsolete after the hydrated page exists; WP10 removes
their routes and files only after equivalent production tests pass.
Target Module Boundaries
| File | Ownership |
|---|---|
riff/postcall/state_program_artifact.py |
normalize durable artifacts and flow metadata into riff.state_program_debugger/v1 |
riff/state_program_debugger.py |
I/O-free cursor projection, stepping, reverse, breakpoints, hashes, and later simulation commands |
riff/session_slot_ledger.py |
seeded baseline, safe value representation, per-session fingerprints, and mutation helpers |
scripts/state_program_debugger.py |
thin JSON CLI over the artifact builder and pure engine |
riff/web_server.py |
thin authenticated/local HTTP adapter; no projection or simulation logic |
web/state-program-debugger.js |
API client and rendering only |
web/state-program-debugger.html |
production debugger surface; the mock remains a reference until replacement |
docs/guide/state-program-debugger.md |
operator and AI instructions for build, CLI/API stepping, evidence labels, and safety |
docs/html/state-program-debugger.html |
complete rendered operator guide |
docs/html/index.html |
canonical discovery links; obsolete mock/debugger links removed or relabeled |
Do not put hydrator joins in JavaScript or command semantics in HTTP handlers.
Shared Schemas
Seeded Slot Event
New event type:
{
"type": "session_slots_seeded",
"session_id": "v3:example",
"slots": [
{
"slot": "caller_name",
"safe_value": "<str len=5>",
"availability": "redacted",
"value_fingerprint": "hmac-sha256:...",
"writer": "replicant_identify"
}
],
"snapshot_fingerprint": "sha256:...",
"fingerprint_scheme": "session-hmac-sha256/v1"
}
The per-session HMAC key is random, lives only on SessionContext, and is not
persisted. Fingerprints can be compared inside one session but cannot correlate
caller values across sessions. safe_value uses the existing PII-safe slot
representation. Internal routing enums may remain visible under the existing
policy.
Every later slot_write adds old_fingerprint and new_fingerprint using the
same session key. Dictionary-valued changes emit a safe shape plus fingerprint;
they are not skipped. A boundary fingerprint is computed from sorted
slot -> value_fingerprint pairs.
Execution Target
Every command that executes logic uses:
{
"purpose": "recorded_reproduction|candidate_regression|controlled_isolation",
"engine_revision": "git:<immutable-sha>",
"flow_revision": "sha256:<immutable-flow-hash>",
"runtime_lock": "sha256:<dependency-lock-hash>",
"fixture_hash": "sha256:<capsule-fixtures>"
}
Read-only recorded navigation has no execution target because it executes no FSM, resolver, prompt, or tool code.
Work Packet Summary
| Packet | Outcome | Depends on | Suggested model |
|---|---|---|---|
| WP0 | redacted failing fixture and schema tests | none | low-cost/mechanical |
| WP1 | complete flow transition metadata | WP0 | low-cost/mechanical |
| WP2 | strict visit framing and canonical recorded events | WP0-WP1 | medium |
| WP3 | seeded baseline and complete mutation attribution | WP0 | medium; strong review |
| WP4 | read-only artifact and cursor engine | WP1-WP3 | medium |
| WP5 | CLI and local read-only API | WP4 | low-cost/mechanical |
| WP6 | production read-only browser UI | WP4-WP5 | medium frontend |
| WP7 | state capsule and candidate simulation | WP4 | strong model + security review |
| WP8 | historical reproduction runner | WP7 | strongest model + human security review |
| WP9 | bounded pattern runner and regression export | WP7-WP8 | strong model |
| WP10 | promotion review and production integration | all | Fable/Opus adversarial review + human GO |
Lower-cost models should not receive WP7-WP10 as unsupervised implementation tasks. They can implement fixtures, metadata extraction, adapters, and focused tests once the interfaces below are fixed.
Current Progress
- WP0 completed in
e935aced: redacted property-menu boundary fixture and ownership contract tests. - WP1 completed in
e6f553d2: declared legal edges and static slot-reference metadata. - WP2 completed in
e8e8cd6b: deterministic state-visit framing, commit boundaries, explicit visit IDs, late-choice attribution, and quarantine. - WP3 completed: one redacted, source-attributed seed baseline; fingerprinted
writes including dict-valued tool and locked-choice writes; boundary
snapshots; and baseline-plus-write hydration with explicit incompleteness
findings. See
FX-122. Existing uninstrumented direct slot mutations are detected by the boundary fingerprint gate rather than silently represented. - WP4 completed: a pure, read-only cursor engine opens normalized artifacts and provides deterministic visit, variable, exit, evidence, audio-reference, and forward/reverse navigation projections.
- WP5 delivered:
scripts/state_program_debugger.pybuilds and commands retained artifacts;/api/debug/state_programexposes the same engine to loopback browser clients. Additional remote-token route tests remain hardening work, not a Mode 1 blocker. - WP6 in progress: the hydrated browser renders state visits, transcript/event progression, entry/recorded-exit values, grouped findings, retained-WAV play/pause/scrub, deterministic event step/reverse, duplicate playback spans, and shareable issue markers. Canonical static-playback lifecycle is retained for browser and normal Telnyx playback. Streamed model-audio lifecycle, carrier recording/alignment, and S1 simulation remain outstanding.
WP0 - Redacted Boundary Fixture
Goal: create the smallest durable fixture that reproduces the original announce/menu attribution defect without caller PII.
Files:
tests/fixtures/state_program_debugger/property_menu_boundary/manifest.jsontests/fixtures/state_program_debugger/property_menu_boundary/bus_events.jsonltests/fixtures/state_program_debugger/property_menu_boundary/trace.jsonltests/fixtures/state_program_debugger/property_menu_boundary/expected.jsontests/test_state_program_artifact.py
Fixture facts:
menu_full_announce#1enters, pushes/completes audio, evaluatesaudio_done, writes_menu_announced=yes, and exits tomenu.menu#1enters, receives caller inputStatus, resolvesexisting_request, writes_route=existing_request, and exits tocap_existing_request__list_tickets.- At least one duplicate-target edge is present in metadata to prove edge ID, not target name, is the join key.
- Times and sequence numbers are internally consistent but synthetic and clearly fixture-labeled.
Tests written first:
- announce visit contains no
Statusor_routeevent; - menu visit contains both;
- expected JSON contains immutable entry/exit snapshots and all legal edges;
- fixture contains no E.164 number, address, name, token, or raw caller audio.
Do not: copy a production session directory into the repository.
WP1 - Flow Transition Metadata
Goal: make declared legal exits machine-readable.
Files:
- modify
riff/postcall/state_audio_timeline.py::_load_flow_metadata - tests in
tests/test_state_audio_timeline.py
Required state metadata:
{
"transitions": [
{
"edge_id": "menu:1",
"declaration_index": 1,
"to": "cap_existing_request__list_tickets",
"when": "slot_equals:_route:existing_request",
"reason": "caller chose existing_request",
"sets": [],
"clears": []
}
],
"static_slot_references": []
}
edge_id is <state_id>:<zero-based declaration index> and is stable for a
specific flow hash. Preserve declaration order. Never join by target alone.
static_slot_references is a list of {slot, source, detail} rows. Allowed
sources in this packet are required_slot, must_include,
locked_choice_slot, prompt_interpolation, required_tool_arg,
edge_guard, edge_set, and edge_clear. Parse slot
names only from structured fields, {{slots.<name>}} interpolation, and known
guard DSL forms such as slot_equals: and slot_set:. For a custom registered
guard whose reads cannot be derived from its authored string, emit
{source: "custom_guard", slot: null, detail: <guard-name>}; do not inspect
Python source or guess its reads. Runtime guard_evaluated.slots_read supplies
the actual guard-read evidence later.
Definition of done: focused tests cover duplicate targets, sets, clears, reasons, static reference sources, a custom guard with unknown static reads, and a state with no transitions.
WP2 - Visit Framing and Canonical Events
Goal: build strict state visits without variable reconstruction yet.
Files:
- create
riff/postcall/state_program_artifact.py - tests in
tests/test_state_program_artifact.py
Pure entry point:
build_state_program_artifact(
*, manifest: dict, bus_events: list[dict], trace_events: list[dict],
flow_metadata: dict, transcript_events: list[dict], audio_refs: list[dict]
) -> dict
No file reads occur inside this function. A separate thin loader reads session files and calls it.
Rules:
- derive
visit_id=<state>#<ordinal>for legacy events; - prefer explicit visit ID when present;
- attach
transition_committedto the outgoing boundary selected by itsfrom_state/to_state, even when its sequence follows target entry; - attach a late legacy
locked_choice_evaluatedto its source visit only whenfrom_stateandturn_indexidentify exactly one matchingtransition_committed; label the attribution derived and cite both rows; - quarantine conflicts under
unattributed_events; - normalize only the recorded vocabulary from the design;
- preserve source event kind and evidence refs;
- order by
(seq, source_suborder, event_id); - never invent
clock_ms.
Definition of done: WP0 attribution tests pass; re-entry creates distinct visits; a post-exit event cannot appear in the prior visit; hydration is byte-identical on two runs.
WP3 - Seeded Baseline and Mutation Ledger
Goal: make entry/current/exit variable claims reconstructable.
Files:
- create
riff/session_slot_ledger.py - modify
riff/types.pyfor a non-serialized per-session fingerprint key - modify
riff/phone/telnyx_transport.py - modify
riff/phone/server.pyto preserve phone seed-source writers - modify the browser simulator session setup in
riff/live/transport.py - modify
riff/locked_choice.pyso matched-choice bulk writes use the ledger - modify
riff/state_manager.pyandriff/live_turn_controller.pywrite sites - add
session_slots_seededtoriff/state_trace_writer.py - focused tests in
tests/test_session_slot_ledger.pyand phone/simulator tests
API:
seed_session_slots(ctx, sources: list[tuple[str, dict]]) -> dict
slot_write_event(ctx, *, slot, old, new, kind, writer) -> dict | None
snapshot_fingerprint(ctx) -> str
The phone path passes separate sources for prior_call, caller_identity,
replicant_identify, transport, and flow_default; it must not merge them
before attribution. Keep the old constructor argument temporarily for test and
caller compatibility, assigning it writer legacy_initial_slots.
Replace direct baseline slots.update and initial caller/called-number writes
with seed_session_slots before the first state performs work. Do not treat
slot_filled as mutation evidence. Replace the matched locked-choice
session.slots.update(slot_updates) with per-key ledger writes before guard
evaluation; keep the event's current turn semantics and add an explicit
visit_id when available.
Definition of done:
- phone and simulator produce one seeded event before state work;
- every baseline key has a writer and safe value state;
- dictionary-valued tool changes emit a write;
- same-length string changes produce different fingerprints;
- no fingerprint correlates across two sessions with the same raw value;
- fold mismatch yields
completeness=incomplete; - raw PII is absent from durable ledger assertions.
WP4 - Read-Only Artifact and Cursor Engine
Goal: deliver Mode 1 for AI use without simulation.
Files:
- complete
riff/postcall/state_program_artifact.py - create
riff/state_program_debugger.py - tests in
tests/test_state_program_debugger.py
Pure engine API:
open_debugger(artifact: dict) -> DebuggerState
apply_command(state: DebuggerState, command: dict) -> tuple[DebuggerState, dict]
Supported commands: list_visits, inspect_state, seek, step, reverse,
continue_until, inspect_variables, inspect_exits, inspect_evidence, and
fetch_audio_range metadata. The engine returns audio references; it does not
play audio or read a WAV.
Every response includes schema, mode=recorded, old/new cursor, projection,
evidence refs, and determinism hash. Unknown action, stale hash, unavailable
event, and boundary overrun return typed errors without moving the cursor.
Definition of done: forward then reverse returns the original hash; Entry/Exit never change; CLI/API/UI fixture projections can be compared later.
WP5 - JSON CLI and Local Read-Only API
Goal: let Codex and Claude step without a browser.
Files:
- create
scripts/state_program_debugger.py - add thin routes in
riff/web_server.py - tests in
tests/test_state_program_debugger_cli.pyand web API tests
CLI examples:
.venv/bin/python scripts/state_program_debugger.py build <session-dir> --out <artifact.json>
.venv/bin/python scripts/state_program_debugger.py command <artifact.json> \
--json '{"action":"seek","visit_id":"menu#1"}'
The command prints one JSON object to stdout and diagnostics to stderr. It does not start a server, execute a model, or call a tool.
HTTP: add one read-only command endpoint under /api/debug/state_program.
Validate session IDs with the existing strict alphabet. Bind/debug access is
loopback-only by default. If the web server is exposed beyond loopback, require
Authorization: Bearer $RIFF_DEBUG_API_TOKEN; when the token is unset, return
403. Do not add wildcard CORS or return raw slot values.
Definition of done: CLI and API return the same response and determinism
hash for WP0; path traversal, stale hash, missing token, and unsupported
simulation actions are rejected. Create the operator/AI Markdown guide, render
its complete HTML counterpart, link it from docs/html/index.html, and pass the
three docs tests listed in Documentation Lifecycle.
WP6 - Production Read-Only Browser
Goal: replace hardcoded mock data with Mode 1 artifact hydration.
Files:
- create
web/state-program-debugger.html - create
web/state-program-debugger.js - add the page route in
riff/web_server.py - browser tests under
tests/web/
Use the existing mock only as a visual reference. Remove hardcoded visits and browser speech from evidence claims. Render the dedicated conversation lane, state breadcrumb, boundary variables, legal exits, six evidence beats, and recorded/unavailable/redacted labels.
Definition of done: semantic browser tests assert frame ownership and
immutable Entry/Exit; desktop/mobile Playwright screenshots have no overlap;
the page and CLI report the same cursor hash. Update the operator guide and
HTML. Make the hydrated page the primary debugger link in
docs/html/index.html; relabel mock links as prototypes rather than leaving two
apparently canonical debugger entries.
WP7 - State Capsule and Candidate Simulation
Goal: support safe counterfactual runs against an explicitly selected candidate engine and flow.
Status (2026-07-11): the call-time identity/snapshot prerequisite and
riff.state_test_capsule/v1 export are implemented. CLI and session-service/API
exports are byte-identical for the same cursor, and the browser exposes the same
export action. Capsules fail closed on mutable-flow provenance, redacted inputs,
uncorrelated tool previews, unaddressed audio, missing mutable context, and
unfrozen execution policies. Candidate simulation,
historical execution, pattern runs, and browser fork controls remain NO-GO until
the no-network/no-write runner and its security gates land.
Files: chosen only after a strong-model design check of WP0-WP6. Extend the pure engine rather than adding simulation to HTTP or JavaScript.
Required behavior:
- export
riff.state_test_capsule/v1with recorded engine/build/flow identity; - resolve labels to immutable hashes before execution;
- run in a subprocess with network disabled and an explicit time/memory limit;
- permit fixture tool reads only; reject every side-effecting tool;
- record overlays and simulated events in a separate namespace;
- return resolved revisions, runtime lock, fixture hash, and determinism hash.
Definition of done: a candidate run cannot mutate the recorded artifact, filesystem fixtures, network, ticket store, or phone transport. Security review is mandatory.
WP8 - Historical Reproduction Runner
Goal: reproduce recorded Python behavior when the historical engine is available.
Resolve the recorded Git SHA and dependency lock from trusted local artifacts.
Use an isolated detached worktree or immutable container with network disabled.
Never fetch or execute an untrusted revision on demand from the API. If code,
flow, lock, or fixture dependencies are absent, return
HISTORICAL_RUNTIME_UNAVAILABLE; never use HEAD as fallback.
Compare four explicit targets when requested:
- recorded engine + recorded flow;
- candidate engine + recorded flow;
- recorded engine + candidate flow;
- candidate engine + candidate flow.
This packet requires strongest-model implementation, human security review, and tests proving unavailable historical code cannot fall through to current code.
WP9 - Pattern Runner and Regression Export
Goal: let an AI sweep bounded patterns against one state.
Input is JSONL with a schema, unique pattern ID, overlays, caller observations, selected source, DTMF/control, audio outcome, tool fixtures, and execution target. Reject unknown keys. Enforce branch, event, time, and model-budget caps.
Output is JSONL with exact exit edge, variable delta, simulated audio/model
refs, invariant findings, resolved revisions, and comparison to the recorded
branch. export_regression writes a redacted fixture plus a focused failing
test template; it never edits production flows.
Definition of done: the property-menu matrix reproduces the recorded route, finds a transcript-selection counterexample, and exports a deterministic test that fails before the candidate fix and passes after it.
WP10 - Promotion
Run all focused suites, the full relevant regression suite, docs truth checks, security tests, Playwright desktop/mobile screenshots, and the Claude Fable re-review packet. Verify the thirteen design GO criteria and all seventeen acceptance gates.
Promotion states are separate:
DESIGN GO: contract approved.MODE1 GO: recorded AI stepping approved.SIMULATION GO: candidate state forks approved.HISTORICAL GO: recorded-SHA execution approved.PRODUCTION GO: human authorization after all required gates.
Before PRODUCTION GO, audit every debugger entry in docs/html/index.html.
Remove superseded entries and delete obsolete mock files/routes only when no
test, review packet, or operator link references them. Re-render every changed
Markdown page and run the documentation lifecycle tests.
Required Handoff From Every Implementation Model
Each packet result reports:
- packet ID and status;
- commit hash;
- exact files changed;
- commands and focused tests run;
- artifact or fixture paths;
- Markdown/HTML documentation pairs changed and index entries updated/removed;
- unresolved evidence gaps;
- security or privacy impact;
- next packet unblocked.
Do not report a higher promotion state than the packet can prove.