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 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:
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.