# Pending Tasks and Cancellation

How a run waiting on the user is parked, and what cancel and pause do to a running task.

## Pending registry

`internal/runtime/pending.go` is the prefix-routed confirm/ask registry shared by the main agent and any in-process subagents. Producers (`toolCall` confirm, `ask_user` handler, `store_secret` handler) call `Ask(ctx, req)` and block on a per-entry buffered=1 reply channel. Each front-end registers a listener via `RegisterListener(prefix)` — the web dashboard `"chat-"`, and the Telegram / Discord listeners `"tg-"` / `"dc-"` (an empty prefix matches everything) — and claims only matching entries through `PickNextMatch(prefix, accept)`. Since v1.2.0 the TUI has no listener of its own: each daemon socket connection registers `"cli-"` with `RegisterListenerMatch(prefix, match)`, matching only requests whose ctx carries a window hash that connection started, and forwards them as `ask` frames, so a confirmation or question from a TUI run reaches only the TUI window that started it. A restricted-path confirmation asks for the system password in a TUI password field (skipped while sudo credentials are still cached); the daemon verifies it with `sudo -S -v` and reports the result in a `verify` frame when a password was entered or verification fails. Closing the connection resolves its open questions with an error. ctx cancellation removes the entry so a stale producer never wastes a human interaction.

The gate `HasListener(origin)` checks whether a listener whose prefix matches the session's origin (`OriginOf` recognizes `cli-`, `chat-`, `tg-`, `dc-`) is registered. With a match function, the listener counts only when the request's ctx passes it, so a TUI run whose window is disconnected finds no listener. This replaces the old global `pending.Active atomic.Bool`, so TUI, web, Telegram, and Discord confirm flows run side by side without blocking each other.

Since v1.0.23 `ask_user` splits by origin. From a TUI run (origin `cli-`) the question goes through `Ask` as an inline request and the same run continues with the answer. From any other origin the round's other tool results are committed, a pending snapshot is written (each tool result clamped to 32 KiB and its arguments to 4 KiB since v1.1.2 — 4 KiB / 1 KiB before; a clamped value carries a `...[N bytes elided]...` marker, and the resume prompt tells the model to re-run the call when the elided part matters), the run ends, and the answer resumes it as a new turn.

## Cancel and pause

Cancellation carries a cause since v1.0.10 (`context.WithCancelCause`). Only a cancel that names `runtime.ErrUserCanceled` — Ctrl+C in the TUI, or **Yes** in the Esc popup — deletes the pending task. Any other cancel, including the popup's **Pause**, tears down the run but leaves the pending record in place, so `/pending` or `GET /v1/session/:id/task` can resume it. Since v1.2.0 the TUI's `/pending` resumes through the daemon with a `pending` frame; a saved question is asked inline over the socket and the resumed run streams back to the same window. A TUI run is bound to its socket connection: closing the TUI cancels the run without `runtime.ErrUserCanceled`, so its pending task is kept.

The TUI's Ctrl+C and popup choices travel as a `cancel` frame (with `pause` for **Pause**), which the daemon applies through `exec.Cancel(taskHash, cause)` only for tasks that connection started. Since v1.2.0 `exec.Run` (named `exec.Start` before v1.2.1) generates the task hash and registers the cancel function before it emits the first event, so a cancel works from the first event onward; a cancel requested before the TUI has seen any task hash is held and sent once the first one arrives. Outside a popup, `Esc` first closes an open `/` command picker, then the notice box, then cancels the run (immediately if nothing has been shown yet, otherwise through the popup), then clears the input. An `ask_user` reply that fails for a reason other than user cancellation no longer discards the pending task either.
