carry of #28303: time the live turn instead of its old prompt
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'sturnStartedAtMs. The prompt's time is used only when that value is unset. #26214's commits and authorship are kept.direct.ts:turnStartedAtMsis set onturn_started. When that event is outside the attached page, it is taken from a running summary'sturn.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,turnRunningandcontinueInnow share one predicate (summaryRuns: running or waiting, not stale).- Merge with base (about 309 commits, conflicts in
direct.tsandmodel.ts), resolved by hand. Base's turn identity replaces this branch'sturnId/endsCurrentTurn: that isrunningTurnId, #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 bothturnStartedAtMsand base'sturnsEnded. - 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 ownturn_end/turn_result, or on a liveidlethat comes right after a liverunningorwaiting. A closed chat that reopens idle after its prompt is still a cold start, so that idle does not end the turn. Aturn_endfor 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:
| Test | f2af597 | this head |
|---|---|---|
| a cold start's idle and ready rows keep the turn its prompt opened (the P2) | fail | pass |
| a live idle after the turn ran ends it, with no result | fail (timed out) | pass |
the other 8 in the file (resumed start, summary-only start, newer/older summary, stale turn_result, waiting attach, resync rebuild) | pass | pass |
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.
| State | Theme | Before (base) | After (head) |
|---|---|---|---|
| Thinking | Dark | ![]() | ![]() |
| Thinking | Light | ![]() | ![]() |
| Working (a command ran) | Dark | ![]() | ![]() |
| Working (a command ran) | 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







