carry of #28303: time the live turn instead of its old prompt

TE
3d ago
+760-25

Carry of #28303, itself the carry of #26214: the live turn's "Thinking" / "Working for" clock counts from the live turn's start instead of an older prompt that a resumed turn still opens with. This supersedes #26214 and #28303. #26214's base merges dropped the direct.ts and model.ts plumbing, so turnStartedAtMs was never set and the timer fix never landed on base.

Leo reported a Session status screenshot that read "Thinking 118h 21m 48s" (from #26214). A resumed live turn kept a prompt from days earlier as its opener, and the working line counted from that prompt.

What changes

  • turns.ts / App.tsx: the live turn's rows (thinking, working line, activity line, the Codex live block) count from the snapshot's turnStartedAtMs. The prompt's time is used only when that value is unset. #26214's commits and authorship are kept.
  • direct.ts: turnStartedAtMs is set on turn_started. When that event is outside the attached page, it is taken from a running summary's turn.startedAt; a summary that names an older start than the one already held changes nothing. The value is cleared when the turn ends and is carried through resets, rebuilds and older-page folds. workingNow, turnRunning and continueIn now share one predicate (summaryRuns: running or waiting, not stale).
  • Merge with base (about 309 commits, conflicts in direct.ts and model.ts), resolved by hand. Base's turn identity replaces this branch's turnId / endsCurrentTurn: that is runningTurnId, #28160 and #28286's Stop binding (stopTurnId, stoppedTurnId, refusals that answer only their own turn), and the early return for a late terminal event. Every Stop path behaves as it does on base. The snapshot carries both turnStartedAtMs and base's turnsEnded.
  • P2 from the review at f2af597 is fixed. At a cold start, acpmux logs the prompt's user_message (which opens its turn) while the chat's row still says idle or ready and names that turn. Before this change, any non-running summary cleared the turn, which hid Stop and enabled Continue. Now a summary that is not running ends only a turn that the summary alone named, and only once it no longer names an open turn (turn: null, or a turn this client saw end). Otherwise a turn ends on its own turn_end / turn_result, or on a live idle that comes right after a live running or waiting. A closed chat that reopens idle after its prompt is still a cold start, so that idle does not end the turn. A turn_end for the running turn, or a late end of another turn, marks that turn ended, so a summary that still names it counts as stale.

Tests

Red on #28303's head f2af597, green here, focused command cd webviews && bun test test/agent-pane-silent-turn.test.ts:

Testf2af597this head
a cold start's idle and ready rows keep the turn its prompt opened (the P2)failpass
a live idle after the turn ran ends it, with no resultfail (timed out)pass
the other 8 in the file (resumed start, summary-only start, newer/older summary, stale turn_result, waiting attach, resync rebuild)passpass

At head 3c909a94068 on fast checks (Linux, run 41149022685): bun run typecheck exit 0; vp fmt --check on the changed files exit 0; and bun run test over 66 files, which cover the agent-pane turn, timer and Stop tests (every test that imports direct.ts, plus turns.test.ts, TurnRows, transcript-working-line, composer-send-stop, escape-stop, composer-turn-cycle, agent-pane-turn-footer, agent-pane-working-counts, reasoning-chip-steps): 66 files, 0 failed or timed out, 0 retried. reasoning-chip-steps ("New Tab at 300 px: the model button's effort reads "Med" whole") passes here, and the same run passed it at base b623fbb2a51 too, so the earlier red on #28303 is not this change's.

Stills

Gallery entry agent-pane.live-turn-timer (new, LiveTurnTimer.gallery.ts): a prompt sent 118 h 21 m before the gallery's now opens a turn that started 26 s ago. Playwright Chromium at 420 px, in the same fast checks job: base b623fbb2a51 (with this entry copied in) in run 41149022685, and head 3c909a94068 in run 41154486110.

StateThemeBefore (base)After (head)
ThinkingDarkbefore thinking darkafter thinking dark
ThinkingLightbefore thinking lightafter thinking light
Working (a command ran)Darkbefore working darkafter working dark
Working (a command ran)Lightbefore working lightafter working light

Viewed after image: https://raw.githubusercontent.com/manaflow-ai/cmux/pr-media/28303/head-thinking-dark.png reads "Thinking 26s" (base: "Thinking 118h 21m"); https://raw.githubusercontent.com/manaflow-ai/cmux/pr-media/28303/head-working-light.png reads "Thinking 26s" under "Ran bun test src/fetch" (base: "Thinking 118h 21m").

Gallery: agent-pane.live-turn-timer

Changelog

  • Fixed: a resumed agent turn's "Thinking" / "Working for" time counts from the turn's start, not from a prompt sent days earlier.

Observed / Not run

Observed: the focused red/green above (run locally on this lane VM, a single-file unit test); typecheck, fmt and the 66-file webview run on fast checks at 3c909a94068; the gallery stills before and after, all 8 opened and read. scripts/web-lint.sh fails only in src/pages/settings (design tokens, layers, fmt, lint on the context-menu files), which is red on base b623fbb2a51, and this change touches none of those files.

Not run: a tagged Mac build or a live acpmux cold start; the cold-start order (a prompt's user_message, then the row reads idle or ready while still naming the turn) is taken from hub/turns.rs and hub/lifecycle.rs and exercised through the fake daemon. There is no independent review at this head yet.

Supersedes #26214 and #28303. #28303's pre-push ownership check refused this session's push to its branch, so the work moved to this branch, as the hand-off allowed.

Agent session: agent session

🤖 Generated with Claude Code