# Conflicts: # apps/web/tests/background-job-list.e2e.ts # apps/web/tests/live-job-stream.e2e.ts # apps/web/tests/snapshots/live-job-stream/settled.expected.md # apps/web/tests/snapshots/live-job-stream/streaming.expected.md # packages/api/job-controller/README.i18n.yaml # packages/api/job-controller/README.md # packages/api/job-controller/README.zh.md # snapshots/web/background-job-list/running.expected.md # snapshots/web/background-job-list/settled.expected.md
description, kind
| description | kind |
|---|---|
| Host and Client job control: stream one background job's output record to the browser without touching the model's consuming cursor, and stop a job on a human's behalf. | package-reference |
Job Controller
English | 中文
Summary
@deepseek-ai/dsh-api-job-controller owns the Host ctx.jobController service and the generated Client ctx.remote.job namespace. Its Remote stream job.observe delivers a record-declaring job's retained output from an absolute byte offset, and its job.kill Remote stops one job on a human's behalf; the Client half installs ctx.jobOutput, the reference-counted per-job observation and kill service whose accumulated views the session-header job list renders. The roster itself is not here: SessionJob rows still ride the session control stream owned by dsh-api-session-controller, and a row's record says whether this controller has anything to stream for it.
Table of Contents
Use this package
The Host controller requires the live Agent registry and the job registry (dsh-jobs-local in the shipped compositions) and fails to load without them. job.observe({ sessionId?, jobId, from? }) resolves the fenced-read caller from the request's session — an unowned job needs no session — and yields one opened anchor, coalesced output frames, then one terminal status once the job has settled and its record is drained, after which the stream closes normally. Record reads are non-consuming: the model-facing job_output cursor and completion-notice state never observe them. A job the session does not own, a job without a record, or an unknown job rejects the stream. Reconnecting callers resume by passing the last frame's next as from; a from below the oldest retained byte gets a lossy first frame instead of an error.
job.kill({ sessionId, jobId }) cancels one job with the reason cancelled by the user and leaves the terminal report unclaimed, so the owning agent's completion notice stays due; it answers { outcome: 'requested' } or { outcome: 'already-finished' }, rejects an unknown or foreign job with job/not-found, and applies the Session Controller's subagent ownership fence (session/agent-busy) to a subagent-owned live session.
The Client entry provides ClientJobOutput (ctx.jobOutput) over ClientJobOutputModel. kill(sessionId, jobId) forwards to job.kill and returns the Remote result for the caller's admission verdict. observe(sessionId, jobId) opens one Gateway stream per job however many viewers expand it, keeps a bounded render tail per job with gapBefore marking eviction or resume gaps, records the terminal status or a stream failure on the view, and drops the view after the last viewer releases. The plugin resolves the Gateway stream factory and the job namespace while its own context is current, because observation (re)opens run on caller stacks that have not declared remote.job.
Config
| Field | Default | Meaning |
|---|---|---|
observeFlushMs |
100 |
Coalescing window after new record output before an observation read, in milliseconds |
observeMaxFrameBytes |
65,536 |
Soft byte budget per observation output frame; one larger chunk ships whole |
The generated configuration catalog is the exhaustive source for accepted fields and their JSDoc.
Model Experience
None, as job observation is browser and Host control state; it registers no prompt, tool, or session event. The model's own view of the same work stays with dsh-tool-jobs.
KV Cache effect
No direct effect; observation reads never touch model requests.
Known Limitations and Deferred Work
- The record is a best-effort live preview, not a terminal transcript: producers copy stdout and stderr per poll round, so the two streams' writes inside one poll window reach the record stdout first, and the client concatenates chunks regardless of
channel. - The stream is process-local: a Host restart loses every record, and a resumed observation then anchors on an empty registry.
- Per-session fencing is enforced by the registry read; the Remote layer itself serves any connected browser, matching the session control stream's
jobsBySessionbroadcast.
Dev Note
Working context for maintainers — click to expand
None.
Runtime invariant: No companion is published. The controller is a stateless projection of ctx.jobs reads; the registry's own @deepseek-ai/dsh-jobs/invariant owns the offset relations this stream forwards.