Conversation
Replaying the transition logic over a 599-sample tape of a live Microsoft Teams meeting (6 s speech / 7 s silence cycle, captured 2026-08-20) gives 10 deactivations at 400 ms — one speech phase fragments into segments as short as 0.1 s — against 7 at 800 ms, where the reconstructed segments match the fixture's real structure. Teams' mixer pauses inside a turn for longer than one packet-train, so 400 ms synthesizes deactivations for turns that are still open, and every false edge is a turn boundary the downstream namer has to un-learn. 800 ms still closes a finished turn well inside the UA's own multi-second 'recently contributed' retention, and inactiveMs stays configurable for callers that want the old value. Test timings that encoded the 400 ms default move accordingly (500 ms -> 900 ms staleness steps); no logic changes. Signed-off-by: Jacob Weinhold <29459386+jfw-ppi@users.noreply.github.com>
|
👋 Thanks for opening your first PR to Vexa, @jfw-ppi! Highly recommended (not required): hop into our Discord Your PR is judged on its evidence — the observation bundle + the diff — not on whether you |
🃏 Merge card — #1374
Not mergeable yet — every row above must be accepted before merge (choke point 1). Fill in what's ❌ above, then this clears automatically. How a PR reaches merge: the merge bar. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9efc5e6a1f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * fragmenting one speech phase into segments as short as 0.1 s — against 7 at 800 ms, where the | ||
| * reconstructed segments match the fixture's real structure. Teams' mixer pauses inside a turn | ||
| * for longer than one packet-train, so 400 ms closes turns that are still open. */ | ||
| export const CSRC_INACTIVE_MS = 800; |
There was a problem hiding this comment.
Preserve the mixed-lane handoff grace
Keep the combined delay unchanged for the non-Teams consumers of this shared default. startCaptureBridge supplies these transitions for every mixed platform, while createBotPipeline routes Zoom/Jitsi through ChunkedTranscriber/CsrcTurnSource, which adds its own 600 ms hysteresis. When A stops and B starts within the new 800 ms sensor window, A has no pending close when B's activation arrives, so the state machine opens a contested turn; B's first roughly 800 ms is therefore emitted as unattributed even though the speakers did not overlap (previously roughly 400 ms). Keep the global 400 ms/default total grace, or make 800 a Teams-only caller option, rather than globally extending this producer delay.
AGENTS.md reference: AGENTS.md:L156-L161
Useful? React with 👍 / 👎.
| * Measured on a live Microsoft Teams meeting (2026-08-20): replaying this transition logic over a | ||
| * 599-sample tape of a 6 s speech / 7 s silence cycle yields 10 deactivations at 400 ms — | ||
| * fragmenting one speech phase into segments as short as 0.1 s — against 7 at 800 ms, where the | ||
| * reconstructed segments match the fixture's real structure. Teams' mixer pauses inside a turn | ||
| * for longer than one packet-train, so 400 ms closes turns that are still open. */ |
There was a problem hiding this comment.
Move capture history out of source
The date, sample count, 400→800 comparison, and “real structure” assertion are a capture's historical evidence rather than the enduring contract for this exported constant. Leaving them here will make the source stale and unverifiable on the next tuning change; record/replay evidence belongs in an evaluation fixture or delivery artifact, and this comment should retain only the present behavioral rationale.
AGENTS.md reference: AGENTS.md:L165-L166
Useful? React with 👍 / 👎.
…tions, two stale comments
Adversarial review of this branch found what the first commit's own claim
('the three test timings that encoded the default') missed:
- csrc-capture.boundary.test.ts slept 900ms over a real browser — a 500ms
cushion above the old 400ms window that the bump silently cut to 100ms,
one poll tick, in the test most exposed to scheduler jitter. Now 1300ms.
- The unit suite's timing checks were vacuous as regression guards: a
revert to 400ms failed only the pinned-constant literal. A boundary
straddle (still open at 750ms staleness, deactivated at 850ms) makes the
behavior itself catch it — verified by mutation: 4 checks fail at 400ms
now, 1 before.
- turn-source.ts documented its hysteresis as the second grace over a
hardcoded '400ms' first grace; corrected to CSRC_INACTIVE_MS/800ms and
the combined ~1.4s worst-case close latency is now stated where the
tuning decision lives.
- chunked-transcriber.ts's trailing-pad rationale de-numericized the same
stale 400ms.
Signed-off-by: Jacob Weinhold <29459386+jfw-ppi@users.noreply.github.com>
A measurement-backed default change to the CSRC sensor's inactivity window, from running your mixed-capture lane against live Microsoft Teams meetings downstream
Contribution rights
Commits are DCO-signed.
What and why
CSRC_INACTIVE_MSmoves from 400 to 800 ms. Measured, not guessed: replaying the transition logic over a 599-sample tape of a live Teams meeting (a 6 s speech / 7 s silence cycle, captured 2026-08-20) yields:Teams' mixer pauses inside a turn for longer than one packet-train, so at 400 ms the sensor synthesizes deactivations for turns that are still open — and each false edge is a turn boundary the downstream namer has to un-learn. 800 ms still closes a finished turn well inside the UA's own multi-second "recently contributed" retention, and
inactiveMsremains configurable per poller for callers that want the old value.Side observation from the same capture, relevant to anyone weighing
audioLevelheuristics: on Chrome,audioLevelwas absent in 1200 of 1200 observations — not merely optional in practice, but never present.Changes
mixed-capture-core/src/csrc-poll.ts— the constant (with the measurement recorded in its doc comment) and the option's doc line.mixed-capture-core/src/csrc-poll.test.ts— the timing steps that encoded 400 ms, plus a boundary straddle (still open at 750 ms staleness, deactivated at 850 ms) so the behavior itself — not only the pinned constant — catches a regression. Verified by mutation: reverting the constant to 400 now fails 3 checks (2 behavioral), previously only the pinned literal.services/bot/src/csrc-capture.boundary.test.ts— the fourth timing the first commit missed: the real-browser test slept 900 ms, a cushion the bump silently cut from 500 ms to 100 ms (one poll tick) over Playwright scheduling jitter. Now 1300 ms, comment corrected.mixed-pipeline/src/turn-source.ts—CSRC_HYSTERESIS_MS's doc called the sensor's grace "400ms" (now stale); corrected, and the combined worst-case close latency (~1.4 s at the defaults, up from ~1.0 s) is stated where the tuning decision lives.mixed-pipeline/src/chunked-transcriber.ts— the same stale 400 ms in the trailing-pad rationale, de-numericized.For the maintainers — two open points, honestly stated
CSRC_INACTIVE_MS + CSRC_HYSTERESIS_MSrises 1.0 s → 1.4 s worst-case beforeCsrcTurnSourcecloses a turn. Since the CSRC path is diagnostic-only today (capture-bridge.tsmarks it NOT wired to__vexaSpeakerHint), nothing user-visible shifts — but if/when the transport becomes the turn spine, you may want to re-tuneCSRC_HYSTERESIS_MS(env-overridable) against the new baseline. Happy to run that measurement on our Teams tapes if useful.inactiveMsoption keeps the old behavior reachable if you want to hold the default pending that.docs: no changelog fragment — the CSRC sensor is diagnostic-only (not wired to transcript output), per the
changelog.dno-user-visible-effect carve-out. Say the word if you'd rather have one anyway.Happy to adjust shape (e.g. keep 400 and change only the bot's call site) if you'd rather hold the sensor's default — the measurement is the contribution either way.