spec(slice-1): self-review fixes + terminology

- Clarify RtcSession/Channel ownership: ChannelId IS the session id;
  RtcSession owns both str0m Live + codecs and the Channel. Fixes ambiguity.
- Drop the separate /ice endpoint: non-trickle ICE bundles candidates
  into the SDP offer/answer, so the endpoint was redundant. Fixes
  contradiction.
- Clarify idle timeout = 'no RTP for 60s' (not 5 min blanket), and that
  no per-session tokio task is spawned (pre-paves the wrong pattern
  for step 4's dedicated thread).
- 'reflex to police' -> 'reflex to enforce' (terminology: avoid
  authoritarian verbs).
This commit is contained in:
adlee-was-taken
2026-06-28 09:30:55 -04:00
parent 2310367b15
commit 23cec14977

View File

@@ -47,7 +47,7 @@ though it pre-paves the tap by exposing the PCM boundary as a clean trait seam.
| Trickle ICE | When real-world NATs demand it (likely step 5) | Non-trickle (one POST with offer+candidates, one response with answer+candidates) suffices for local loopback and keeps the dev loop zero-dependency. |
| The tap itself (audio routing to an external echo process) | Spearhead step 2 | Slice 1 *pre-paves* the tap by exposing the PCM boundary as `AudioSource`/`AudioSink` traits in `rutster-media`; step 2 implements the WSS tap client behind that seam. |
| The brain (STT/LLM/TTS) | Spearhead step 3 | Slice 1 echoes; step 2 swaps echo for an external process, step 3 swaps echo-process for a real brain. |
| Barge-in / VAD-driven playout kill | Spearhead step 4 | No reflex to police yet; no VAD even on the inbound side. |
| Barge-in / VAD-driven playout kill | Spearhead step 4 | No reflex to enforce yet; no VAD even on the inbound side. |
| PSTN trunk (SIP client) | Spearhead step 5 | WebRTC-only ingress in slice 1; ADR-0003's Rust-native trunk SIP lands with the trunk integration. |
| Spend cap / abuse gate | Spearhead step 6 | No trunk yet to gate spend against. |
| CDR emission, event bus, OTel traces beyond the per-channel `tracing` span | Later rungs | PORT_PLAN keeps these as services *around* the core; slice 1 has one peer, one channel, no fanout needed. |
@@ -243,16 +243,20 @@ policy is match-and-continue).
### 4.1 HTTP surface (slice 1)
- `POST /v1/sessions` → mint a `RtcSession` keyed by a fresh `ChannelId`. Returns
`{ "session_id": "<uuid>" }`.
- `POST /v1/sessions/:id/offer` (body: browser SDP offer) → core produces SDP answer.
Returns the answer as `application/sdp`.
- `POST /v1/sessions/:id/ice` (body: browser ICE candidate, non-trickle) → feed to
str0m. Returns `204 No Content`.
- `POST /v1/sessions` → mint a `RtcSession` (which owns a fresh `Channel`; the
`ChannelId` *is* the session id). Returns `{ "session_id": "<uuid>" }`.
- `POST /v1/sessions/:id/offer` (body: browser SDP offer, including all ICE
candidates — non-trickle) → core produces SDP answer (including its ICE candidates),
feeds candidates to str0m, returns the answer as `application/sdp`.
- `DELETE /v1/sessions/:id` → tear down: transition `Channel` to `Closing → Closed`,
drop the `RtcSession`, close the peer connection cleanly via str0m.
- `GET /` → serve the static HTML test client.
There is **no separate `/ice` endpoint** in slice 1. Non-trickle ICE bundles all
candidates into the SDP offer/answer exchange, so one POST (`/offer`) carries
everything. A separate `/ice` endpoint is a step-5 concern (trickle ICE — see the
out-of-scope table).
### 4.2 ICE strategy
Non-trickle ICE. Browser gathers all candidates, sends offer+candidates in one POST,
@@ -285,8 +289,16 @@ A single self-contained HTML file with inline JS, no build step. Behavior:
### 4.5 Session lifecycle
- Sessions held in an in-process `DashMap<Uuid, RtcSession>` in the binary crate.
- Idle timeout: 5 min hard cutoff. No re-INVITE / renegotiation in slice 1.
- Sessions held in an in-process `DashMap<ChannelId, RtcSession>` in the binary crate.
The `ChannelId` (a UUID newtype from `rutster-call-model`) is the session id surfaced
in the REST API. `RtcSession` owns both the str0m `Live` + codecs and the `Channel`
(signaling state); see §3.1 and §5.
- Idle timeout: 5 min hard cutoff. "Idle" = no RTP packets received from the peer in
the last 60 s (a longer window than the strict "no RTCP" definition, because RTC
quiet periods are normal; no-RTP-for-60s is a real "the browser tab is dead" signal).
Implemented as a per-session deadline checked on each poll cycle; no per-session
tokio task spawned (which would clutter the runtime and pre-pave the wrong pattern
for the dedicated timing thread in step 4).
- Restart on browser refresh: new session. No resumability.
### 4.6 API posture