Skip to content

fix(observer): merge split user utterances into same turn - #101

Merged
qdang46 merged 3 commits into
mainfrom
fix/turn-split-merge
Sep 4, 2026
Merged

fix(observer): merge split user utterances into same turn#101
qdang46 merged 3 commits into
mainfrom
fix/turn-split-merge

Conversation

@qdang46

@qdang46 qdang46 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #100. Root-causes issue #98 (latency false readings caused by turn misattribution).

Problem

STT/VAD can split a single user utterance into two transcript.user.final events with a short/backchannel-like agent reply interjected in between. Observer.on_transcript's turn-boundary heuristic only merged consecutive user finals into the same turn when no agent final had landed in between — so the second half was always counted as a new turn.

Side effects:

  • turn_taking_ms (used by the latency asserts in Latency detector misses ~9s gap before follow-up question (ebroad scenario) #98) is measured against the spurious interjection instead of the real answer, producing artificially fast/slow/missing latency samples.
  • EventWriter.turn_metrics() overwrote user_text per turn instead of appending, so the first half of the split utterance was silently dropped from the transcript row the judge/asserts see.

Fix

  • observer.py: extend the same_turn merge to also cover a short agent final (≤6 words) that lands within a 700ms grace window of the prior user final — treat it as an interjection rather than the turn-ending answer, and keep waiting for the real reply. The existing echo/dedupe check runs first so it doesn't change behavior for legitimate short agent acks.
  • event_writer.py (and the Rust mirror in lks-core/src/logging/event.rs): turn_metrics() now concatenates user_text across same_turn merges instead of overwriting it.
  • asserts.rs: unrelated drive-by fix — cargo build -p lks-core was broken on main by a duplicate negate field in OutcomeExpect introduced by the Feat/negate transcript assert #97 merge; fixed so the Rust changes above could actually be verified to compile.

Tests

  • Added test_split_utterance_with_short_agent_interjection_stays_same_turn and test_new_turn_starts_after_a_real_agent_answer to tests/test_observer.py.
  • uv run pytest -q → 682 passed.
  • cargo build -p lks-core and cargo test -p lks-core --lib → pass.

STT/VAD can split a single user utterance into two finals with a
short/backchannel-like agent reply interjected in between (e.g. a
premature partial answer). The turn-boundary heuristic in
Observer.on_transcript only merged consecutive user finals when NO
agent final had landed yet, so the second half was counted as a brand
new turn, corrupting turn_taking_ms (issue #98's root cause) and
making transcript.turn_metrics() drop the first half of the utterance
entirely (event_writer overwrote user_text instead of appending).

- observer.py: extend the same_turn merge to also cover a short agent
  final (<=6 words) landing within a short grace window (700ms) of the
  prior user final — treat it as an interjection, not the turn-ending
  answer, and keep waiting for the real reply.
- event_writer.py / lks-core event.rs: turn_metrics() now concatenates
  user_text across same_turn merges instead of overwriting it, in both
  the Python and Rust implementations.
- asserts.rs: drop-in fix for a pre-existing duplicate `negate` field
  in OutcomeExpect (introduced by the #97 merge) that broke the
  lks-core build entirely; unrelated to this bug but blocked
  verifying the Rust changes above.

Tests: added test_split_utterance_with_short_agent_interjection_stays_same_turn
and test_new_turn_starts_after_a_real_agent_answer to tests/test_observer.py.
Full python suite (682 tests) and `cargo build -p lks-core` pass.
qdang46 pushed a commit that referenced this pull request Sep 4, 2026
…uild

Same pre-existing main-branch build break as in #101 (introduced by the
#97 merge); needed here too since this branch was cut from main before
that fix landed.
The #97 merge (negate/OutcomeExpect) left a duplicated test-function
block and duplicated struct-literal fields in
crates/lks-core/tests/asserts.rs (mirrors the duplicate-field bug in
asserts.rs itself, fixed in the prior commit) — this broke
`cargo test -p lks-core` with E0428/E0062. Removed the duplicate
outcome_transcript_contains_negate_{pass,fail}_* functions and the
duplicate negate: false lines.
@qdang46
qdang46 merged commit 63bc522 into main Sep 4, 2026
7 checks passed
@qdang46
qdang46 deleted the fix/turn-split-merge branch September 4, 2026 09:06
qdang46 added a commit that referenced this pull request Sep 4, 2026
…) (#102)

* fix(evals): ground judge verdicts to reduce hallucination false-pass (#99)

The LLM-judge path (llm_bool/goals_met/constraint_respected assert
types + judge_run) let a criterion's met=true stand even when the
judge cited no transcript evidence for it, and unconditionally
promoted a fail→pass whenever the model self-labeled the failing
criteria as "irrelevant" — neither self-label is independently
verified. Together these let a topically-related-but-wrong agent
reply (e.g. "I'm ready to hear more about your company..." instead of
re-asking for the missing company name) slip through as a pass.

- evals/types.py::parse_judgment_payload: when a criterion is reported
  met=True with blank/missing evidence, flip it to met=False and mark
  the whole judgment needs_human_review — an ungrounded "met" claim is
  not trustworthy.
- evals/relevancy.py::apply_relevancy: keep the fail→pass promotion
  when all relevant criteria are met (usually correct), but flag
  needs_human_review instead of trusting the model's own
  relevant=False self-label blindly.

Tests: added grounding/promotion coverage to tests/test_evals_judge.py.
Full suite (uv run pytest -q) → 683 passed.

* fix(rust): drop-in fix for duplicate negate field breaking lks-core build

Same pre-existing main-branch build break as in #101 (introduced by the
#97 merge); needed here too since this branch was cut from main before
that fix landed.

* fix(rust): dedupe botched merge-conflict remnants in asserts.rs tests

Same pre-existing #97 merge damage as on fix/turn-split-merge — this
branch was cut from main before that fix landed.

---------

Co-authored-by: quangdang46 <tranquangdang21@gmail.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.

Split-utterance turns not detected when agent interjects between the two halves (English)

2 participants