spec(slice-4): epoch is load-bearing, not forward-compat
All checks were successful
All checks were successful
Propagate the 2026-07-01 two-sender revision (local VAD primary + brain advisory secondary) into the epoch-rationale sections. §2.1, §3.2, §6.1, §6.4 previously described barge_epoch as a forward-compat seam for a deferred local-VAD wrapper; with LocalVadReflex in-scope this slice, the epoch disambiguates the local-VAD primary trigger from the slower brain advisory landing on the same event (~300 ms later). Refs slice-4 spec §3.2, §6.1. DCO-signed. Signed-off-by: opencode controller <controller@local>
This commit is contained in:
@@ -131,12 +131,15 @@ applies the state machine and delegates to `inner.next_pcm_frame()` per the tabl
|
||||
|
||||
The reflex owns three pieces of state:
|
||||
- `advisory_rx: mpsc::Receiver<AdvisoryEvent>` — drained sync-non-blocking via `try_recv`
|
||||
on the 20 ms tick before delegating to `inner`. Fed by the TapEngine task over tokio mpsc.
|
||||
on the 20 ms tick before delegating to `inner`. Fed by TWO senders on the same channel:
|
||||
the outer `LocalVadReflex` (local VAD, the primary trigger) and the TapEngine task (the
|
||||
brain's advisory, secondary/confirmation) — both over tokio mpsc.
|
||||
- `muted: bool` — the kill state. `next_pcm_frame` returns `None` while muted, *unless* the
|
||||
inner returns `Some` (the resume condition — the first fresh `audio_out` clears mute).
|
||||
- `barge_epoch: u64` — incremented on every `SpeechStarted`. Not strictly required for the
|
||||
advisory-only MVP (the flush + drain makes the resume race-free), but it's the seam for a
|
||||
future local-VAD wrapper that could race the advisory. Documented as forward-compatible.
|
||||
- `barge_epoch: u64` — incremented on every `SpeechStarted`. Load-bearing this slice: the
|
||||
local VAD (primary) and the brain's advisory (secondary) can both fire on the same barge,
|
||||
and the epoch distinguishes a genuine re-barge from the slower confirmation landing on the
|
||||
same event (§3.2). The flush + drain keeps resume race-free regardless.
|
||||
|
||||
### 2.2 The dedicated media thread
|
||||
|
||||
@@ -241,10 +244,12 @@ un-mutes too early, brain's audio overlaps caller's next word). The resume condi
|
||||
"the brain has yielded and started a new response," which is provably signaled by the first
|
||||
`audio_out` frame after the barge — not by the caller's silence.
|
||||
|
||||
**Why `epoch`:** not strictly needed for advisory-only (MVP), but it's the seam for the
|
||||
local-VAD backstop (deferred per §1.2). A future `LocalVadReflex` wrapper racing the
|
||||
advisory would need to disambiguate "is this barge a re-barge of the same event or a new
|
||||
one" — the epoch is the disambiguator. Forward-compatible.
|
||||
**Why `epoch`:** with two concurrent trigger sources this slice — the local VAD (primary)
|
||||
and the brain's advisory (secondary/confirmation, §6.1) — the epoch disambiguates "is this
|
||||
barge a re-barge of the same event, or a new one" when both race into the same advisory
|
||||
channel. The local VAD trips first; the brain's slower ASR-grade advisory lands ~300 ms
|
||||
later on the *same* event and must not be counted as a fresh barge. The epoch is that
|
||||
disambiguator — load-bearing now, not a forward-compat seam.
|
||||
|
||||
### 3.3 `AudioPipe` trait extension
|
||||
|
||||
@@ -487,7 +492,7 @@ impl<P: AudioPipe> AudioPipe for Reflex<P> {
|
||||
}
|
||||
|
||||
fn barge_in_flush(&mut self) {
|
||||
// Allow outer wrappers (future local-VadReflex) to barge the inner.
|
||||
// Allow the outer `LocalVadReflex` (primary trigger) to barge the inner.
|
||||
self.inner.barge_in_flush()
|
||||
}
|
||||
}
|
||||
@@ -710,15 +715,15 @@ from §6.4, which was originally specced as deferred to "when local VAD arrives.
|
||||
|
||||
### 6.4 Why `Reflex<P>` as a wrapper (not inline in `TapAudioPipe`)
|
||||
|
||||
- Composition: a future `LocalVadReflex<P>` composes outside the advisory `Reflex<P>`, the
|
||||
same way `Reflex<TapAudioPipe>` composes today. The pattern (decorator over `AudioPipe`) is
|
||||
forward-compatible without restructuring.
|
||||
- Composition: `LocalVadReflex<P>` composes outside the advisory `Reflex<P>` (§6.1), the
|
||||
same way `Reflex<TapAudioPipe>` composes. The pattern (decorator over `AudioPipe`) stacks
|
||||
the two trigger sources without restructuring either.
|
||||
- The seam: `loop_driver.rs` byte-identical (still calls `pipe.next_pcm_frame()`). If the
|
||||
reflex lived inline in `TapAudioPipe`, the binary-side wiring would still change but the
|
||||
`TapAudioPipe` module itself would grow the reflex state — less isolated.
|
||||
- YAGNI caveated: the wrapper is the right abstraction for advisory-only because there's
|
||||
exactly one reflex. When local VAD arrives, the wrapper pattern pays off; the spec does
|
||||
not pre-empt that by collapsing the wrapper now.
|
||||
- The payoff is realized this slice, not deferred: two stacked reflexes (local-VAD primary +
|
||||
advisory secondary) live as independent, separately-testable decorators rather than one
|
||||
module's commingled state. Keeping them as wrappers is what makes that separation free.
|
||||
|
||||
### 6.5 Why `barge_in_flush` on `AudioPipe` (not just `clear_playout_ring`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user