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

Since v1.3.0 a TUI run can also leave work in the background, since v1.3.2 through `background_command` and `background_subagents`; only requests whose origin is `cli-` and that have no deliver target qualify (this no longer depends on a decision model). `run_command_background` was renamed `background_command` (Removed in v1.3.2); `run_websocket` and the `subagents` `background` / `timeout_min` parameters were dropped (Removed in v1.3.2): use `background_command` and `background_subagents` instead. Each call starts one leg, with an id such as `cmd_` or `sub_` plus 8 hex characters. Since v1.3.8 the legs a task starts in the same turn form one job, stored as `~/.config/agenvoy/sessions/<session>/background/job_<8 hex>.json` instead of in the task's pending snapshot (Removed in v1.3.8), and both tools take `objective` (what the group is for, written to stand alone) and `next` (what to do with the reports); the first non-empty value in the group is kept. A session runs at most `MAX_BACKGROUND_TASKS` legs at once, shared by both tools (`DEFAULT_CONCURRENCY`: half the CPU count, at least 3). While a leg runs, its output is appended to `~/.config/agenvoy/sessions/<session>/background/<leg id>.log`, which is removed when it ends. A finished leg's result is kept up to 32 KiB; watch reports from a running `background_command` are kept up to 8 KiB each and 32 KiB in all, with the oldest marked `...[N reports elided]...`. Since v1.3.8 the starting task no longer stays pending for its legs: it ends normally and is written to task history, and the daemon no longer resumes it with a `[Resumed Task — background task finished]` message for every report (Removed in v1.3.8). Where a report goes depends on the starting task:

| Starting task | Report |
| --- | --- |
| Running | Added at the start of its next turn as a `[Background Report — background task finished]` (or `... update]`) message; a report that arrives just before it finishes adds one more turn |
| Paused or waiting on `ask_user` | Kept in the job and carried into the resume message when the task resumes |
| Ended | Held until every leg of the job has finished; the job then runs once as a new task whose hidden input starts with `[Resumed Task — background group finished]` and carries `objective`, `next`, every report and the starting task's id for `chat_history(mode=read)`. A job whose reports were all taken by the starting task does not run |

Background jobs, and the `background/` directories with them, are cleared when the daemon starts, so background work does not survive a restart.

`background_command` always watches its output: a new line containing an error keyword (`keywords`; by default `error`, `err!`, `fail`, `fatal`, `panic`, `exception`, `traceback`, `segmentation fault`, `core dumped`, `out of memory`, `permission denied`, `command not found`, `no such file`) reports the latest output (up to 8 KiB) while the command keeps running, once output settles (2 s of quiet, at most 10 s) and at most one report per 30 s. With a decision model set, a decision check first drops benign hits and repeats of the last report, and a failed check reports the output unchecked with the failure noted; without a decision model every hit is reported. The `interval` and `prompt` parameters of the old watch mode were dropped (Removed in v1.3.2). A `background_subagents` leg reports its saved report path; background legs never raise confirmations, they return what they gathered and say the call still needs approval.

While the TUI showed `Waiting for background tasks`, a message sent from that window joined the waiting task; that mode was Removed in v1.3.8, since the starting task now ends normally and a new message is an ordinary request. `POST /v1/send` still treats a `pending_task` that names a pending task with background jobs as a follow-up to it: it resumes that task, or becomes steer while the task is running. Resuming a task that is not the session's most recent task and has no background jobs moves it to a new task hash, so its replies are not inserted into the older task (v1.3.0).

## 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, **Yes** in the Esc popup, or **Cancel task** in a confirmation (v1.3.3) — deletes the pending task; since v1.3.9 the task is first written to chat history with a `[Canceled]` reply (plus the last error when it had failed), so `chat_history` still finds it. Any other cancel, including the popup's **Pause** and, since v1.3.3, **Abort task** in a confirmation (`runtime.ErrUserAborted`), 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. Since v1.3.0 cancel and pause also stop the task's background jobs; cancel then deletes the pending task, while pause keeps it. A single leg is cancelled from the TUI background-task menu (v1.3.8). The `/pending` `[Active]` mark that kept a task with running background jobs from being picked was Removed in v1.3.8. A resume that finds the task already claimed by a run fails with `[Conflict] pending task: running`. Since v1.3.3 a cancelled task no longer leaves an empty resumable pending entry behind.

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, then — since v1.3.8 — opens the background-task menu when the session has legs running: **No**, each leg, and **All** (two or more), followed by a confirmation; the TUI sends the pick as a `background_cancel` frame, and the daemon cancels the leg, drops its late result and settles its job. An `ask_user` reply that fails for a reason other than user cancellation no longer discards the pending task either.
