# Time estimates

> Show how long each running session and subagent has left, from the decision model.

With time estimates on, each running session shows how long it has left, and each running subagent shows its own time left in its header. The estimate counts down live. It stays the same when you close and reopen a window, because it is saved on the run.

| Where | Shows |
| - | - |
| Sidebar row of a running session | `~12m` in place of the elapsed time. Hover it for the elapsed time. With Run details in the sidebar off, it shows after "Working". |
| Subagent header in the transcript | `~3m` next to the task, while the subagent runs |
| While the run waits for you (an approval, a question) | The countdown stops and dims. Time spent waiting is not counted. |
| The estimate is spent, but the run is not over it yet | `almost done` |
| More than a day left | `24h+`. The last step has no upper bound, so the chip shows no number past a day. |
| After the run passes its estimate | `over`, in the warning color |
| The run could not be estimated, and asking again would fail the same way | A muted warning mark next to the elapsed time. Its tooltip gives the reason, such as `invalid` or `auth`, and the provider's message. |

A wait pauses the run that asked and the runs above it: a subagent's approval pauses that subagent and the session's run, while other subagents keep counting.

The desktop app and the web app show estimates. The terminal app does not, so it lists the setting as a desktop setting.

## Turn it on

In the desktop app, open Settings › Agent › Helper models › **Estimate time remaining** (`runEstimates`). In the terminal app, it is under Settings › Agent › Models. It needs [Decisions](https://zidane.si/docs/decisions.md) on, because each estimate is one decision call. It is off by default: decisions are billed by the provider.

## How a run is estimated

The host asks the decision model how much more working time the run needs. The model rates it on 10 steps, each about 2.5 times the last: 1, 3, 8, 20 and 45 minutes, 2, 5, 12 and 24 hours, then more than 24 hours for work that goes on for days. Ten is the most a score question takes. Its answer can fall between two marks, so an estimate is a real duration such as 11 minutes or 12 hours, not a range. The spread of the answer sets the point after which the run counts as over (its 80th percentile).

The model gets the task (the prompt, or a subagent's task, up to 4,000 characters), the project name, the working time so far, the number of tool calls and the last few tool names, the todo list and its progress, and the previous estimate. These go to the decision provider, which can be another provider than the session's, as with automatic titles.

| When | Asks |
| - | - |
| The run has gone 10 seconds | The first estimate. A run that ends sooner costs nothing and never shows one. |
| The todo list changes | Again, at most once a minute |
| The run passes its estimate | Once more, then never again for that run |
| The run is resumed with an estimate | Nothing. It keeps the estimate it had. |

A run usually gets 1 to 4 estimates. Each subagent gets its own, with the same rules.

A failed decision is saved on the run (`estimateFailure`), and the next trigger tries again. A timeout, a rate limit or a network error shows nothing. A failure the next attempt would hit again (`invalid`, `auth`, `unsupported`, `quota`, `unavailable`) shows the warning mark, so a setup problem does not look like a run with no estimate yet.

## Calibration

Each project learns from its own runs. When a run finishes normally, the host records how long it worked against its first estimate. A stopped or failed run records nothing. Once a project has 5 finished runs, the next estimates are multiplied by the median of the last 50 ratios, kept between 0.5 and 3. Calibrated estimates save the factor on the run (`calibration`).

A session's runs and subagents learn apart: a subagent does a narrow task, often on another model, and is off in its own way. A project is the session's root folder, so its worktrees learn together.

The ratios are kept in `run-estimates.json` in the data directory, per project and kind. Delete the file to start over.

## Saved on the run

The estimate is part of the run record (`SessionRun`), so every session store keeps it with no setup:

| Field | Holds |
| - | - |
| `estimate.totalMs` | Expected working time of the whole run, from its start |
| `estimate.highMs` | Working time after which the run is over its estimate |
| `estimate.at` | When the estimate was made |
| `estimate.revisions` | How many estimates the run has had |
| `estimate.initialMs` | The first estimate, before calibration. Calibration compares the actual time to it. |
| `estimate.calibration` | The factor applied, when there was one |
| `estimate.overrunChecked` | The run went over and was estimated again |
| `userWait` | Time spent waiting for you: `ms` for finished waits, `since` while a wait goes on |
| `estimateFailure` | The last failed attempt: `at`, `reason`, `message` |

Working time is the time since the run started, minus `userWait`. Time left is `totalMs` minus the working time.

## On the wire

| Signal or field | Carries |
| - | - |
| `run:estimate` | `{ sessionId, estimate }`. Sent when a run is estimated, and when a wait for you starts or ends. |
| `SessionRow.runEstimates` | The estimates (or lasting problems) of a running session's runs, the session's own first. A window that missed the signals reads them here. |

`estimate` is a `RunEstimateView`: `runId`, `startedAt`, `depth`, and `label` and `callId` for a subagent, then either the timing (`totalMs`, `highMs`, `at`, `waitMs`, `waitingSince`) or `problem` (`at`, `reason`, `message`). `isRunEstimated(view)` tells them apart. `estimateReadout(view, now)` from `zidane/chat/pure` says what it shows at a moment: `live`, `due` (spent, not over yet), `paused` or `over`, and the time left. `formatEstimate(ms)` writes the time left.

## In your own app

The UI kit has the pieces the desktop app uses:

```tsx
import { RunEstimateChip, RunEstimatesProvider, useRunEstimate } from 'zidane-ui/chat'
import { createRunEstimates } from 'zidane-ui/chat/headless'
import { createSidebarLive } from 'zidane-ui/chat/sidebar'

const estimates = createRunEstimates({ subscribe: listener => client.onSignal(listener) })
estimates.start()

// Rows: the time left replaces the elapsed time.
const live = createSidebarLive({ /* … */ activity, badges, estimates })

// Subagent headers in the transcript read the provider.
<RunEstimatesProvider store={estimates}>{app}</RunEstimatesProvider>
```

Feed it the session list's rows too (`estimates.noteRows(rows)`, in the directory's `onListed`), so a window that opens mid-run counts down from the same point.

## Reference

- [Decisions](https://zidane.si/docs/decisions.md)
- [Sub-agents](https://zidane.si/docs/sub-agents.md)
- [`zidane/chat`](https://zidane.si/docs/reference/chat.md)
