#2537·chat-ui

Turn continuity P4: reaper closure in-band, producer fencing, and guarantee tests

Author: pngwnCreated Aug 28, 2026Updated Aug 28, 2026

Phase P4 of the Turn Continuity plan (#2532 shipped P0–P2): the closure and proof layer — guarantee G5 (no zombies) plus the property tests that keep the whole design honest permanently.

Already covered by #2532's follow-up commits (do not redo)

  • Abandoned parked calls close their turn: turnAbandoned CASes waiting → failed and persists the terminal state into the message.
  • A live sweeper resume renews its claim lease, so a legitimate long resume can no longer be double-claimed.
  • The stream drain skips a permanent event-log hole after a 10s tolerance instead of starving forever.

Remaining work

1. Producer heartbeats into the turn state, reaper closure in-band (G5). The writer already heartbeats the generations row, and the reaper marks stale runs interrupted — but nothing folds that into the turn state. A producer that dies mid-running leaves the message's last in-band state running: isTurnAlive correctly reads the turn dead (subscriptions close), but the client's turnStateOf still renders a spinner until something else refreshes it. The reaper should CAS the stale turn to failed and persist the terminal update into the message, the same shape turnAbandoned uses.

2. Producer fencing on the turn-state upserts. turnRunning / turnWaiting / turnAwaitingInput are unfenced upserts: any producer can stomp the state. The lease renewal removed the known trigger (dueling resumes), but the guarantee should not depend on the trigger being gone — a monotonic producer generation (fencing token) should make a stale producer's writes no-ops.

3. The guarantee tests.

  • Replay property test: for arbitrary disconnect/reconnect points across a scripted multi-park turn, client reconstruction ≡ server log (extends the replayRoundTrip suite).
  • Mid-wait join: a fresh subscriber during a park renders the correct remaining time, with injected clock skew of ± minutes.
  • Chaos: kill the producer mid-run — observers see failed within the reap window, never an eternal spinner.

Done when

All three test families run in CI. These tests are the contract: any future "wait is flaky" report starts by asking which invariant's test failed to catch it.

Design doc: the "Turn Continuity" plan (P4 — Heartbeats, reaping, and proof), summarized in #2532's description.