---
title: Turn duration (how long Claude took to respond, shown on every turn bubble)
---

# Turn duration (how long Claude took to respond, shown on every turn bubble)

## What it is

Every Claude turn in an Omniscio chat carries a small **duration label** so you can see at a glance how long the agent spent on that turn. While the turn is still running, the label ticks live: `streaming… 1m 12s`. Once the turn settles, the live ticker is replaced with a frozen badge: `… · took 1m 30s`.

There's a third place duration shows up: at every **compact divider** (the system row that appears when the conversation gets compacted because the context window filled), an activity bubble renders the rolled-up duration of the work between this divider and the previous one — `13 actions · took 1h 42m`. This one is more careful about what it counts: it can span many turns, including stretches where you were away from the keyboard, so it explicitly excludes the user-idle gaps between operator messages and only counts the time the agent was actively working.

## Where to find it

1. **Live ticker on the in-flight turn bubble.** While the session is `running`, the merged turn bubble's header shows the elapsed wall-clock since the operator's prompt landed, ticking every second. The moment the session leaves `running` (it finishes, pauses, errors, anything), the ticker hides — you never see a live counter ticking while the agent is paused.

   A **declared quiet wait** is the one case worth naming, because it stays `running` on purpose. When an agent finishes a turn and parks itself waiting on something external, Omniscio holds the session `running` for up to about half an hour so a queued message can still reach it. The agent is not working, so since 2026-09-19 the live treatment is suppressed for the whole hold: no ticker, no `streaming…` header, and no "Thinking…" line. The turn renders settled, and the bubble that says "agent is working" only appears when one genuinely is.

2. **Settled `took` badge on the merged turn bubble.** Once the turn finishes (a final agent reply arrives, or the last hidden tool output is the only thing left), the ticker is replaced with a static `took 1m 30s` baked into the bubble header. This badge uses real timestamps from the operator message and the final agent activity — the CLI was alive the whole time the turn was happening, so wall-clock IS active time at this scope.

3. **Settled `took` badge on the inter-compaction expander.** At every compact-divider system row, a separate activity bubble appears alongside the divider with a `took` badge covering everything between this divider and the previous compact divider (or the start of the conversation). Because that window can span many turns and many user-idle gaps, the math here is different — see below.

## How it behaves

### Why the inter-compaction expander does different math

Within a single turn, the agent is doing real work the whole time, even if the work is a long-running tool call. A 15-minute `npm test` that completes within one turn is genuine active time and the duration label reflects that — `took 15m`. That's why sites 1 and 2 just use wall-clock from real timestamps.

Across turns is different. Between the agent's last message of one turn and your next prompt, the session sits in **`needs_you`** waiting on you. That gap is idle time, not active time. Counting it would produce misleading totals — a 7-hour gap where you went to lunch shouldn't show up as 7 hours of agent work.

So the inter-compaction expander uses a different algorithm: it splits its window at every `operator` row and only counts the stretches in between (from each operator message forward to the last activity before the next operator message). The cumulative sum is what renders as `took`. A real fixture session that elapsed 7h 54m wall-clock, but contained a 5h 56m user-idle gap between operator messages, reports `took 1h 42m` — exactly the agent's active time.

### What gets shown when a turn was paused

Pausing and resuming a turn mid-flight is a known edge case. If you pause an in-flight turn and resume it hours later, the live ticker and the settled `took` badge will both inflate to include the pause — because the underlying timestamps (operator message in, final reply out) don't change when you pause, and the bubble math just uses those two endpoints. Fixing this properly requires Omniscio's backend to accumulate active seconds across pause/resume cycles per turn, which it doesn't do today.

If you see a `took` value on a single turn that feels too high, check whether you paused that turn — if so, the agent didn't really spend that long; you did. The inter-compaction expander **is not** affected by this because it uses the operator-row splitting algorithm and the wall-clock-only sites are within a single turn.

### How to read the labels

- `streaming…` (no number visible) — Claude is still talking; the seconds tick every second once they start counting.
- `streaming… 23s` — currently 23 seconds into this turn; will keep counting as long as the session is `running`.
- `… · took 1m 30s` (in the merged bubble header) — turn is done, agent took 1 minute 30 seconds.
- `13 actions · took 1h 42m` (alongside a compact divider) — between this divider and the previous one there were 13 agent operations spanning roughly 1 hour 42 minutes of active work (idle gaps excluded).
- `took 0s` — yes, this can render. It's a real zero (the backend reported `activeDurationSeconds = 0` and the inter-compaction expander honored it). It is not a missing value; missing values fall through to wall-clock instead.

## For agents

### Where to inspect this in the source

- **Active-time helper (the one used by the inter-compaction expander):** `computeActiveDuration` in `src/shared/turn-active-duration.ts`. Pure, dependency-free, takes a list of rows + window start/end, returns active seconds with operator-row splitting. The fixture lock for the 7h 54m → 1h 42m reduction lives in `tests/unit/shared/turn-active-duration.test.ts`.
- **Live ticker hook:** `useThinkingTimer` in `src/renderer/src/hooks/useThinkingTimer.ts`. Wall-clock against an `inFlightTurnKey` that's null whenever the session isn't `running`, which is the gate that hides the ticker on pause.
- **Settled merged-bubble `took`:** `settledTook` useMemo in `src/renderer/src/features/sessions/MergedTurnBubble.tsx`. Uses the operator timestamp and the final agent timestamp.
- **Inter-compaction expander `took`:** `settledTook` useMemo in `src/renderer/src/features/sessions/TurnActivityBubble.tsx`. Takes an explicit `activeDurationSeconds` prop from the caller (which is `computeActiveDuration` over the divider-to-divider window) and ignores wall-clock when the override is supplied.
- **Contract:** `.claude/memory/contracts/turn-duration-contract.md` is the present-tense single source of truth, lists every test that locks an invariant, and documents the pause-then-resume limitation. Read it before changing any of the above.

## Related

- [real-conversation-layout.md](real-conversation-layout.md) — the cold-mount chat layout that decides which bubbles render, including the merged turn bubble that hosts the live ticker and the settled `took`.
- [scroll-position-memory.md](scroll-position-memory.md) — how the chat pane decides where to land on cold mount; not duration-related directly, but the same merged bubble is the scroll anchor that the duration labels sit on.
