slice-4 (dev-a): Reflex<P> + LocalVadReflex<P> (Task 2 + 2b) #8

Merged
alee merged 4 commits from slice-4-dev-a-reflex into main 2026-07-03 13:50:42 +00:00
Owner

What lands

  • Task 2: Reflex<P> barge-in state machine (4-state machine per spec §3.2 — Playing/SpeechStarted→Muted; Muted/SpeechStarted re-barge; Muted/Playing SpeechStopped observation no-toggle; Muted inner Some → Playing; Muted inner None → Muted + frames_suppressed).
  • Task 2b: LocalVadReflex<P> — the wedge-#1 PRIMARY trigger (spec §3.4). RMS/energy VAD in on_pcm_frame on the dedicated thread, in the 20ms loop, with ZERO brain round-trip. Debounce N=3 frames = 60ms. Composes as LocalVadReflex<Reflex<TapAudioPipe>>; both the local VAD + the brain's advisory feed the same advisory_tx mpsc (§6.1 two-sender composition).

What this proves (so far)

The decorator pattern + the trigger source are landed. Combined with dev-b's chain (Task 5 signature + Task 4 advisory forwarding), the slice-4 e2e (Task 9) proves wedge #1: caller speech → kill within ≤80ms wallclock, zero brain round-trip.

What is deferred from this PR

  • Task 6 (MediaThread dedicated std::thread) — blocked on dev-b Task 5 (spawn_tap_engine signature change accepting advisory_tx).
  • Task 7 (session_map.rs rewire to command-channel pattern) — blocked on Task 6.
  • Task 9 Step 1 (the PRIMARY-path e2e proof) — PM-assigned; flagged writable now without dev-b's chain but waiting on PM clearance (QUESTION posted to relay).
  • Task 10 (CI seam gate) — PM-assigned.

Why partial

The plan's dependency graph makes Task 6 strictly depend on dev-b's Task 5 signature change. Shipping Tasks 2 + 2b now locks the decorator + trigger patterns for review while the thread-chain waits on the dependency. Lane-stays-clear: dev-b's chain is the critical path through Task 5 → Task 6.

Merge instructions

  • squash-merge
  • DCO signoff on every commit (AGENTS.md)
  • Reviewer: PM (per kickoff). Tasks 2 + 2b both passed task-scoped reviews (spec , quality Approved). 2 Minor findings recorded for final whole-branch review (both brief-inherited; trivial follow-ups).

Test evidence

  • cargo test -p rutster-media --lib reflex::tests: 15 passing (3 Task 1 + 6 Task 2 + 6 Task 2b).
  • cargo fmt --all --check: pass
  • cargo clippy --all --all-targets -- -D warnings: pass
  • cargo test --all: 108 passing across all test bins
  • Seam gate: loop_driver.rs + rtc_session.rs verified byte-identical to main via git diff origin/main -- ... (empty).
