Skip to content

fix(mixed): raise CSRC_INACTIVE_MS 400 → 800 ms, measured on live Teams - #1374

Open
jfw-ppi wants to merge 2 commits into
Vexa-ai:mainfrom
jfw-ppi:csrc-inactive-800ms
Open

jfw-ppi wants to merge 2 commits into
Vexa-ai:mainfrom
jfw-ppi:csrc-inactive-800ms

Conversation

@jfw-ppi

@jfw-ppi jfw-ppi commented Aug 31, 2026

Copy link
Copy Markdown

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

  • Independent: I created this contribution, or otherwise have the right to submit it under Apache-2.0, and it is not owned or controlled by an employer, client, or other entity.
  • Employer/client authorization required
  • Unsure

Commits are DCO-signed.

What and why

CSRC_INACTIVE_MS moves 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:

inactiveMs synthesized deactivations segment quality
400 ms 10 one speech phase fragments into segments as short as 0.1 s
800 ms 7 reconstructed segments match the fixture's real structure

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 inactiveMs remains configurable per poller for callers that want the old value.

Side observation from the same capture, relevant to anyone weighing audioLevel heuristics: on Chrome, audioLevel was 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.tsCSRC_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

  1. Composed latency: with the sensor at 800 ms, CSRC_INACTIVE_MS + CSRC_HYSTERESIS_MS rises 1.0 s → 1.4 s worst-case before CsrcTurnSource closes a turn. Since the CSRC path is diagnostic-only today (capture-bridge.ts marks it NOT wired to __vexaSpeakerHint), nothing user-visible shifts — but if/when the transport becomes the turn spine, you may want to re-tune CSRC_HYSTERESIS_MS (env-overridable) against the new baseline. Happy to run that measurement on our Teams tapes if useful.
  2. Overlap analysis: our tape counted deactivation fragmentation only; a contested-turn/overlap rate at 400 vs 800 ms was not measured. The inactiveMs option 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.d no-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.

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>
@github-actions

Copy link
Copy Markdown

👋 Thanks for opening your first PR to Vexa, @jfw-ppi!

Highly recommended (not required): hop into our Discord
and tell us in a sentence or two what this change does and why. It helps us review
your value bundle faster and connects you with the reporter and maintainers.

Your PR is judged on its evidence — the observation bundle + the diff — not on whether you
show up. But showing up makes the whole thing smoother. See
the delivery guide for how a PR reaches merge.

@github-actions

Copy link
Copy Markdown

🃏 Merge card — #1374

check what it needs
Value missing state: value-signed (the value sign-off)
Diff no non-author approval on the current head sha (a new push dismisses a stale approval)

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +137 to +141
* 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. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant