Pending Tasks and Cancellation
Last updated
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.