## What lands - Task 2: `Reflex<P>` barge-in state machine (4-state machine per spec §3.2 — Playing/SpeechStarted→Muted; Muted/SpeechStarted re-barge; Muted/Playing SpeechStopped observation no-toggle; Muted inner Some → Playing; Muted inner None → Muted + frames_suppressed). - Task 2b: `LocalVadReflex<P>` — the wedge-#1 PRIMARY trigger (spec §3.4). RMS/energy VAD in `on_pcm_frame` on the dedicated thread, in the 20ms loop, with ZERO brain round-trip. Debounce N=3 frames = 60ms. Composes as `LocalVadReflex<Reflex<TapAudioPipe>>`; both the local VAD + the brain's advisory feed the same `advisory_tx` mpsc (§6.1 two-sender composition). ## What this proves (so far) The decorator pattern + the trigger source are landed. Combined with dev-b's chain (Task 5 signature + Task 4 advisory forwarding), the slice-4 e2e (Task 9) proves wedge #1: caller speech → kill within ≤80ms wallclock, zero brain round-trip. ## What is deferred from this PR - Task 6 (`MediaThread` dedicated std::thread) — blocked on dev-b Task 5 (`spawn_tap_engine` signature change accepting `advisory_tx`). - Task 7 (`session_map.rs` rewire to command-channel pattern) — blocked on Task 6. - Task 9 Step 1 (the PRIMARY-path e2e proof) — PM-assigned; flagged writable now without dev-b's chain but waiting on PM clearance (QUESTION posted to relay). - Task 10 (CI seam gate) — PM-assigned. ## Why partial The plan's dependency graph makes Task 6 strictly depend on dev-b's Task 5 signature change. Shipping Tasks 2 + 2b now locks the decorator + trigger patterns for review while the thread-chain waits on the dependency. Lane-stays-clear: dev-b's chain is the critical path through Task 5 → Task 6. ## Merge instructions - squash-merge - DCO signoff on every commit (AGENTS.md) - Reviewer: PM (per kickoff). Tasks 2 + 2b both passed task-scoped reviews (spec ✅, quality Approved). 2 Minor findings recorded for final whole-branch review (both brief-inherited; trivial follow-ups). ## Test evidence - `cargo test -p rutster-media --lib reflex::tests`: 15 passing (3 Task 1 + 6 Task 2 + 6 Task 2b). - `cargo fmt --all --check`: pass - `cargo clippy --all --all-targets -- -D warnings`: pass - `cargo test --all`: 108 passing across all test bins - Seam gate: `loop_driver.rs` + `rtc_session.rs` verified byte-identical to main via `git diff origin/main -- ...` (empty).
alee added 4 commits 2026-07-03 13:23:50 +00:00
Reflex<P> owns a tokio::sync::mpsc::Receiver<AdvisoryEvent> (the advisory
channel the TapEngine feeds from its tokio task), so tokio must be a
runtime dep of rutster-media at lib-compile time, not dev-only. The
crate itself makes no tokio runtime calls — the dedicated media thread
drives Reflex, not an executor (ARCHITECTURE.md; slice-1 spec §3.4).

Signed-off-by: Aaron D. Lee <himself@adlee.work>
The decorator that instruments any AudioPipe with turn-taking reflexes.
SpeechStarted -> muted=true + barge_in_flush + epoch bump. First fresh
audio_out -> un-mute. SpeechStopped is observational (no toggle). Inbound
audio (on_pcm_frame) is NEVER gated — the brain still hears the caller
during barge; the FOB only kills its OWN playout. loop_driver +
rtc_session byte-identical (seam holds).

barge_epoch is load-bearing THIS slice (spec §6.1, commit 86b7460) — the
local VAD (Task 2b) fires ~0 ms after caller speech; the brain's slower
ASR advisory fires ~300 ms later on the SAME barge; the epoch
disambiguates a fresh re-barge from the late confirmation.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
Fix 1: removes a Task-1 forward-reference comment in reflex.rs that
contradicted itself once Task 2 added the imports it claimed were
"intentionally absent."

Fix 2: MockPipe::on_pcm_frame was a no-op, so Test 6 could not
distinguish "delegated to inner" from "dropped on the floor." Add
last_inbound_frame: Option<PcmFrame> capture + assert delegation so
the reflex's inbound-passthrough guarantee is verified, not assumed.

Addresses the two Important findings from the Task 2 review.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
feat(media): LocalVadReflex — primary barge-in, zero brain round-trip
All checks were successful
CI / fmt (pull_request) Successful in 1m50s
CI / clippy (pull_request) Successful in 2m58s
CI / test (1.85) (pull_request) Successful in 4m17s
CI / test (stable) (pull_request) Successful in 6m41s
CI / deny (pull_request) Successful in 1m32s
58410675ea
The wedge-#1 proof. RMS/energy VAD in on_pcm_frame on the dedicated
thread, in the 20ms loop — caller speech trips SpeechStarted locally,
without any brain round-trip. Debounce (N=3 frames = 60ms) filters
transients. Composes as LocalVadReflex<Reflex<TapAudioPipe>>; both
the local VAD + the brain's advisory feed the same advisory_tx mpsc
(spec §6.1). Revised after adversarial review (initial brainstorming
was advisory-only, which contradicts ARCHITECTURE.md:79-81; wedge-#1
mandate at README:98-100). Spec §3.4 (VAD trigger) + §6.4 (wrapper-
not-inline) load-bearing.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
alee merged commit bc7e8f1acd into main 2026-07-03 13:50:42 +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#8