Skip to content

feat(telemetry): trace the end-of-turn wait and the turn handoff - #7130

Open
davidzhao wants to merge 6 commits into
dz/telemetry-loop-monitorfrom
dz/telemetry-eot-wait
Open

feat(telemetry): trace the end-of-turn wait and the turn handoff#7130
davidzhao wants to merge 6 commits into
dz/telemetry-loop-monitorfrom
dz/telemetry-eot-wait

Conversation

@davidzhao

@davidzhao davidzhao commented Sep 5, 2026

Copy link
Copy Markdown
Member

What

eou_detection only covered the turn-detector inference, so the endpointing delay itself (often seconds) showed up as an empty gap between user_turn and agent_turn. This makes the wait, the user hook, and the speech queue visible.

How

eou_wait span, one per user turn, child of user_turn:

  • back-dated to last_speaking_time, so the bar starts where the user stopped talking;
  • ends on the turn decision with lk.eou.outcome (committed / user_resumed / dropped), lk.eou.wait_duration, lk.eou.rearm_count, the final lk.eou.endpointing_delay, and the last lk.eou.source;
  • a later trigger for the same turn (late STT final, another VAD end of speech) re-arms the wait and records a rearmed event instead of starting a new span, so the bar stays whole and late transcripts are readable off it;
  • a rejected commit (min_words, realtime backchannel) records a not_committed event and keeps waiting; resumed speech ends the span at the VAD/STT speech start; teardown drops it.
  • resumed speech ends the span at the VAD/STT speech start, kept as a user_resumed event; when that start is reported after the fact and a detection or re-arm was recorded in between, the bar runs to the last of those so it always contains its children (an eou_detection still running is closed with a superseded event). A turn-detection mode change to or from manual drops the pending wait.

eou_detection keeps its name and now nests under eou_wait; the span names and the lk.eou.* attribute keys stay on the same vocabulary, so nothing on the cloud side needs to change.

on_user_turn_completed span around the user hook that gates the reply, nested under the user_turn it completes: recognition hands the open user_turn span to the activity when it schedules the hook, and the activity ends it after the hook (or when the turn is skipped), so the turn's duration covers the wait for the hook. Exceptions are recorded redaction-aware, honouring record={"redaction": True} on the session as well as the job's flag; StopResponse is an event.

lk.speech.queue_wait on agent_turn: seconds between scheduling the speech and its first generation authorization.

user_turn
├─ user_speaking
└─ eou_wait                 ← new, 2.5s, rearmed ×1
   └─ eou_detection         ← now nested under the wait
on_user_turn_completed      ← new
agent_turn  (lk.speech.queue_wait=0.01)
├─ llm_node
└─ tts_node

Tests

tests/test_eou_wait_span.py drives _run_eou_detection with real spans: commit (parent, back-dated start, attributes), re-arm keeps one span, resumed speech ends at speech start and leaves user_turn open, teardown drops, not_committed keeps waiting, eou_detection nests under the wait and stays inside it when the user resumes mid-inference, a mode change drops the wait; plus a full fake session asserting eou_wait, the hook span, the queue-wait attribute, and a session-only redaction keeping a hook exception's message out of the trace.

Stacked on #7128.

🤖 Generated with Claude Code

@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from 71e137c to f52117e Compare September 5, 2026 20:07
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 2 times, most recently from e5d040a to dfdc981 Compare September 5, 2026 21:00
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 2 times, most recently from 0126c90 to c248a86 Compare September 5, 2026 21:10
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 3 times, most recently from bae9049 to 3fc7868 Compare September 6, 2026 01:09
@davidzhao
davidzhao marked this pull request as ready for review September 6, 2026 01:40
@davidzhao
davidzhao requested a review from a team as a code owner September 6, 2026 01:41
devin-ai-integration[bot]

This comment was marked as resolved.

@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from f1b1d9f to 1348da7 Compare September 6, 2026 05:23
devin-ai-integration[bot]

This comment was marked as resolved.

@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 6 times, most recently from ea80860 to cc498be Compare September 6, 2026 08:09
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 2 times, most recently from 45988c2 to bfa49c1 Compare September 6, 2026 16:48
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from bfa49c1 to cf9e01f Compare September 6, 2026 17:13
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from cf9e01f to 05c7265 Compare September 6, 2026 21:10
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from 05c7265 to f337d61 Compare September 6, 2026 22:13
davidzhao and others added 6 commits September 6, 2026 15:16
`eou_detection` only covered the turn-detector inference, so the endpointing
delay itself (often seconds) showed up as an empty gap between the user turn
and the agent turn.

- New `eot_wait` span, one per user turn, child of `user_turn`. It is
  back-dated to `last_speaking_time` so the bar starts where the user stopped
  talking, and ends on the turn decision with `lk.eou.outcome` =
  committed | user_resumed | dropped, `lk.eou.wait_duration`,
  `lk.eou.rearm_count`, the final `lk.eou.endpointing_delay`, and the last
  `lk.eou.source`. A later trigger for the same turn (late STT final, another
  VAD end of speech) re-arms the wait and records a `rearmed` event instead of
  starting a new span, so the bar stays whole; a rejected commit (min_words,
  realtime backchannel) records `not_committed` and keeps waiting. Resumed
  speech ends it at the VAD/STT speech start; teardown drops it.
- `eou_detection` is renamed `eot_detection` and nests under `eot_wait`. The
  `lk.eou.*` attribute keys are unchanged.
- New `on_user_turn_completed` span around the user hook that gates the reply,
  parented to the session root; exceptions are recorded (redaction-aware) and
  StopResponse is an event.
- `agent_turn` gains `lk.speech.queue_wait`: seconds between scheduling the
  speech and its first generation authorization, recorded at all four
  authorization sites.

Tests: tests/test_eot_wait_span.py drives `_run_eou_detection` with real spans
(commit, rearm, user resumed, dropped, not committed, detection nesting) and a
full fake session for the hook span and queue wait. Existing AudioRecognition
test harnesses gain the new state fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/test_realtime_reply_chat_ctx.py drives _realtime_reply_task with a
SimpleNamespace standing in for the activity; a method call on self there
raised AttributeError inside the task and the test then waited forever for a
reply future, hanging the unit gate. Reply tasks must not depend on telemetry
helpers living on the activity, so the helper is a module-level function.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lk.eou.wait_duration and the span length came from the same float seconds
but were truncated to nanoseconds separately, so they could differ by a
nanosecond. Keep the start in nanoseconds and compute the attribute from the
same two integers the span is bounded by; the test asserts exact equality.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n is eou_wait

A partial move to "eot" left a span named eot_detection carrying lk.eou.*
attributes. Renaming the keys too would need a cloud-side migration that is
not worth it right now, so both spans stay on the vocabulary the attribute
keys already use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hanges, honour session redaction

Review follow-ups on #7130:
- a resumed-speech end time reported after the fact could land before an
  eou_detection that was still running or a re-arm recorded meanwhile; the
  wait now ends no earlier than what it contains, closes a running detection
  with a superseded event, and keeps the real resume time as a user_resumed
  event.
- switching turn detection to or from manual cancelled the endpointing task
  but left the wait open; it is dropped with the decision.
- on_user_turn_completed exceptions are redacted when redaction is enabled
  for the session alone, not only via the job flag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts under it

The hook gates the reply, so the turn is not over until it has run. Recognition
hands the open user_turn span to the activity when it schedules the hook; the
activity parents the hook span to it and ends it after the hook (or when the
turn is skipped), just before any reply is generated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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