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 TUI uses "cli-", the web dashboard "chat-", and the Telegram / Discord listeners "tg-" / "dc-" (an empty prefix matches everything) — and claims only matching entries through PickNext(prefix) or PickNextMatch(prefix, accept). PickNextFor was removed; use PickNext / PickNextMatch. 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. 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 the TUI (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 4 KiB, arguments to 1 KiB), 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. An ask_user reply that fails for a reason other than user cancellation no longer discards the pending task either.