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

9 Commits

Author SHA1 Message Date
0a76cec1df 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
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>
2026-07-05 12:14:43 -04:00
a72825359c feat(binary): MediaLeg enum + MediaCmd::RegisterTrunk variant + POST /v1/trunk/{sessions,webhook} routes + TwilioMediaStreamsServer router mount (slice-5 T5 Step A)
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>
2026-07-05 12:11:45 -04:00
ba7d2939cd fix(trunk): trait imports + remove unused (AudioSink/AudioSource) in session tests (T4 cleanup)
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 12:10:58 -04:00
e94caf52bb fix(trunk): trait imports (AudioSink, AudioSource) for trunk_driver + clippy let_and_return on test helper (slice-5 T4)
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 12:10:58 -04:00
caf4c5ba91 feat(trunk): TrunkSession + trunk_driver::drive -- FOB-side trunk-leg tick (slice-5 T4)
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>
2026-07-05 12:10:58 -04:00
c7d48559a5 fmt(trunk): rustfmt reflow on twilio_media_streams (T3)
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 12:09:48 -04:00
bf47634e3b feat(trunk): TwilioMediaStreamsServer -- axum WSS handler (slice-5 T3)
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>
2026-07-05 12:09:48 -04:00
56f7da577b feat(trunk): TwilioMediaStreamsServer skeleton (slice-5 T3 placeholder commit)
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 12:09:48 -04:00
bcd775747a feat(trunk): G711Codec — µ-law encode/decode + 8kHz↔24kHz linear-interpolated resampling (slice-5 T1)
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>
2026-07-05 12:09:48 -04:00