Three shipped scenarios assert distinct FOB reflex properties per spec section 5.3: - loud-barge.toml: PRIMARY barge-in path (local VAD, 20 loud frames @ 20ms = 400ms) - quiet-advisory.toml: SECONDARY path (sub-VAD-threshold quiet frames; in the slice-4-half standalone-wiring mode this exercises the harness's own latencies rather than real brain ASR-VAD -- the latter deferred to post-spearhead MockRealtimeBrain composition per spec section 8.6) - sustained-call.toml: multi-barge / anti-fatigue check (3 loud bursts across 5 speak cycles; the second + third bar's kill-time should drift <= 1.5x the first's). LEARNING.md gains 5 new slice-4-half concept pointers covering: measurement discipline (the callers clock), post-hoc dedup of noise captures, in-process concurrency sweep, atomic accumulators with compare_exchange_weak, and pub(crate) visibility for the percentile_ms helper used by ConcurrencyRunner's sample-level aggregation (avoiding the interleaved-captures-corrupt-LatencyProbe-pairing problem). Signed-off-by: Aaron D. Lee <himself@adlee.work>
8.9 KiB
LEARNING.md — to learn concept X, read file Y
This index maps a Rust concept you might be learning to the file where
slice 1 makes the concept concrete. Each entry is a worked example you
can read in cargo doc --open plus the source file itself.
Concepts + pointers
-
Newtype pattern (type-safety via single-field wrappers) →
crates/rutster-call-model/src/lib.rs—ChannelId(Uuid). The newtype stops us from mixing up aChannelIdwith some futureSessionIdat the type-system level. Compile-enforced where a comment could only ask. -
enumfor closed state sets + exhaustivematch→crates/rutster-call-model/src/lib.rs—ChannelState(New → Connecting → Connected → Closing → Closed). Exhaustiveness checking forces everymatchto consider each state; adding a state later surfaces every site that needs handling. -
Sans-IO pattern (no I/O inside the library; input via method calls, output via return values) →
crates/rutster-media/src/loop_driver.rs— the str0m poll loop.Rtc::handle_inputtakes a network packet as a struct argument, not from a socket the library owns;poll_outputreturnsTransmitpackets the caller sends. Fully testable without a network — str0m integration tests use this property to drive faster than realtime. -
Trait design for extension points (a futures-compatible seam) →
crates/rutster-media/src/pcm.rs— theAudioSource/AudioSinktraits. Slice 1 wires anEchoAudioPipebetween them; step 2 swaps that for a real WSS tap client without touchingRtcSession. The traits describe what the splice point does, not how it's filled. -
Error enums with
thiserror+ hot-path match-and-continue →crates/rutster-media/src/lib.rs(MediaError) andcrates/rutster-media/src/opus_codec.rs(OpusDecoder::decodereturnsOption<PcmFrame>). Cold path:thiserror-derived enum +?. Hot path: match-and-continue, never?, never panic — "drop + observe, don't crash" (spec §3.8). -
Arc<Mutex<T>>vsArc<RwLock<T>>— when each is right →crates/rutster/src/session_map.rs. TheRtcSessionlives behindArc<Mutex<...>>because every access mutates it (str0m's&mut selfcontract) —RwLock's read-mode would be useless. Comment on the struct explains the trade-off. -
DashMapfor sharded concurrent maps →crates/rutster/src/session_map.rs.DashMapshards its inner map so two handlers operating on differentChannelIds don't contend;HashMapwrapped in a singleMutexwould serialize every access. -
str0m 0.21's single-mutation invariant →
crates/rutster-media/src/loop_driver.rs. Mutate (handle_input / Writer::write) → drainpoll_outputtoOutput::Timeout→ next mutate. Violating this leaves str0m in an inconsistent state. -
tokio graceful shutdown via signal handlers →
crates/rutster/src/main.rs(shutdown_signal). Ctrl-C / SIGTERM drops the AppState; the AppState drops the DashMap; the DashMap drops every RtcSession. No in-flight call preservation in slice 1. -
include_str!for embedding static assets →crates/rutster/src/routes.rs(include_str!("../static/index.html")). The HTML test client is compiled into the binary at build time — no separate file to ship, no disk IO to serve it. -
mpsc+oneshotfor cold-path task supervision →crates/rutster/src/tap_engine.rs— how a spawned tokio task is supervised + cancelled viaoneshot::Receiver. -
VecDequeas a bounded playout ring with drop-oldest policy →crates/rutster-tap/src/tap_audio_pipe.rs— why a manual ring (notmpsc) when the overflow policy is drop-oldest, not drop-newest. -
Async WS connect +
Sink/Streamtraits →crates/rutster-tap/src/tap_client.rs—tokio_tungstenite::connect_async,WebSocketStream, theSinkExt/StreamExtextension traits,tokio::select!over inbound + outbound + close. -
Box<dyn Trait + Send>field widening (the seam test) →crates/rutster-media/src/rtc_session.rs— why thepipefield type changed fromEchoAudioPipetoBox<dyn AudioSource + AudioSink + Send>soloop_driver's call sites are byte-identical (slice-2 §8.5 #6). -
Zero-sized marker newtype for state flags →
crates/rutster-call-model/src/lib.rs—TapHandle(())compilesOption<TapHandle>to a singlebool; no runtime cost for the type-system marker.
Slice 3 — OpenAI Realtime brain
async-traitpatterns / async fns in trait objects →crates/rutster/src/tool_registry.rs— theTooltrait'sasync fn calland howasync-traitlowers it toPin<Box<dyn Future + Send>>(the same boxing the slice-1pipewidening used, now applied to trait methods).- OpenAI Realtime adapter + event translation (pure-function layer) →
crates/rutster-brain-realtime/src/translator.rs— the §4.2 event mapping table as purefns: tapaudio_in⇄ OpenAIinput_audio_buffer.append,response.audio.delta⇄audio_out,function_call_arguments.done⇄function_call. Testable without a network (the pump is in a separate file). - Tap protocol additive extension + forward-compat via
#[serde(other)]→crates/rutster-tap/src/protocol.rs— how slice-3 addsspeech_started,speech_stopped,function_call,function_call_output,tools.updatewithout breaking slice-2 brains: unknown variants deserialize to a catch-all arm instead of erroring (forward-compat by construction). - Side-channel mpsc for FOB-boundary dispatch →
crates/rutster/src/session_map.rs— howdrive_all_sessionsdrains a function-call mpsc side channel into the tap WS writer without blocking the 20 ms media loop. The pattern: hot loop pollstry_recv; cold path spawns. - HTTP request builder for WS subprotocol handshake →
crates/rutster-brain-realtime/src/openai_client.rs— why we hand-roll the WS upgradeRequestinstead of lettingconnect_asyncbuild it: to setAuthorization+OpenAI-Betaheaders on the WS handshake (tungstenite doesn't expose subprotocol/auth headers via the simpleconnect_async(url)entry point — seeopenai_headers/openai_realtime_url).
Slice 4½ — benchmark + simulation harness
- Measurement discipline: the caller's clock →
crates/rutster-sim/src/sim_audio_pipe.rs— theAudioPipetest-double that IS the caller. Both onset + receipt timestamps are captured inside the SimAudioPipe (the wall-clock the caller started speaking + the wall-clock the caller heard the reply). The harness cannot lie about latency because the only clock it uses is the caller's (spec §2.2 — the load-bearing design choice). - Post-hoc p50/p99 computation + dedup of noise captures →
crates/rutster-sim/src/latency.rs— theLatencyProbeconsumesCaptureevents + pairsCallerLoudOnsetwith the nextBargeKillObserved(kill-time) and the nextCallerHeardReply(mouth-to-ear). Captures without a prior onset are silently dropped (the SimAudioPipe captures BargeKillObserved unconditionally on empty ring; the probe is the dedup gate). - In-process concurrency sweep + the doctrine-drift detector →
crates/rutster-sim/src/concurrency.rs— theConcurrencyRunnerspawns NSimCalls at levels [1, 10, 50] (the spearhead-scale envelope) + aggregates per-call latencies. Per spec §2.4: 1 isolates the baseline, 10 is the warm working set, 50 is the saturation point. - Atomic accumulators on the hot path:
compare_exchange_weak→crates/rutster-sim/src/tick_lag.rs—TickLagStatskeeps a per-tick max + overrun counts viaAtomicU64. The CAS loop onmaxis the idiomatic pattern for "atomic max update" in Rust (a single load-then-CAS-on-fail loop, ordering::Relaxed because stat counters don't need cross-thread synchronization ordering). 3 atomic ops per tick (max CAS + conditional fetch_add + unconditional fetch_add) — no Mutex, lock-free hot path. pub(crate)visibility for cross-module helper →crates/rutster-sim/src/latency.rs—percentile_msispub(crate)(notpubnor private). TheConcurrencyRunner(sibling module) needs it to compute p50/p99 over the merged sample across N probes — mergingkill_times()samples per-probe (not concatenatingCapturevectors) avoids the interleaved-captures-corrupt-LatencyProbe-pairing problem (each probe has its ownOption<Instant>pairing cursor; merging captures across probes would interleave their cursors).
How to read
cargo doc --open— every module has a//!doc comment; the doc tree is the high-level map.- Pick a concept above; open the named file. The first occurrence of
each non-obvious pattern has a
//comment explaining why. - Cross-ref back to the spec sections cited inline (
spec §3.8,ADR-0002, etc.) for the architecture-level rationale.