@spinejit/pi-background-job
Background jobs and output monitors for Pi: exits and new log lines wake the agent by themselves.
Package details
Install @spinejit/pi-background-job from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@spinejit/pi-background-job- Package
@spinejit/pi-background-job- Version
0.1.0- Published
- Oct 23, 2026
- Downloads
- 248/mo · 25/wk
- Author
- ghabix
- License
- Apache-2.0
- Types
- extension
- Size
- 86 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions/background-job.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-background-job
English | 简体中文
Background jobs and output monitors for Pi, modeled on Claude Code's background tasks and Monitor. The agent starts a build, a server, or a log watch, keeps working, and gets notified when an event occurs—no polling and no sleep loops.
background_job(action="start", command="npm run build", name="build")
background_job(action="status")
background_job(action="stop", name="build")
monitor(command="tail -f app.log | grep --line-buffered -E 'ready|ERROR'", description="errors in app.log")
Why it is built this way
- Events, not polling. When a job exits or a monitor collects a batch of new lines, the extension delivers a message to the session that started it. An idle session starts a new turn; a busy session receives the message after the current tool batch finishes, before the next model call. The agent never spends turns polling job status.
- Owned by the Pi process. Jobs live only as long as the Pi process that started them; quitting Pi stops every running job. There is no daemon to install or clean up. Work that must outlive Pi belongs in systemd or tmux.
- One mechanism, two tools.
background_jobandmonitorshare the same job runtime with different watches: one watches for process exit, the other watches for stdout lines. - Plain files, one writer. Each Pi process stores its job records and logs in its own directory and is the sole writer there, so no file locking is needed.
Features
- Start, list, and stop jobs by name;
/jobslists jobs and lets you stop a running one. - Exit messages include the exit code or signal, runtime, log path, and the last 20 lines of output.
- Monitors batch stdout lines (200 ms) and stop automatically on timeout (default 5 min, at most 30 min) or flood (more than 30 messages a minute).
stopsignals the entire process group with SIGTERM, followed by SIGKILL after 2 s.- The footer shows how many jobs and monitors of the current session are running.
- Works inside codemode scripts in both
onandonlymodes. - Finished records and logs older than 30 days are swept automatically while running jobs stay untouched.
Install
pi install npm:@spinejit/pi-background-job
Try it for one session without installing:
pi -e npm:@spinejit/pi-background-job
Use foreground bash when the current turn needs the result before doing anything else, such as a test run whose outcome decides the next step. Pair this package with pi-timeout-policy so foreground commands cannot hang indefinitely.
Reference
Messages
| Tool | Job watch | Messages |
|---|---|---|
background_job start |
exit |
one, when the shell exits: exit code or signal, runtime, log path, last 20 lines |
monitor |
lines |
one per batch of new stdout lines (lines within 200 ms form a batch), then one when the watch ends |
Each notification is a custom message (customType: "background-job") sent with triggerTurn: true and deliverAs: "steer". An idle session starts a turn immediately; a busy session receives the message after the current tool batch finishes, before the next model call. Messages go only to the session that started the job. When another session is active, the extension suppresses delivery and still writes the completion state to the job record. details.toolCallId holds the ID of the tool call that started the job (calls from a codemode script use Pi's nested ID <calling id>/<n>), so a host can treat the message as that call's deferred output—for example, hiding it together with the call's result.
A monitor's watch ends when its command exits, after timeout_ms elapses (default 300000, capped at 1800000), or when it emits more than 30 messages in 60 s. Only stdout lines trigger events; stderr goes to the log file. Each message includes at most 40 lines / 4 KiB of output, while the full output stays in the log.
background_job stop sends SIGTERM to the job's process group, follows up with SIGKILL after 2 s if processes remain, and returns the final state in the tool result without sending a separate exit message. Processes that have left the process group (for example via setsid) are not reached.
Codemode
background_job and monitor keep Pi's default direct exposure because they manage external shell commands rather than session structure or interactive approvals. Enable codemode with defaultTools: ["+codemode"]; scripts can call tools.background_job({...}) and tools.monitor({...}) in both codemode.mode: "on" and "only".
Nested calls retain Pi's tool_call, tool_result, and execution events with a parentToolCallId. Their immediate return values live inside the codemode result rather than separate transcript tool results, while job and monitor notifications still arrive as session-scoped custom messages that wake an idle model or steer it after the outer codemode call finishes. The footer continues to track running jobs and monitors; nested calls do not create separate transcript cards. A script deadline does not own an already-started job: only job startup honors a pre-aborted signal, and a running job continues after a later script cancellation. In print and JSON modes, the lifetime limit described below applies.
Lifecycle
Each Pi process stores its jobs in <Pi agent dir>/state/background-job/<bootId>/<pid>-<start time>/ (typically under ~/.pi/agent/state/), with one JSON record per job name and one log file per run. Aside from stopping an orphan job, only the owning Pi process writes there, so no locking is required. When the shell exits, the owner writes end into the record (exit code, signal, and stop reason when ended by this extension) before the name can be reused in that process.
Job names are unique within a Pi process. status lists jobs across all Pi processes: running jobs from any working directory, plus finished jobs from the current working directory. A job name resolves first to this process's own job, and otherwise to the newest job of that name from another Pi process; stop refuses to stop a job owned by another live Pi process, but stops an orphan job whose owning Pi process has exited.
/reloadkeeps jobs running; the reloaded extension instance takes over delivery. Any message generated between the old and new instances is held and delivered on the nextsession_start./new,/resume, and fork keep background jobs running, but stop monitors belonging to the session being left.- Quitting Pi (including via SIGTERM or SIGHUP) kills all jobs of that Pi process. Jobs that must outlive Pi belong in systemd, tmux, or another supervisor.
- In print and JSON modes, the run ends as soon as the model stops, so only messages that arrive during the run are delivered; the start result notes this constraint.
When a Pi session starts, records and logs older than 30 days are removed unless their job is still running. Directories owned by other live Pi processes are left untouched, while empty directories from earlier boots and dead Pi processes are removed.
The OS can reuse a process-group ID after every process in the group has exited. On Linux, the record stores the group leader's start time, and a group whose current leader started at a different time counts as Dead. If the leader exits while other processes remain in the group, the owner records them in end.leftovers; a leaderless group counts as the job's group only while at least one recorded leftover process remains in it, so an unidentifiable leaderless group is never signaled. Leftover processes of a job whose Pi process died before its group leader exited therefore cannot be stopped through this extension.
The footer shows how many jobs and monitors of the current session are running. /jobs lists known jobs, and selecting a running job offers to stop it. Stopping a job owned by this Pi process reports its exit to its session like any other exit (when that session is active); stopping an orphan job sends no exit message because no Pi session observes it.
The detached shell uses Pi's configured shell (getShellConfig()), inherits the process environment, and prepends Pi's managed <Pi agent dir>/bin to PATH. This package does not read the shellCommandPrefix setting; include any required prefix in the command itself.
Limits
- Unix only (process groups). Windows support would require a separate process-tree or job-object adapter.
- By design, jobs do not outlive the Pi process that started them.
- Processes that leave their process group (for example via
setsid) are not reached bystop.
Compatibility
Tested with Pi 1.3.0 and 1.4.0 on Node 22.23. Requires Node >= 22.19. Pi's packages are "*" peer dependencies.
Development
npm test # core and event layer
npm run test:e2e # real Pi sessions driven by a scripted model
npm run test:codemode # real codemode with paired direct and nested calls in on/only modes
End-to-end tests use a temporary PI_CODING_AGENT_DIR, leaving your user Pi settings untouched. The coexistence test also loads pi-timeout-policy from a checkout next to this repo (or from PI_TIMEOUT_POLICY_DIR) and is skipped without one. The core and end-to-end suites intentionally send SIGTERM and SIGKILL to the test jobs they spawn and delete expired test records.
More from @spinejit
- pi-btw-cached:
/btwside questions on the main session's own request. - pi-timeout-policy: one timeout policy for bash, grep, and find.
- pi-compact-tree: a compact process tree for Pi's terminal UI.
License
Apache-2.0. See LICENSE.