pi-bg-bash
Runs the model's bash commands as detached background tasks and wakes the agent when they finish
Package details
Install pi-bg-bash from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-bg-bash- Package
pi-bg-bash- Version
0.5.0- Published
- Nov 1, 2026
- Downloads
- 1,174/mo · 154/wk
- Author
- gvanderclay
- License
- MIT
- Types
- extension
- Size
- 172.4 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-bg-bash
A Pi extension that lets the model run shell commands in the background. It
replaces Pi's bash tool with the same tool plus a background flag: a call
with background: true starts the command detached, returns a task id at once,
and wakes the agent with the result when the command exits. Ordinary foreground
calls wait for the command to finish and return its output; they never move to the
background just because time passed. Only an explicit waitFor match or the
user's keyboard shortcut can promote a running foreground call.
Install
You need:
- Pi on macOS or Linux (not Windows). Pi 1.0.0 is the version pi-bg-bash is
tested with. Pi supplies the peer dependencies
@earendil-works/pi-coding-agentandtypebox. - A POSIX
sh. The command itself runs in Pi's own shell (bash when present). - No other extension that replaces the
bashtool or registers/bgorctrl+shift+b. pi-bg-tasks claims all three, so install only one of the two.
Install the package from npm:
pi install npm:pi-bg-bash
To follow the latest commit instead, install it from GitHub:
pi install git:github.com/gvanderclay/pi-bg-bash
The package has no runtime dependencies and no build step.
First use
Ask the model for something that keeps running, for example "start the dev
server in the background and tell me when it is up". It calls bash with
background: true and gets back a task id such as bg-1. The footer shows
bg: 1 while the task runs, and the model can read its output with
bash_output while it works on something else. When the command exits, the
agent receives one message with the exit state and the last lines of output,
which starts a turn if it was idle.
Run /bg to see every task: enter shows a task's output, and x kills it
after you confirm. Press
ctrl+shift+b while a foreground command runs to move it to the background
at once instead of waiting.
What it adds
- The
bashtool keeps Pi's parameters and addsbackground,waitForandhold. Withoutbackground: true, a call waits in the foreground and returns normal output when the command exits. Explicittimeoutand turn abort still stop it. Only an opt-inwaitFormatch or deliberate user shortcut can promote it. Its description tells the model to wait for something to finish (a CI run, a server coming up) with a blocking command such asgh run watch <run-id> --exit-statusandbackground: true, neversleep Nand then a check. The result of a call that goes on as a task tells the model to end its turn if nothing else is left, since the completion message starts its next one.waitFor: "<text>"(foreground only) returns as soon as the text appears in the output while the command keeps running as a task, for a server that should stay up. - A codemode script's
await tools.bash(...)resolves to Pi's bash object,{ output, truncated, full_output_path?, exit_code?, wall_time_seconds, task_id? }. When the command goes on as a task (background: true, awaitFormatch or the user'sctrl+shift+b),task_idholds its id forbash_outputorbash_kill,exit_codeis absent,outputholds the output so far (empty forbackground: true), andfull_output_path, when the output was truncated, is the task's log. bash_watch({ id, action?, contains? })subscribes to output events while a task keeps running. It is a native tool, available outside codemode even incodemode.only. See Output events and completion holds.bash_output({ id, latest?, filter? })reads a task's output from where the last read stopped, or only the newest output, optionally filtered by a regex.bash_tasks()lists every task of the session with its state and runtime.bash_kill({ id })stops a task and everything it started.- In a codemode script these three resolve to objects, not text (see Structured results), and these tools carry annotation hints for permission extensions.
/bglists the tasks; enter shows the selected task's output andxkills it after a confirmation.ctrl+shift+bmoves the running foreground command to the background. It needs a terminal that reports modified keys (the kitty keyboard protocol ormodifyOtherKeys); inside tmux, alsoset -g extended-keys on.
Tasks belong to the session that started them. /reload keeps them; quitting
or switching sessions (new, resume, fork) kills them, and if Pi crashes a
watcher process kills them. Logs live under
$XDG_STATE_HOME/pi-bg/<session-id>/ (default ~/.local/state) and are
removed after 7 days.
The rest of this README describes the behaviour in detail.
Background tasks
- Wrapper:
shspawneddetached, stdout and stderr on the log's file descriptor, the handleunref()ed. It runs the command, passed as an argument, in Pi's own shell (getShellConfig, so bash-only syntax works) with Pi's shell environment (getShellEnv; where a Pi version lacks an export it falls back tosh -candprocess.envplus<agent dir>/binon PATH), plus the session variables Pi's bash tool sets, taken from the context:PI_SESSION_ID,PI_SESSION_FILE,PI_PROVIDER,PI_MODEL,PI_REASONING_LEVEL(each left out when the context has no value; inherited ones are cleared first, as Pi does). When the command exits the wrapper appends__PI_BG_EXIT__:<nonce>:<code>. The nonce is random per task and passed as an argument, so output that looks like a marker is not one. The marker is found wherever it sits, since a backgrounded grandchild may write after it, and is never shown as output. The wrapper's own stderr is silenced (the command's still reaches the log), so a shell job notice likeKilled: 9for a command that killed itself is not output. A separateshwatcher, its own detached process outside the task's group, is given Pi's pid and the task's group; it exits once the group is empty, and when Pi dies first it appends__PI_BG_GONE__:<nonce>and SIGKILLs whatever is left. A watcher that fails to start leaves the task running without a crash watch (fail open). - Spawn failures: a missing working directory is refused with "Working directory does not exist"; a failed spawn rejects, and no log is left.
- Row timer: the chat row of a
bashcall that started a task or was promoted to one replaces Pi'sTook Nsline with when the task started, such asRunning since 2:14:13 PMin the local time format, then, redrawn once when the task ends, how it ended, such asExited (code 0) after 4m 10sorKilled (timed out) after 2m 0s. The row never counts up: redrawing a row that has scrolled off screen makes Pi clear and repaint the whole screen and scrollback, which flickers. A row whose task this Pi process does not know, such as one in a session resumed after a restart, keeps Pi's line. - Logs:
$XDG_STATE_HOME/pi-bg/<session-id>/<id>.log(default~/.local/state), directory0o700, file0o600, created exclusively. The session directory carries anowner.pidmarker naming the Pi process that owns it, so another Pi's cleanup never deletes a live task's log. A pid the OS has reused can keep a dead owner's directory out of reach for one session start, but the next start re-checks and removes it once the pid is gone. Ids restart atbg-1per Pi process, so a resumed session skips any id whose.logor.log.gzalready exists. Once a task's completion message has been sent, the log is streamed throughnode:zlibto<id>.log.gz(0600) and the plain file is deleted;bash_outputand the completion tail read it transparently. A task is killed, with the messagekilled (log limit passed)and a__PI_BG_LIMIT__marker ending its log, once its log passes 100 MiB; the size is checked four times a second, so a runaway is stopped just past the limit.PI_BG_BASH_LOG_LIMIT_BYTESlowers the limit for the contract test. Atsession_start, logs and emptied session directories older than 7 days are removed — never a running task's log or a directory owned by a live Pi, and never anything but the extension's ownbg-<n>.log/.log.gzfiles. - Not honoured: Pi's
shellPathandshellCommandPrefixsettings. An extension cannot read them, so neither background nor foreground commands use them. - Registry: on
globalThisunderSymbol.for("pi-bg-bash.registry"), with one 2 s poller and a 250 ms stat-only log-limit check. A task ends when its marker appears, or when its pid is gone with no marker (exit unknown). A log that is missing also means exit unknown. Any other read error is retried on later ticks; once the pid is dead and five reads in a row have failed, the exit is unknown too. A single failure never hides a real exit. One task's failure, a stale footer context or a throwingsendMessagenever stops the poller; a message that failed to send is retried on a later tick. A task is registered only after its spawn resolves. - Completion message: one
pi.sendMessagecustom message per task,deliverAs: "steer",triggerTurn: true, with the id, the command (on one line, newlines shown as⏎, cut to 200 characters), exit state (exited (code N),killed (timed out),killed (killed by user),killed (log limit passed), orexit unknown, the same wording asbash_tasks), runtime and the last 20 lines of output, capped at 4 KiB. When the cap cut the output, its label says so and namesbash_output <id>, which reads the whole log since the completion message never moves the read position. A busy agent gets the message when its current turn ends, before its next model request; one that ends while the agent writes its final answer still arrives after it. AfollowUpwould be held until the agent stopped, so every task that ended during a run would arrive after its final answer, one turn each, and a delegate's reply would become its answer to the last of them. - Footer:
ctx.ui.setStatus("bg", "bg: N")while N tasks run. - A call without
backgroundis a foreground call; see below. - Stripped output: what the agent reads (the foreground result,
bash_outputand the completion message's tail) has ANSI escape codes, carriage returns and other control characters except newline and tab removed, the pipeline Pi applies to the user's!commands (stripAnsi,sanitizeBinaryOutput, then\rdropped;sanitize.tscarries an own copy, since Pi does not export them). Pi's ownbashtool hands its output on unstripped, so the foreground result is stripped here too. The logs on disk stay raw, byte for byte.
Foreground calls and promotion
A call without background runs Pi's own bash (createBashToolDefinition's
execute, built per call for the session's cwd) over a custom operations.exec
(foreground.ts). That exec launches the command through the same wrapper as
a background task, with Pi's shell, environment and PI_* variables as the
delegate resolved them, and feeds the log to onData (polled every 50 ms; a
trailing partial exit marker is held back, never shown). Output, truncation,
the error texts (Command exited with code N, Command aborted, Command timed out after N seconds) and the timeout checks (Invalid timeout: ... for a value
that is not above 0 or exceeds 2147483.647 s) are Pi's. A timeout or an abort of
the turn, including one that lands while the process is being spawned, sends
SIGKILL to the command's process group at once, as Pi's bash does; only
bash_kill and /bg use SIGTERM, then SIGKILL after the grace period. A
foreground command that ends leaves no task and no log, and its id is given back.
After the exit marker the call keeps reading the log until it has been idle for
100 ms (reset on new output), so a short-lived background child's last writes are
kept, as Pi's bash keeps inherited-pipe output. A command that vanishes without an
exit marker ends the call with Command terminated without an exit code, its last
output kept.
- Deliberate promotion: an explicit
waitFormatch or the user'sctrl+shift+bturns the live command into a task. The same wrapper is registered (id, footerbg: N,bash_tasks, one completion message, runtime counted from the start).execthen ends as if the command had exited 0, so Pi's bash closes its output and returns; the extension keeps that text and structured output (itsexit_codedropped,task_idadded), prefixed with the reason and elapsed time described below. Nothing in Pi's bash is left pending. After promotion the turn's abort signal no longer reaches the process and no output flows to the finished call. A command that ends in the same instant ends normally. The task'sbash_outputcontinues after the output the promoted call showed (never re-read). A truncated output's "Full output" footer points at the task's log, not at Pi's temp file, which is deleted; the result carries nodetails. The text says how long the command had actually run. waitFor: a plain substring (not a regex) matched against the stripped output as it arrives, including across two reads and inside a line that has no newline yet. On a match the call is promoted through the samepromote(), at once, even with an explicittimeout, and answers"<text>" appeared after N s; the command keeps running as background task <id>. ... Output so far: .... An explicittimeoutthen becomes the task's deadline (startedAtplustimeout, as forbackground: true); until a match it still kills the foreground call at thetimeout. If the call ends without a match (the command exits, the user pressesctrl+shift+b, or it fails), the result also says"<text>" never appeared in the output..waitForwithbackground: trueand an emptywaitForare tool errors before anything starts. A match sends no message of its own; the completion message comes when the task ends. An escape sequence split across two reads is not stripped before matching.- No automatic promotion: elapsed time and command spelling never turn an
ordinary foreground call into a task. The former
PI_BG_BASH_PROMOTE_MSvariable is ignored; it no longer enables a timed move to the background. - Shortcut:
ctrl+shift+bmoves the running foreground command to the background at once, when the user deliberately requests it. It applies to any command, includingsleepand ones with an explicittimeout. The call answersThe user pressed ctrl+shift+b after N s: this command was deliberately moved to the background as task <id>, so you are not blocked on it. Do not run it again or start a sleep in its place. .... With no foreground command running it does nothing.ctrl+bis left to Pi as cursor-left. When several foreground commands run at once (parallel tool calls), one press promotes every one of them, since the turn stays blocked until they all finish.ctrl+shift+bneeds a terminal that reports modified keys, through the kitty keyboard protocol ormodifyOtherKeys; inside tmux it needsextended-keys onas well (set -g extended-keys on). - Hint: 2 s into a foreground command, a widget below the editor shows
"(ctrl+shift+b to background)", so fast commands never flash it. The moment
the command's exit marker is seen the hint timer is cancelled and a shown
hint is cleared, before the 100 ms late-output grace; it is also cleared when
the command is promoted, aborted, or times out. No widget is set when there is
no UI. The show and clear are best-effort: a stale context's
hasUI/uigetters throwing is ignored, and the call still ends. - The one promotion path is
promote()on each entry offoregroundRuns()(foreground.ts), the set of foreground calls in flight; only awaitFormatch or the user's shortcut calls it.
Debug log
Set PI_BG_BASH_DEBUG=1 (the value 1 exactly, read on every write) to append
diagnostics to $XDG_STATE_HOME/pi-bg/debug.log (default
~/.local/state/pi-bg/debug.log, mode 0o600). Without it nothing is written
and nothing else changes. A failed write to the log is dropped, never raised.
- Format: JSON Lines. Every line has
t(ISO timestamp) andevent; task lines addtask,session,pidandcommand(on one line, as inbash_tasks). - Events:
task-start,task-exit(state,exitCode),kill-requestandkill(reason,outcome; a log-limit kill has thatreason),deadline,promote(by: "shortcut"or"waitFor",elapsedMs),session-end(the task count and the groups it kills, with anerrorline for each kill that fails) androtate. - Errors: every error the extension swallows or downgrades is an
errorline withwhere(a short tag),error(the message),codeandstack. Two expected cases are not logged: ESRCH/EPERM from signalling a group that is gone, and a missing file where absence is ordinary (the owner marker, the log-size check of a deleted log). - Rotation: at
session_start, adebug.logover 5 MiB is renamed aside (so only one Pi process rotates it), gzipped todebug.log.1.gz, and the older generations shift up; five are kept,debug.log.5.gzthe oldest. The 7-day cleanup never touches these files.
bash_output
bash_output({ id, latest?, filter? }) reads a task's log — plain or gzipped,
running or finished.
- Returns the oldest unread output from the task's read position, capped like
Pi's
truncateHead(DEFAULT_MAX_LINES,DEFAULT_MAX_BYTES), counted in raw bytes. When more is left it says "More remaining" and how many bytes; call again for the next page. The position moves only past what was returned, so calls never overlap. - A running task's half-written last line waits for its newline.
latest: truereturns the newest unread output (truncateTaillimits; the window never starts before the read position) and moves the position to the end, noting any unread bytes it skipped. With nothing unread it returns "(no new output)".- The position counts raw log bytes, so invalid UTF-8 does not lose lines. The text returned is stripped (see above) after the page is cut from the raw log.
filteris a regex applied to the stripped lines returned, so escape codes do not break a match; the position still moves past every line read, matching or not.- The exit marker is never output. The first line reports the task, worded as
in
bash_tasks:running,exited (code N),killed (reason)orexit unknown. - An unknown id, or an invalid filter, throws, which Pi reports as an error result.
- The completion message's tail is separate and never moves the position.
Structured results
bash_output, bash_tasks and bash_kill declare an outputSchema and return
a matching structuredContent, so a codemode script gets an object; the model
still reads the text. A task is described by task_id, state (running,
exited, killed or exit-unknown), exit_code only when exited and
reason only when killed, the same state the text reports.
bash_output: the state fields plusoutput(the text returned, afterfilter; empty when nothing was),truncated(more is left, orlatestskipped some) andunread_bytes(raw log bytes not yet read).statecomes from the exit marker this read saw, so a task is done whenstateis notrunningandunread_bytesis 0.bash_tasks:{ tasks }, each with the state fields, the fullcommand,started_at_ms,ended_at_msonce ended, andruntime_ms.bash_kill: one such task plusoutcome:killed,finished(had already ended),gone(had already ended, exit unknown),cleared(had ended; what it left running was stopped), orstuck/leftover-stuck(survived SIGKILL).
Annotations: bash is destructive and open-world; bash_output changes only
the read position (not read-only, not destructive); bash_tasks is read-only
and idempotent; bash_kill is destructive and idempotent. None but bash
is open-world.
Output events and completion holds
The output-event capability is tested with Pi 1.4. Start the command through the
existing bash tool with background: true, then call the native bash_watch
tool with its id. Launch permissions still apply to the original bash command;
the subscription cannot launch, adopt or kill processes, or read arbitrary paths.
bash_watch({ id, action?: "start" | "stop", contains?: string[] }):
startis the default. It returns immediately and sends new output as steering messages at the next tool gap, or starts a turn when idle. The task keeps the same process; a subscription does not interrupt a foreground tool call.- Replay begins at the start of the log, independently of
bash_output. If the log already exceeds 256 KiB, replay starts in its last 256 KiB, discards the first cut line and reports skipped bytes. At most 16 watches may be active. containskeeps lines containing any of 1–16 literal, case-sensitive substrings, each at most 256 UTF-8 bytes. It is not a regex. Omit it for all nonempty lines. Matching uses the stripped output.- A complete LF-terminated line is emitted even if the producer then goes silent. Split UTF-8 and ANSI sequences within that line are retained until framing. Invalid UTF-8 becomes replacement characters without changing byte offsets.
- Output messages label the command output as data, not instructions. Each
JSON-string-framed record includes
task_id,seq,from_byte,to_byte,textandpartial. Byte offsets use the marker-removed raw log. Repeated equal lines are separate events; consumers deduplicate their own workflow actions. - The poller reads at most 64 KiB per watch and 256 KiB total per tick, round-robin. Lines over 16 KiB raw or stripped are discarded through their LF. Pending output is capped at 32 records and 32 KiB of JSON per watch, and each message at 32 KiB, including its header. A record whose escaped JSON cannot fit is discarded.
- Only one output message per session may be sent but not yet seen in request
context. Further output coalesces locally, dropping the oldest records when
necessary. Messages report cumulative
skipped_bytes,dropped_linesandoversized_lines, with sequence numbers showing which event lines were kept. Delivery is bounded, not lossless. Recheck current external state after a gap; do not infer that an omitted event succeeded. No output notification movesbash_output's read position. - An aborted or failed agent run pauses unsolicited output wakes and recovery of lost held-completion messages. A new user run, or a run started by another extension's message, resumes them. Initial completion delivery and unheld completion behavior are unchanged.
- If Escape, abort or an RPC queue clear discards a queued output batch, the same batch/token is re-armed after the run settles. A held task's unseen completion is likewise retained and retried with its original bounded payload/token; retries respect the stop/error pause. Messages already included in request context are not re-sent, even if that run was aborted. Their held completion still needs a later normal run to commit observation. Queue-count APIs are not used to infer delivery, since they omit custom steering messages.
stoppermanently unsubscribes output for that task. Starting the same active configuration is idempotent; restarting a stopped watch or changing its filter is refused. Usebash_outputfor later reads. Stop does not kill the command.- Starting on an already-ended task reports
endedand its state; it does not replay a compressed historical log. Readbash_outputinstead. - A shrinking log stops its watch with a gap. Read failures are retried; running log rotation is not supported. Watch polling never reads gzip files.
- Completion takes precedence over output. Before the ordinary completion message,
the watch makes one bounded final scan and closes. Final unterminated text is
classified as partial; buffered event records not already sent are discarded.
The completion message for a watched task reports undelivered lines and gaps
and points to
bash_output. Compression then works as before.
bash_watch has Pi exposure model-only: the model calls it directly, even with
codemode.mode: "only"; scripts cannot call tools.bash_watch. Its structured
result reports the task state, watch_state (active, stopped, ended or
absent), and the three cumulative gap counters. Permission extensions can
authorize this tool separately. Delegation tool ceilings must include its exact
name at each intended writable owner; registering it only in a child does not
bypass a parent's ceiling.
Optional completion hold, atomic with launch
A running background task does not ordinarily prevent a delegated session from
reporting its final answer or exiting. For an operation that must keep that
owner's task unfinished, use bash({ command, background: true, hold: true }).
The default is false. hold: true requires background: true and a live completion
host registered for the exact current session. Without one it fails before
spawn; a failed spawn refunds its provisional hold.
A completion host, such as a compatible Squire implementation, checks all holds
before publishing its final task reply/tree completion or auto-exiting. No model
request loop is created merely to wait. The hold becomes ready only after the
task's terminal notification was included in a request and the agent run ended
normally. An unseen terminal, abort or provider error does not release it.
A successful bash_kill result also counts, including the top-level codemode
result when kill was nested in a script, even if the script later throws: the
recorded kill succeeded, but a normal agent run end is still required. stuck
and leftover-stuck do not;
a later successful kill is the recovery path.
A hold is not a success predicate: exit errors and cancellation can release it
once processed, and natural-exit leftovers do not delay it. The owner must inspect
the outcome and confirm cleanup with bash_kill when needed. Session shutdown
still kills all tracked groups, including leftovers. Stopping output does not
release a launch hold. Passive servers should normally use the default, no hold.
The host/provider interface is process-local data at
globalThis[Symbol.for("pi.completion-holds.v1")]:
{
version: 1,
hosts: Map<sessionId, Map<hostName, runtimeToken>>,
holds: Map<sessionId, Map<holdId, { provider: string; ready: boolean }>>
}
A host registers a fresh runtime token only after acquiring its current session, and removes only that token on every shutdown, including reload. Providers own their hold entries; hosts read them and fail closed on malformed current-session data. This extension checks live exact-session host presence before launch, writes its own provisional entries, and marks them ready after observation. Reload preserves task/watch cursors, random notification tokens and holds. Non-reload shutdown removes this provider's entries and clears subscriptions while keeping the existing process cleanup contract. There is no restart/reopen recovery, durable scheduler, cross-session subscription or automatic forge action.
Killing, listing and deadlines
- Group kill (
kill.ts): SIGTERM the task's process group (the wrapper leads it, so the command and everything it started), wait about 3 s, then SIGKILL the group if anything is left. It signals only pids this registry launched, and refuses a pid of 1 or below. A group that is already gone is not an error. After SIGKILL it waits 1 s; a group still there then is reported as such (bash_killsays the group is still running), never as gone. The grace period is 3 s;PI_BG_BASH_GRACE_MSoverrides it, for the tests. - The process port (
port.ts): the extension's only contact with real processes, with four operations: launch a task, signal a group, check whether a group is alive, check whether a pid is. It owns the errno mapping: ESRCH and EPERM both mean "gone". EPERM is what macOS answers for a group of only zombies, and for a pid that now belongs to someone else. The escalation above runs on timers, so tests can drive it. Tests replace the port through the test harness only; there is no option or setting for it. - How a kill is recorded: a killed task has state
killedand areason(killed by agent,killed by user,timed out,log limit passed) kept in the registry, and reported askilled (reason)bybash_tasks,bash_outputand the completion message. The group kill also takes the wrapper down, so no exit marker is written and the log's exit marker stays digits-only; the log-limit kill appends a__PI_BG_LIMIT__marker instead, hidden from output like the exit marker. A task whose command exits while the limit kill is in flight is reported asexitedwith its code. Session end reports nothing (the tasks are forgotten), and Pi-gone is written by the watcher as__PI_BG_GONE__:, not as a registry reason. bash_tasks(): one line per task,id | state | runtime | command. State isrunning,exited (code N),killed (reason)orexit unknown. Says "No background tasks." when there are none.bash_kill({ id }): kills as above, setsnotifiedso no completion message follows, and reports the final state. An unknown id throws; a task that already ended (even if the poller had not yet noticed) is reported as already finished; a "running" task whose group is already gone is settled as exit unknown. Two kills of one task share the same kill.- Deadline:
bashwithbackground: trueand an explicittimeout(seconds) recordsstartedAt + timeoutas the task's deadline. The poller, on the first tick after it, runs the group kill and sends the completion message sayingtimed out. Without atimeouta background task has none. /bg: in the terminal UI, a list of every task (same line asbash_tasks), running tasks first, then the most recently started. Enter shows the end of the selected task's log, stripped likebash_outputand capped like it (2000 lines or 50 KiB), opened at the bottom and scrolled with the arrows, page up/down, home and end; it re-reads the log every second and follows new output while it is at the bottom (scrolled up, it keeps its place), and never movesbash_output's read position. The list refreshes every second too, keeping the selected task.xasksKill <id>?(for a finished task,Stop what <id> left running?), showing the command on one line (newlines as⏎, cut to 200 characters), and on yes runs the group kill: a running task gets the completion message sayingkilled by user, a finished one has what it left running stopped, and you are told what happened. Esc in the output goes back to the list, which comes back after a kill too, with the tasks' current state;qor esc in the list closes it. In RPC mode, which has no custom components,ctx.ui.selectover the tasks, and a pick asks the same confirmation before the kill. Without a UI it prints the list withctx.ui.notifyand kills nothing.
Session lifetime
Tasks belong to the session that started them.
/reloadkeeps them. Pi loads extensions withmoduleCache: false, so a reload runs a fresh copy of the extension. It finds theglobalThisregistry, refreshes itspireference and replaces the poller, so one poller runs and completions go through the newest registration, once. A foreground command in flight is left running too, and is still inforegroundRuns()(kept in the registry, not in module state), waiting normally unless awaitFormatch or the user's shortcut deliberately promotes it.quit,new,resumeandforkkill everything (session_shutdown, awaited by Pi): the group kill (SIGTERM, grace, SIGKILL) on every task's group, finished ones too since aserver &outlives its command, on every foreground command in flight, and on a spawn still under way, once it resolves. The tasks are then forgotten and their completion is never reported, since the session that wanted it is over.- A crash or hard kill of Pi is caught by a separate watcher, its own
shprocess spawned detached outside the task's group right after the wrapper. It is given Pi's pid and the task's group, checks both about once a second, and exits when the group has emptied; when Pi is gone first and the group still has members, it appends__PI_BG_GONE__:<nonce>to the log and SIGKILLs them. So aserver &a finished command left behind is covered here too, as well as by session end andbash_kill//bg. No Pi is left to read the GONE marker, sobash_outputdoes not know it. A watcher that fails to start leaves the task running without a crash watch (fail open). bash_killand a/bgkill on a task whose command has exited run the group kill when anything is left in its group and say so (had already finished: exited (code 0); stopped the processes it left running, or that they survived SIGKILL). An empty group gives "already finished" as before. The task's state is not changed.
Uses no pi.events hook.