T7: proves slice-4's Reflex<TapAudioPipe> + LocalVadReflex decorates the
trunk leg's TapAudioPipe identically; barge-in fires on PSTN caller speech
through the same state machine as WebRTC caller speech.
T8: MockRealtimeBrain + BrainShim drives a synthetic PSTN caller through the
FOB reflex loop end-to-end: loud PCM -> local VAD trips -> barge kills ->
brain reply -> un-mute -> idle timeout (caller hangup) closes the session.
Dev-dependencies added to rutster-trunk/Cargo.toml so the integration tests
reach MockRealtimeBrain, futures-util, and tokio-tungstenite without pulling
FOB source into the trunk crate.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Step A -- minimal-viable T5 surface (no ThreadSession refactor):
* MediaLeg enum { WebRTC(RtcSession), Trunk(TrunkSession<TapAudioPipe>) }
+ as_webrtc_mut + channel_id + channel_state + is_closed + set_tap_handle
helpers (dead_code-suppressed per AGENTS.md with documented rationale;
Step B will wire them).
* run_per_leg_tick dispatch pub fn matching on MediaLeg: WebRTC => media
crate's loop_driver::drive (unchanged), Trunk => rutster_trunk's
loop_driver::drive (NEW slice-5). The seam gate holds: trunk leg never
enters the media crate's loop_driver.rs.
* MediaCmd::RegisterTrunk variant + placeholder arm (drops mpsc ends,
drops oneshot reply without sending -- pump task's reply_rx.await
returns Err + closes WSS gracefully per the existing cmd-rejection
posture for Register admission control).
* Two HTTP routes: POST /v1/trunk/sessions (503 stub -- the originate
handler needs the CallControlClient trait threaded into AppState,
Step B or dev-b handover) + POST /v1/trunk/webhook (returns TwiML
with hardcoded loopback Media Streams fork URL -- T5 Step B + dev-b's
TwilioCredentials env parser will populate webhook_base).
* main.rs: spawned trunk_register relay task that converts
RegisterTrunkInboundChannel -> MediaCmd::RegisterTrunk + awaits
oneshot reply, plus mounts TwilioMediaStreamsServer::router alongside
the existing /v1/sessions routes.
Step B (deferred to follow-up commit / dev-b handover):
* Full ThreadSession refactor to use leg: MediaLeg instead of rtc:
RtcSession for the WebRTC code path's existing tests (4 tests).
* Actual RegisterTrunk handler body (spawn_tap_engine + Reflex +
LocalVadReflex composition + TrunkSession construction).
* ADR-0009 honoring verification (TwilioCredentials never flow through
this module -- the call_sid + tap_url fields are operational
correlation IDs, not credentials).
* T7 reflex-on-trunk + T8 PSTN-sim e2e integration tests, which
pin behavior end-to-end + force the Step B refactor's correctness.
ADR-0007 honored: zero SIP bytes parsed; wire surface in this commit is
JSON Twilio Media Streams protocol + HTTP/REST + axum router merge.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The trunk leg's tick function. Parallels crates/rutster-media/src/loop_driver.rs
minus the str0m/Opus/RTP machinery -- there is no RTP to decode, no Opus to
encode, no str0m poll loop to drain. Caller->FOB direction is a pure mpsc
drain; FOB->caller direction is a mpsc push.
Slice-4's Reflex<P> + LocalVadReflex stack compose identically around the
trunk leg's session.pipe -- proving the FOB reflex loop is ingress-agnostic
(spec §2.3 -- the architecture's load-bearing claim). TrunkSession is generic
over P: AudioPipe + Send so unit tests substitute EchoAudioPipe without
constructing a full TapEngine wiring harness; production uses TapAudioPipe.
The seam gate holds: crates/rutster-media/src/{loop_driver.rs,rtc_session.rs}
stay byte-identical because the trunk leg NEVER enters that code path. The
MediaThread dispatches via the new MediaLeg enum (T5).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Accepts Twilio's inbound WSS connections; parses the JSON envelope
(connected/start/media/stop per Twilio's documented protocol); decodes
base64 µ-law via G711Codec; ferries decoded PCM frames to a per-call
inbound mpsc. Concurrently drains the outbound mpsc + sends back JSON
media frames. Same tokio/std-thread split as slice-2's TapEngine: tokio
owns IO; the std thread owns the 20ms tick via trunk_driver::drive (T4).
ADR-0009 honored: TwilioCredentials never reach this module -- only
per-call CallSid (operational log correlation only) + tap_url (operator
configured brain WS URL) flow through the RegisterTrunkInboundChannel.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
In-core ~30-line table-driven codec (no dep). The ITU-T G.711 µ-law
companding formula is a piece of telephony history worth teaching (AGENTS.md
learner-facing comment mandate). 3× linear upsample on decode; 3× decimation
downsample on encode. The resampler artifacts are below the barge-in trigger
threshold (LocalVadReflex only needs RMS energy); rubato lands in a post-
spearhead refinement if a downstream consumer needs better (spec §6.6).
Task T1 of slice-5 — T3 (TwilioMediaStreamsServer) consumes this codec.
Signed-off-by: Aaron D. Lee <himself@adlee.work>