Skip to content

fix(agents): anchor active-turn timer to authoritative start time#2033

Open
wpfleger96 wants to merge 1 commit into
mainfrom
duncan/turn-timer-liveness-anchor
Open

fix(agents): anchor active-turn timer to authoritative start time#2033
wpfleger96 wants to merge 1 commit into
mainfrom
duncan/turn-timer-liveness-anchor

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 17, 2026

Copy link
Copy Markdown
Member

The desktop active-turn badge next to the channel name was resetting to 0s repeatedly while agents were still actively working, even though the observer feed showed continuous thinking/tool-call activity.

Root causes:

  • Turn liveness only started after session setup (the final prompt call), not at turn_started. Session creation and context fetches can themselves take longer than the desktop's bounded prune pause, so the badge appeared to reset during that window.
  • Observer frames carried no authoritative turn-start timestamp. When a liveness/ACP frame recovered a turn no longer in the live map (reconnect, resurrection), the badge re-anchored to the recovery frame's timestamp instead of the turn's true start.
  • turn_error terminal events carried no turn_id, so they couldn't reliably close out the turn they belonged to.
  • Prune-pause was gated on activity across ALL agents globally, so one agent's frame-stream gap could be masked or unmasked by unrelated agents' activity, and had no bounded backstop for a genuinely dead turn.

Changes:

  • run_prompt_task now starts the liveness background task at turn_started (via a LivenessGuard that aborts on drop) and stamps every observer frame for the turn with an authoritative started_at.
  • PromptResult carries turn_id, threaded into emit_turn_error so error terminal events target the correct turn.
  • resurrectTurn uses the frame's startedAt when present and not later than the frame itself, preserving true elapsed time across recovery; falls back to the recovery timestamp otherwise.
  • shouldPausePrune is now scoped per agent instead of globally, with a bounded 3-minute cap so a genuinely dead turn still prunes.

Liveness-spawn deviation from plan: liveness now runs as a spawned background task (not a select arm) so it can cover pre-prompt session setup, not just the final prompt call. That introduced a cross-thread race — a liveness tick could be mid-emit when the guard's abort() lands, letting a stale turn_liveness frame reach the wire after turn_completed. Closed (not just narrowed) via a shared mutex: LivenessGuard::drop takes the same lock the emitting tick holds across its check-then-emit, so drop either sees the tick hasn't started (and suppresses it) or blocks until the in-flight emit has fully landed before aborting — no interleaving can emit a liveness frame after the guard has dropped. Pinned by two tests that assert the mechanism itself rather than timing: test_liveness_emits_nothing_once_closed_flag_is_set (pre-set flag suppresses the emit) and test_liveness_guard_drop_blocks_while_emit_lock_is_held (real OS threads, no cooperative scheduling — proves drop can't return while the emit lock is held).

Same deviation also meant the spawned task captured its context once, before session resolution, so liveness frames carried session_id: None for the whole turn (a wire regression vs. the prior code, which built liveness after session resolution). Fixed by backfilling the session ID onto the shared liveness state once known (LivenessGuard::set_session_id), so ticks after resolution carry the real session; covered by test_liveness_backfills_session_id_after_resolution.

@wpfleger96
wpfleger96 requested a review from a team as a code owner July 17, 2026 16:41
The desktop active-turn badge reset to 0s repeatedly while agents were
still working. Liveness pings previously started only after session
setup and lacked a true start timestamp, so reconnect/resurrection
re-anchored the badge to the recovery frame instead of the turn's
actual start. turn_error also carried no turn_id, leaving terminal
events unable to close out the right turn.

- Start turn liveness at turn_started (covers session setup, not just
  the final prompt call) and carry the authoritative started_at on
  every observer frame for the turn.
- Thread turn_id through PromptResult into emit_turn_error so error
  terminal events target the correct turn.
- Resurrect uses the frame's startedAt when present and not later than
  the frame itself, so recovered turns keep their true elapsed time.
- Prune-pause is now per-agent (not global) with a 3-minute bounded
  cap, so a genuinely dead turn still prunes instead of pausing
  indefinitely.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/turn-timer-liveness-anchor branch from fc402ab to 74071e6 Compare July 17, 2026 18:16
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.

2 participants