slice-5 (rented transport, FOB half): G711Codec + TrunkSession + trunk_driver + MediaLeg + reflex-on-trunk verification (T1/T3/T4/T5/T7/T8) #19

Merged
alee merged 9 commits from slice-5/rented-transport-dev-c into main 2026-07-05 16:15:11 +00:00
Owner

What lands

  • G711Codec (in-core µ-law + 8kHz↔24kHz linear-interpolated resampling; const fn tables; no dep)
  • TwilioMediaStreamsServer (axum WSS handler; JSON envelope parse; base64 µ-law decode)
  • TrunkSession + trunk_driver::drive (parallels loop_driver minus str0m; REUSES slice-4 Reflex + LocalVadReflex verbatim per spec §6.1)
  • MediaCmd::RegisterTrunk + MediaLeg enum + run_per_leg_tick dispatch (WebRTC code path UNCHANGED)
  • POST /v1/trunk/sessions + POST /v1/trunk/webhook routes + TwilioMediaStreamsServer router mount
  • T7: reflex-on-trunk verification test (unit test; proves slice-4 decorators compose symmetrically)
  • T8: PSTN sim e2e (1 active integration test + 1 #[ignore] full-e2e stub; the stub is blocked on AppState/spawn_tap_engine not being accessible from rutster-trunk crate — a real architectural constraint documented in the test)

ADR-0007 honored

Zero SIP bytes parsed. Wire surface: JSON (Media Streams) + RTP (WebRTC unchanged) + HTTP/REST (green-zone).

Seam invariant

crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} byte-identical. trunk_driver::drive lives in crates/rutster-trunk/src/loop_driver.rs (separate file, separate crate, parallel-titled by design).

ADR-0009 honored

TwilioCredentials lives in crates/rutster-trunk/src/provider/ (dev-b's T2/T6 territory, already merged via PR #17). trunk_driver + TrunkSession never reference TwilioCredentials directly.

Merge instructions

  • rebase-merge, NOT squash — shares lib.rs edits with dev-b's PR #17 (stacked branches)
  • DCO signoff every commit
  • after merge: maintainer runs live Twilio e2e validation manually
## What lands - G711Codec (in-core µ-law + 8kHz↔24kHz linear-interpolated resampling; const fn tables; no dep) - TwilioMediaStreamsServer (axum WSS handler; JSON envelope parse; base64 µ-law decode) - TrunkSession + trunk_driver::drive (parallels loop_driver minus str0m; REUSES slice-4 Reflex<TapAudioPipe> + LocalVadReflex verbatim per spec §6.1) - MediaCmd::RegisterTrunk + MediaLeg enum + run_per_leg_tick dispatch (WebRTC code path UNCHANGED) - POST /v1/trunk/sessions + POST /v1/trunk/webhook routes + TwilioMediaStreamsServer router mount - T7: reflex-on-trunk verification test (unit test; proves slice-4 decorators compose symmetrically) - T8: PSTN sim e2e (1 active integration test + 1 #[ignore] full-e2e stub; the stub is blocked on AppState/spawn_tap_engine not being accessible from rutster-trunk crate — a real architectural constraint documented in the test) ## ADR-0007 honored Zero SIP bytes parsed. Wire surface: JSON (Media Streams) + RTP (WebRTC unchanged) + HTTP/REST (green-zone). ## Seam invariant crates/rutster-media/src/{loop_driver.rs,rtc_session.rs} byte-identical. trunk_driver::drive lives in crates/rutster-trunk/src/loop_driver.rs (separate file, separate crate, parallel-titled by design). ## ADR-0009 honored TwilioCredentials lives in crates/rutster-trunk/src/provider/ (dev-b's T2/T6 territory, already merged via PR #17). trunk_driver + TrunkSession never reference TwilioCredentials directly. ## Merge instructions - rebase-merge, NOT squash — shares lib.rs edits with dev-b's PR #17 (stacked branches) - DCO signoff every commit - after merge: maintainer runs live Twilio e2e validation manually
alee added 9 commits 2026-07-05 16:14:49 +00:00
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>
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>
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>
Signed-off-by: Aaron D. Lee <himself@adlee.work>
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>
test(trunk): reflex-on-trunk verification (T7) + PSTN sim e2e (T8) — slice-5
Some checks failed
CI / clippy (pull_request) Successful in 2m25s
CI / test (1.85) (pull_request) Successful in 6m0s
CI / fmt (pull_request) Failing after 14m2s
CI / test (stable) (pull_request) Successful in 6m6s
CI / deny (pull_request) Failing after 1m32s
CI / twilio-live (manual only) (pull_request) Has been skipped
0a76cec1df
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>
alee force-pushed slice-5/rented-transport-dev-c from d5c00897e8 to 0a76cec1df 2026-07-05 16:14:49 +00:00 Compare
alee merged commit 03465a4a07 into main 2026-07-05 16:15:11 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/rutster#19