Repository navigation
feat(telemetry): trace the end-of-turn wait and the turn handoff - #7130
Merged
Merged
Conversation
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 5, 2026 20:07
71e137c to
f52117e
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
2 times, most recently
from
September 5, 2026 21:00
e5d040a to
dfdc981
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
2 times, most recently
from
September 5, 2026 21:10
0126c90 to
c248a86
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
3 times, most recently
from
September 6, 2026 01:09
bae9049 to
3fc7868
Compare
davidzhao
marked this pull request as ready for review
September 6, 2026 01:40
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 6, 2026 05:23
f1b1d9f to
1348da7
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
6 times, most recently
from
September 6, 2026 08:09
ea80860 to
cc498be
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
2 times, most recently
from
September 6, 2026 16:48
45988c2 to
bfa49c1
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 6, 2026 17:13
bfa49c1 to
cf9e01f
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 6, 2026 21:10
cf9e01f to
05c7265
Compare
jmcclanahan
approved these changes
Sep 9, 2026
jmcclanahan
left a comment
Contributor
There was a problem hiding this comment.
Couple QoL things for downstream, otherwise lgtm
| self._turn_detector_flushed = True | ||
|
|
||
| elif eou_wait_span.is_recording(): | ||
| eou_wait_span.add_event("not_committed") |
Contributor
There was a problem hiding this comment.
Could this also bump a counter that lands as lk.eou.not_committed_count next to lk.eou.rearm_count? Rate of rejected turn decisions is a fleet-level tuning signal, and events need the events array scanned, while an attribute is a cheap read.
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 9, 2026 05:51
b02f1bd to
74fc258
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 9, 2026 05:55
74fc258 to
cad53f6
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
3 times, most recently
from
September 9, 2026 07:37
6e8f532 to
327e89d
Compare
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
2 times, most recently
from
September 9, 2026 07:43
82d347a to
a176fad
Compare
chenghao-mou
approved these changes
Sep 10, 2026
`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>
…sk; rename to _end_user_turn_span The explicit call after the hook duplicated the task's finally. The NOTE about AgentActivity's visibility goes back above the class it describes. Comments trimmed to what a reader of the code needs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_realtime_reply_task recorded it before _realtime_generation_task had opened the turn's span, so the attribute landed on whatever span was current. The generation task records it; the helper refuses to fall back to the current span. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Set only after detection, it was lost on a wait that resumed speech had already ended. The delay in force is stamped at creation and re-arm; a prediction that raises it overwrites while the span is still open. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…an, lazy user_turn starts at its anchor - lk.eou.not_committed_count on eou_wait replaces the not_committed event; lk.eou.resume_count on user_turn counts the waits the user cut short - the reply's agent_turn carries lk.end_of_turn_delay, lk.transcription_delay and lk.on_user_turn_completed_delay next to lk.e2e_latency - a user_turn created from an STT final with no VAD start starts at the earliest known anchor, so the back-dated eou_wait child fits inside it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
davidzhao
force-pushed
the
dz/telemetry-eot-wait
branch
from
September 11, 2026 22:06
a8af38e to
796e9b4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
eou_detectiononly covered the turn-detector inference, so the endpointing delay itself (often seconds) showed up as an empty gap betweenuser_turnandagent_turn. This makes the wait, the user hook, and the speech queue visible.How
eou_waitspan, one per user turn, child ofuser_turn:last_speaking_time, so the bar starts where the user stopped talking;lk.eou.outcome(committed/user_resumed/dropped),lk.eou.wait_duration,lk.eou.rearm_count, the finallk.eou.endpointing_delay, and the lastlk.eou.source;rearmedevent instead of starting a new span, so the bar stays whole and late transcripts are readable off it;min_words, realtime backchannel) records alk.eou.not_committed_countand keeps waiting; resumed speech ends the span at the VAD/STT speech start; teardown drops it.user_resumedevent; 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 (aneou_detectionstill running is closed with asupersededevent). A turn-detection mode change to or frommanualdrops the pending wait.eou_detectionkeeps its name and now nests undereou_wait; the span names and thelk.eou.*attribute keys stay on the same vocabulary, so nothing on the cloud side needs to change.on_user_turn_completedspan around the user hook that gates the reply, nested under theuser_turnit completes: recognition hands the openuser_turnspan 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, honouringrecord={"redaction": True}on the session as well as the job's flag;StopResponseis an event.lk.speech.queue_waitonagent_turn: seconds between scheduling the speech and its first generation authorization.Tests
tests/test_eou_wait_span.pydrives_run_eou_detectionwith real spans: commit (parent, back-dated start, attributes), re-arm keeps one span, resumed speech ends at speech start and leavesuser_turnopen, teardown drops,not_committedkeeps waiting,eou_detectionnests under the wait and stays inside it when the user resumes mid-inference, a mode change drops the wait; plus a full fake session assertingeou_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
lk.eou.not_committed_countoneou_wait(turn decisions the detector rejected before the wait ended) replaces thenot_committedevent;lk.eou.resume_countonuser_turncounts the waits the user cut short by speaking again.agent_turncarrieslk.end_of_turn_delay,lk.transcription_delayandlk.on_user_turn_completed_delaynext tolk.e2e_latency, so a per-turn breakdown reads off one span.user_turncreated lazily from an STT final (no VAD opened it) starts at the earliest anchor known, so its back-datedeou_waitchild never starts before it.