Wait for or cancel background jobs managed by the session async runtime.
Source
- Entry:
packages/coding-agent/src/tools/job.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/job.md - Key collaborators:
packages/coding-agent/src/async/job-manager.ts— job registry, cancellation, delivery suppression.packages/coding-agent/src/tools/bash.ts— explicit async bash and auto-backgrounded bash jobs.packages/coding-agent/src/task/index.ts— async task-job scheduling.packages/coding-agent/src/sdk.ts— automatic follow-up delivery for unsuppressed completions.packages/coding-agent/src/config/settings-schema.ts—async.pollWaitDurationoptions.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
poll | string[] | No | Job ids to watch. Cannot be combined with list. If omitted (and cancel is also omitted), the tool watches all running jobs owned by the calling agent. If provided, missing ids — and ids owned by other agents — are silently filtered out before waiting. |
cancel | string[] | No | Job ids to cancel before any polling. Missing ids (and other agents’ jobs) are reported as not_found; non-running ids as already_completed. |
list | boolean | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with poll or cancel. |
Outputs
The tool returns one text block plus details.
content[0].text: markdown-like plain text sections assembled by#buildResult(...):## Cancelled (N)for cancel outcomes.## Completed (N)for non-running jobs, including storedresultTextanderrorText.## Still Running (N)for jobs still inrunning.
details.jobs: array of snapshots:id: stringtype: "bash" | "task"status: "running" | "completed" | "failed" | "cancelled"label: stringdurationMs: number- optional
resultText,errorText
details.cancelledappears only whencancelwas passed; each item is{ id, status }where status is"cancelled" | "not_found" | "already_completed".
Streaming behavior:
- During a polling wait,
execute(...)emitsonUpdate(...)every 500 ms with an empty text block and freshdetails.jobssnapshots. - Final return is single-shot after a completion, timeout, abort, or immediate fast path.
Read-only snapshot path:
- Calling
jobwithlist: truereturns a markdown summary of every job spawned by the calling agent (running + completed within retention) without waiting.
Flow
JobToolis registered unconditionally inpackages/coding-agent/src/tools/index.ts; there is noasync.enabledgate (the manager may still carry bash or task jobs from before a setting change).execute(...)fetchessession.asyncJobManager. If absent, it returnsAsync execution is disabled; no background jobs are available.cancelids are processed first:manager.getJob(id)missing →not_found.- existing job with
status !== "running"→already_completed. - running job →
manager.cancel(id), which setsjob.status = "cancelled", aborts the controller, and schedules eviction.
- Polling mode is chosen with
const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0:- only
cancelpresent → return immediately, no wait. - explicit
poll, or no args at all → proceed to watch jobs.
- only
- Watch set resolution:
- explicit
poll→ resolve ids via#visibleJobs(...), dropping missing ids and jobs owned by other agents. - no
polland nocancel→manager.getRunningJobs(ownerFilter)(jobs owned by the calling agent).
- explicit
- Empty watch set returns immediately:
- if cancellations happened, return snapshots for the cancelled ids that still exist.
- else return either
No matching jobs found for IDs: ...orNo running background jobs to wait for.
- If every watched job is already non-running,
#buildResult(...)returns immediately without waiting. - Otherwise the tool waits on
Promise.race(...)across:- every watched running job’s
job.promise, - a timeout promise for the poll wait window —
manager.nextPollWaitMs(ownerId)whenasync.pollWaitDurationissmart, otherwise the fixed duration, - the tool-call abort signal when present.
- every watched running job’s
- Before waiting, it calls
manager.watchJobs(watchedJobIds). This suppresses automatic completion delivery for those ids while they are being watched. - If
onUpdateexists, a 500 ms interval sends progress snapshots from#snapshotJobs(...); one snapshot is emitted immediately before entering the race. - In
finally, the tool always callsmanager.unwatchJobs(...), clears the timeout, and stops the progress interval. #buildResult(...)deduplicates jobs, snapshots current manager state, then callsmanager.acknowledgeDeliveries(...)for every non-running job in the result. That suppresses later automatic follow-up delivery for the same completions and removes queued deliveries for those ids.- The final text groups jobs by non-running vs still-running state. A timeout is not an error path; it simply returns the current snapshot.
Modes / Variants
- Poll all running jobs: call with neither
pollnorcancel. - Poll explicit ids: call with
pollonly. - Cancel only: call with
cancelonly; cancellations happen and the tool returns immediately. - Cancel then poll: call with both. Cancellations are applied first, then the tool watches the remaining resolved
pollids. - Read-only inspection: call with
list: truefor the same snapshot data without waiting on completion.
Spawn paths that produce jobs:
packages/coding-agent/src/tools/bash.tsasync: truealways registers atype: "bash"job withAsyncJobManager.register(...)and returns a start message.- auto-background mode (
bash.autoBackground.enabled) starts the same managed job path for non-PTY commands, waits up tomin(bash.autoBackground.thresholdMs, timeoutMs - 1000), and if the command is still running returns a background-job start result instead of inline command output.
packages/coding-agent/src/task/index.ts- every
taskcall registers onetype: "task"job, unless the session has no job manager or the agent definition declaresblocking: true(sync fallback).
- every
Lifecycle and exact state names:
- Conceptual scheduling path:
pending(only task-progress bookkeeping before work starts) →running→completed/failed; cancellation changes a running async job tocancelled. - Exact
AsyncJob.statusvalues inpackages/coding-agent/src/async/job-manager.ts:"running" | "completed" | "failed" | "cancelled". - Exact per-task progress values in
packages/coding-agent/src/task/types.ts:"pending" | "running" | "completed" | "failed" | "aborted".
Side Effects
- Filesystem
- None in
job.tsitself. - Jobs being observed may already have written artifacts/results through their own tool runtimes.
- None in
- Session state (transcript, memory, jobs, checkpoints, registries)
- Reads and mutates
session.asyncJobManagerstate. watchJobs(...)/unwatchJobs(...)toggle delivery suppression for the watched ids.acknowledgeDeliveries(...)marks completed ids as suppressed and removes queued deliveries for them.cancel(...)aborts running jobs through each job’sAbortController.
- Reads and mutates
- User-visible prompts / interactive UI
- Polling emits periodic
onUpdatesnapshots every 500 ms. - Automatic job completion follow-ups are generated by
packages/coding-agent/src/sdk.tsonly for unsuppressed deliveries.
- Polling emits periodic
- Background work / cancellation
- Waiting uses a timeout plus optional tool-call abort signal.
- Cancelling a job does not synchronously await teardown; it flips state, aborts, and returns control to the manager/job promise.
Limits & Caps
- Poll wait duration comes from
async.pollWaitDuration(“Max Poll Time”) inpackages/coding-agent/src/config/settings-schema.ts:- allowed values:
5s,10s,30s,1m,5m,smart - default:
smart - fixed values block for exactly that long;
smartuses the adaptive ladderPOLL_WAIT_LADDER_MS = [5s, 10s, 30s, 1m, 5m]inpackages/coding-agent/src/async/job-manager.ts, climbing one rung per back-to-back poll and resetting to the 5s floor afterPOLL_ESCALATION_RESET_MS = 60_000ms without polling. Per-owner state is driven bynextPollWaitMs(...)/recordPollWaitEnd(...).
- allowed values:
- Progress update cadence while polling:
PROGRESS_INTERVAL_MS = 500inpackages/coding-agent/src/tools/job.ts. - Async job retention default:
DEFAULT_RETENTION_MS = 5 * 60 * 1000inpackages/coding-agent/src/async/job-manager.ts. - Manager fallback max-running limit:
DEFAULT_MAX_RUNNING_JOBS = 15inpackages/coding-agent/src/async/job-manager.ts. - Session wiring clamps
async.maxJobsto1..100before constructing the manager inpackages/coding-agent/src/sdk.ts; settings default is100inpackages/coding-agent/src/config/settings-schema.ts. - Async completion delivery retry backoff in
packages/coding-agent/src/async/job-manager.ts:- base
500ms - max
30_000ms - jitter
< 200ms - exponent capped at 8 doublings
- base
Errors
- Tool-disabled path is returned as normal text, not thrown:
Async execution is disabled; no background jobs are available. - Polling a nonexistent id is not an exception:
- with
pollonly, missing ids are dropped; if none remain the tool returnsNo matching jobs found for IDs: .... - with
cancel, each missing id is reported asnot_foundindetails.cancelledand text.
- with
- Cancelling a non-running job is not an exception; it reports
already_completedeven if the actual status iscompleted,failed, orcancelled. - Tool-call abort during polling stops waiting and returns a final snapshot through
#buildResult(...); it does not cancel watched jobs. - Failures inside the underlying async work are stored on the job (
status: "failed",errorText) and reported in normal tool output, not rethrown byjob. - Calling
list: trueagainst an empty manager returns a normal empty-list result rather than throwing; missing ids passed topollare silently filtered. - Combining
listwithpollorcancelthrows aToolError:`list` cannot be combined with `poll` or `cancel`.
Notes
jobwaits for the first watched running job to settle, not for all watched jobs. If others remainrunning, they are reported under## Still Running; the caller must invokejobagain to continue waiting.- Delivery suppression is the key difference between snapshot and automatic delivery:
- snapshots (
jobcalls withpollorlist: true) read current manager state; - follow-up delivery comes from
AsyncJobManager.#enqueueDelivery(...)andsdk.tsonJobComplete; - watched or acknowledged ids are suppressed via
isDeliverySuppressed(...).
- snapshots (
manager.cancel(id)setsstatus = "cancelled"before the underlying promise settles. The job function may later populateresultTextorerrorText;job-manager.tspreserves that text but does not transition the status away fromcancelled.- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both
jobcalls andlist: truesnapshots behave as if the id never existed.