A. Room lifecycle (总管 review items A-1/A-2): the single cleanup owner now watches
ACTIVE session deadlines as well as debts - a session nobody touches expires into
the ledger and has its room really deleted with zero further requests (old code:
_cleanup_loop only scanned debts, expired rooms were never deleted until some
request ran _prune). Explicit end and the background sweep share ONE persisted
attempt/age cap and abandoned state - _sessions no longer bypasses it (the
manager measured 2 background attempts + 3 more interactive deletes = 5); an
abandoned debt raises CleanupAbandoned -> 503 with room/attempts/reason instead
of pretending done-404, success clears both _sessions and _debts, restart
continues the same persisted cap, close/cancel leaves no dangling coroutine, and
only this instance's own rooms are ever touched. New wake event makes a fresh
debt retry immediately instead of waiting out a deadline sleep.
B. Queued speech turns bind EXACTLY (总管 interleaving note_handle(H1)->H2->
note_stream(S1)->S2 gave H1=None, H2='turn-one'): CyberGrillLLM.chat now reads
the SDK's own agents_speech_handle ContextVar (livekit-agents 1.8.8 sets it in
_create_speech_task around every speech pipeline; source-verified on .12 and
asserted at runtime) so each ChatApiLLMStream carries speech_handle/speech_id of
the very handle whose generation created it. TurnLedger binds only on that exact
attribution - a newer speech NEVER retires an older pending handle, say/proactive
generations bind nobody, missing attribution fails closed (no ack, no subtitle
turn), interrupt/failure discards only its own binding.
C. Versioned subtitle contract v2 for the visual author's wiring card (HEL-898):
every cg.subtitle payload gains {v:2, seq} (session-monotonic), turn identity
(speech_id, turn_id, same-source message_id for proactive rows) and explicit
lifecycle events - turn_start/assistant_interim/assistant_final/turn_end/
turn_cancel(reason) plus proactive_start/proactive_end/proactive_cancel that
ride the say SpeechHandle instead of the receipt branch. Accumulators are
isolated per speech_id (a late delta after cancel is dropped producer-side);
seq + speech_id give the frontend two independent stale-rejection rules. Full
executable contract: docs/voice-subtitle-contract-v2.md with recorded samples in
docs/rework/hel879/subtitle-samples/ (scripts/gen_subtitle_samples_hel879.py).
web/src untouched.
Verification: the .12 real-SDK check now LOADS THE ACTUAL WORKER MODULE and drives
the callbacks IT registered on a real AgentSession (no hand-copied functions) -
both-queued delayed generations bind exactly, a say sandwiched between user turns
steals nothing, unattributed generations fail closed, interrupted turns cancel
without final/receipt (11/11 ALL PASS, source-level proof of the ContextVar wiring
included; evidence docs/rework/hel879/sdk-check-hel879-remaining.log). The old r2
lifespan test flake is root-caused (clock freeze and block were two separate portal
calls; a 10ms life tick could slip between them and claim the frozen day unblocked)
and fixed atomically without raising timeouts or dropping assertions - 12/12 solo,
full suite 350/350 three times in a row on .venv311; ruff back to the single
pre-existing error; voice chain 8/8; release offline R1-R7 with zero failures;
requirement checks 4/4 (origin/main and c121c42, each with/without handoff).
Zero real LLM/embed/ASR/TTS calls, zero downloads/installs, nothing deployed or
merged, real caps stay 0.
Co-authored-by: multica-agent <github@multica.ai>