Skip to content

Voice session reservations (substrate)

Substrate, not an adapter. D41 session reservations are the shared budget lifecycle that LiveKit and Pipecat voice adapters must use. This page does not claim LiveKit or Pipecat adapter coverage; it documents the reservation contract those adapters build on.

Realtime voice agents keep one session open while spend accrues across STT, LLM turns, tool calls, streaming responses, and TTS output. A request-scoped reserve cannot model that lifecycle cleanly. D41 adds a session-scoped ledger hold so a voice adapter can reserve an envelope before the paid session starts, commit positive streaming deltas during the session, and release the remainder when the session ends or expires.

voice room connect
-> ReserveSession # hold session budget before paid provider dispatch
-> CommitSessionDelta # positive idempotent streaming deltas
-> CommitSessionDelta # reconnect replays by streaming_commit_id
-> ReleaseSession # release uncommitted remainder
-> SessionExpire # TTL safety net for crashed sessions

| Path | Status | |---|---| | Session start | ReserveSession creates a ledger hold before a voice adapter connects paid model traffic. | | Streaming spend | CommitSessionDelta applies positive idempotent deltas against the hold. | | Reconnect / replay | Identical replay returns the original outcome; same key with different payload conflicts. | | Overrun | Commit delta beyond the held amount is denied without mutating committed or released balances. | | End / crash | ReleaseSession releases the remainder; TTL expiry releases the remainder if the adapter disappears. |

The hold is not a credit line. Every commit reduces the held remainder and increases committed spend. Release or expiry settles only the uncommitted remainder back to available budget.

The substrate is pinned in the existing sidecar adapter service:

rpc ReserveSession(ReserveSessionRequest) returns (ReserveSessionOutcome)
rpc CommitSessionDelta(CommitSessionDeltaRequest) returns (CommitSessionDeltaOutcome)
rpc ReleaseSession(ReleaseSessionRequest) returns (ReleaseSessionOutcome)

ReserveSessionRequest requires the normal reserve-time tuple from the current SpendGuard substrate: tenant_id, budget_id, window_instance_id, unit, pricing, session_id, route, estimated_amount_atomic, ttl_seconds, and idempotency_key.

Every new adapter using this substrate must thread unitId, windowInstanceId, and the pricing freeze tuple from day one. Ledger commits reject tuple drift.

Current closeout status: the sidecar UDS methods are compile-safe, fail-closed UNIMPLEMENTED stubs. The local D41 demo validates the ledger session lifecycle and canonical audit path directly. A sidecar-to-ledger bridge slice, COV_D41S_06_sidecar_session_bridge, must land before LiveKit or Pipecat adapters call the session RPCs.

Session lifecycle events are written as signed audit decision/outcome pairs and canonicalized through the existing audit path. The event family is:

| Event | Emitted when | |---|---| | spendguard.audit.session.reserve | Session hold accepted. | | spendguard.audit.session.commit_delta | Streaming delta committed. | | spendguard.audit.session.release | Remainder released. | | spendguard.audit.session.expired | TTL sweep released the remainder. | | spendguard.audit.session.denied | Session reserve or overrun denied. |

Reserve DENY returns the full typed outcome to the caller, including current availability, while the signed audit outcome keeps deterministic denial facts that are stable for replay.

Terminal window
make -C deploy/demo demo-down
make -C deploy/demo demo-up DEMO_MODE=session_reservation
make -C deploy/demo demo-verify-session-reservation

Successful runner line:

[demo] session_reservation ALL 7 steps PASS

The demo uses the real Postgres ledger and canonical database. It covers a primary reserve, two positive commits, idempotent replay, conflict detection, overrun denial, release, a real insufficient-budget reserve DENY with replay/conflict checks, and a second reservation that expires through TTL. The hard gates assert exactly eight signed decision/outcome pairs scoped to the three demo session reservation IDs.

LiveKit and Pipecat adapter work should reference the locked D41 substrate design and the sidecar bridge spec instead of inventing a local voice lifecycle. Adapter code is expected to translate framework session/room IDs into session_id, reserve before paid provider work starts, commit positive streaming deltas with stable streaming_commit_id values, and release on session end while relying on TTL for crash recovery.

Runtime adapter work is blocked until the sidecar bridge demo proves the UDS path:

Terminal window
make -C deploy/demo demo-up DEMO_MODE=session_bridge
make -C deploy/demo demo-verify-session-bridge