refactor(jobs): type the producer face by the record declaration, tidy review residue

- `JobStart` is a union discriminated by `record`: `RecordingJobStart.run`
  receives `RecordingJob` (with `append`), `PlainJobStart.run` receives the
  plain `RunningJob`, and the registry builds the plain face without the
  method, so the former warn-and-drop path is unrepresentable.
- Remove the duplicated JSDoc opener in the registry seam and correct the
  job-list slot comment (order 20 sits before the subagent catalog's 30).
- Restore the `cordis-tool-round` session fixture whose only change was a
  re-recorded `createdAt`.
- Update the 2026-08-08 job-display note's stale service-disposal and slot
  ordering facts; archive the superseded 2026-08-25 unified-task-list note
  and point its remaining inbound links at the archived snapshot.
This commit is contained in:
Yichen Jiang committed 2026-09-03 13:15:23 +08:00
1 parent 6a4d7ee242
commit b48c05d117
35 files changed
+326 -212

No files matched your search

+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/jobs.md
jobs.md: 0610d99892da22a25590ab7efe8ff71d9a004f01
jobs.zh.md: 4c35fd7c879ddf7133c0db55df24948b40118b9d
jobs.md: 639faa8da3836cb3efc865c494cd8ca8c60f49fb
jobs.zh.md: 3d008671657847520eccd41b8a3bd75014777680
+55 -47
View File
@@ -23,42 +23,37 @@ interface JobKindMap {
## Producer contract
`JobStart` declares identity and a starter. The runtime finishes preflight before calling `run()` with the job's producer face and commits without a later failable step. Producers own execution resources; the runtime owns identity, access, and lifecycle state.
`JobStart` declares identity and a starter, discriminated by `record`: a `RecordingJobStart` receives the `RecordingJob` face, a `PlainJobStart` the `RunningJob` face without `append`. The runtime finishes preflight before calling `run()` with that face and commits without a later failable step. Producers own execution resources; the runtime owns identity, access, and lifecycle state.
```ts type-equiv
/**
* Producer declaration passed to {@link JobRegistry.start}. The runtime
* preflights access and cleanup before invoking {@link run}; the producer owns
* execution resources while the runtime owns identity and lifecycle state.
* A start with the model-facing surfaces only. Its producer face carries no
* `append`, so output the registry would have nowhere to keep is
* unrepresentable rather than dropped.
*/
interface JobStart {
/** Producer kind — also the id prefix (`bash`, `subagent`, …). */
kind: JobKind
/** One-line model-facing label (the command; the delegation description). */
label: string
interface PlainJobStart extends JobStartBase {
record?: undefined
/**
* Optional UTF-8 byte cap for each complete model-facing completion notice or
* output read, including controller status metadata. Independent of record
* retention: it bounds the consuming model surface, never
* {@link JobRegistry.readRecord}.
* Start the work after preflight and synchronously return its hooks. Called
* once with the job's producer face; a throw leaves nothing registered (the
* spent ordinal is skipped), and the producer must clean up any partially
* started resources.
* @param job - the issued id plus the live-detail writer.
*/
outputLimitBytes?: number
/**
* Owning live agent. Access is fenced by its session id, and agent disposal
* cancels and awaits the job. The instance must be the one currently
* registered under its agent id. Omitting the owner creates an unowned job,
* open to any caller until service disposal.
*/
owner?: Agent
/**
* Declares an observable output record beside the model-facing surfaces:
* {@link RunningJob.append} retains chunks in a bounded ring that any number
* of observers read at absolute byte offsets through
* {@link JobRegistry.readRecord}, and snapshots carry
* {@link JobSnapshot.outputTotal} and {@link JobSnapshot.outputEarliest}.
* Without the declaration `append` logs and drops.
*/
record?: true
run(job: RunningJob): JobHooks
}
```
```ts type-equiv
/**
* A start that declares an observable output record beside the model-facing
* surfaces: {@link RecordingJob.append} retains chunks in a bounded ring that
* any number of observers read at absolute byte offsets through
* {@link JobRegistry.readRecord}, and snapshots carry
* {@link JobSnapshot.outputTotal} and {@link JobSnapshot.outputEarliest}.
*/
interface RecordingJobStart extends JobStartBase {
record: true
/**
* Start the work after preflight and synchronously return its hooks. Called
* once with the job's producer face; a throw leaves nothing registered (the
@@ -66,10 +61,20 @@ interface JobStart {
* started resources.
* @param job - the issued id plus the record append and live-detail writers.
*/
run(job: RunningJob): JobHooks
run(job: RecordingJob): JobHooks
}
```
```ts type-equiv
/**
* Producer declaration passed to {@link JobRegistry.start}, discriminated by
* `record`. The runtime preflights access and cleanup before invoking `run`;
* the producer owns execution resources while the runtime owns identity and
* lifecycle state.
*/
type JobStart = PlainJobStart | RecordingJobStart
```
`JobHooks.done` resolves after the producer releases its resources, not merely when work finishes. Optional `readOutput` distinguishes consuming stream jobs from final-output-only jobs.
```ts type-equiv
@@ -113,34 +118,38 @@ interface JobOutcome {
## Observation record
A producer that declares `record: true` streams raw output into a bounded per-job ring through the `RunningJob` face its starter receives; any number of observers read retained chunks at absolute byte offsets through `readRecord` without touching the consuming model cursor or notice state. Job settlement ends the stream and trims retention to the settled cap — the record has no separate lifecycle. `pumpJobOutput` copies producer offset-readers (the subprocess `readFrom` family) into the record at a bounded cadence. Browsers reach the record through `job.observe`, the Remote stream of [`dsh-api-job-controller`](../../packages/api/job-controller/README.md), whose frames are listed under its Cordis API section below.
A producer that declares `record: true` streams raw output into a bounded per-job ring through the `RecordingJob` face its starter receives; any number of observers read retained chunks at absolute byte offsets through `readRecord` without touching the consuming model cursor or notice state. Job settlement ends the stream and trims retention to the settled cap — the record has no separate lifecycle. `pumpJobOutput` copies producer offset-readers (the subprocess `readFrom` family) into the record at a bounded cadence. Browsers reach the record through `job.observe`, the Remote stream of [`dsh-api-job-controller`](../../packages/api/job-controller/README.md), whose frames are listed under its Cordis API section below.
```ts type-equiv
/**
* Producer face of one registered job, handed to {@link JobStart.run} and
* valid for the job's whole life. All methods are synchronous. Writes staged
* inside the starter call are retained and become visible with the
* registration commit; after settlement — the producer's own outcome, a kill,
* or a registry-forced teardown end — both methods log and drop instead of
* throwing, so a producer's trailing flush cannot break its own teardown path.
* Producer face of one registered job, handed to `run` and valid for the
* job's whole life. All methods are synchronous. Writes staged inside the
* starter call are retained and become visible with the registration commit;
* after settlement — the producer's own outcome, a kill, or a registry-forced
* teardown end — writes log and drop instead of throwing, so a producer's
* trailing flush cannot break its own teardown path.
*/
interface RunningJob {
/** The registry-issued id (`<kind>-N`). */
readonly id: JobId
/**
* Replace the snapshot's status detail with a live progress line (`3/10`).
* @param detail - the new detail line.
*/
updateDetail(detail: string): void
}
```
```ts type-equiv
/** Producer face of a {@link RecordingJobStart}: {@link RunningJob} plus the record append. */
interface RecordingJob extends RunningJob {
/**
* Append one record chunk. Offsets advance by the chunk's UTF-8 byte
* length; an empty chunk is dropped without waking observers. Without a
* {@link JobStart.record} declaration the chunk is logged and dropped.
* length; an empty chunk is dropped without waking observers.
* @param text - the chunk text, exactly as produced.
* @param options - stream label and gap marker.
*/
append(text: string, options?: JobAppendOptions): void
/**
* Replace the snapshot's status detail with a live progress line (`3/10`).
* Works for every job, with or without a record.
* @param detail - the new detail line.
*/
updateDetail(detail: string): void
}
```
@@ -368,7 +377,6 @@ abstract wait(id: JobId, timeoutMs: number, caller?: Agent, signal?: AbortSignal
*/
abstract onJobDone(listener: JobDoneListener): () => void
/**
/**
* Register an effect-scoped observer of visible-set changes. It fires after
* every commit that changes what {@link list} returns for that owner —
+55 -47
View File
@@ -23,42 +23,37 @@ interface JobKindMap {
## 生产方约定
`JobStart` 声明身份和启动器。运行时会在完成预检后携带该 job 的生产者面调用 `run()`,随后提交注册,不再执行可能失败的步骤。生产方拥有执行资源;运行时拥有身份、访问权限和生命周期状态。
`JobStart` 声明身份和启动器,并按 `record` 判别:`RecordingJobStart` 收到 `RecordingJob` 面,`PlainJobStart` 收到没有 `append` 的 `RunningJob` 面。运行时会在完成预检后携带该面调用 `run()`,随后提交注册,不再执行可能失败的步骤。生产方拥有执行资源;运行时拥有身份、访问权限和生命周期状态。
```ts type-equiv
/**
* Producer declaration passed to {@link JobRegistry.start}. The runtime
* preflights access and cleanup before invoking {@link run}; the producer owns
* execution resources while the runtime owns identity and lifecycle state.
* A start with the model-facing surfaces only. Its producer face carries no
* `append`, so output the registry would have nowhere to keep is
* unrepresentable rather than dropped.
*/
interface JobStart {
/** Producer kind — also the id prefix (`bash`, `subagent`, …). */
kind: JobKind
/** One-line model-facing label (the command; the delegation description). */
label: string
interface PlainJobStart extends JobStartBase {
record?: undefined
/**
* Optional UTF-8 byte cap for each complete model-facing completion notice or
* output read, including controller status metadata. Independent of record
* retention: it bounds the consuming model surface, never
* {@link JobRegistry.readRecord}.
* Start the work after preflight and synchronously return its hooks. Called
* once with the job's producer face; a throw leaves nothing registered (the
* spent ordinal is skipped), and the producer must clean up any partially
* started resources.
* @param job - the issued id plus the live-detail writer.
*/
outputLimitBytes?: number
/**
* Owning live agent. Access is fenced by its session id, and agent disposal
* cancels and awaits the job. The instance must be the one currently
* registered under its agent id. Omitting the owner creates an unowned job,
* open to any caller until service disposal.
*/
owner?: Agent
/**
* Declares an observable output record beside the model-facing surfaces:
* {@link RunningJob.append} retains chunks in a bounded ring that any number
* of observers read at absolute byte offsets through
* {@link JobRegistry.readRecord}, and snapshots carry
* {@link JobSnapshot.outputTotal} and {@link JobSnapshot.outputEarliest}.
* Without the declaration `append` logs and drops.
*/
record?: true
run(job: RunningJob): JobHooks
}
```
```ts type-equiv
/**
* A start that declares an observable output record beside the model-facing
* surfaces: {@link RecordingJob.append} retains chunks in a bounded ring that
* any number of observers read at absolute byte offsets through
* {@link JobRegistry.readRecord}, and snapshots carry
* {@link JobSnapshot.outputTotal} and {@link JobSnapshot.outputEarliest}.
*/
interface RecordingJobStart extends JobStartBase {
record: true
/**
* Start the work after preflight and synchronously return its hooks. Called
* once with the job's producer face; a throw leaves nothing registered (the
@@ -66,10 +61,20 @@ interface JobStart {
* started resources.
* @param job - the issued id plus the record append and live-detail writers.
*/
run(job: RunningJob): JobHooks
run(job: RecordingJob): JobHooks
}
```
```ts type-equiv
/**
* Producer declaration passed to {@link JobRegistry.start}, discriminated by
* `record`. The runtime preflights access and cleanup before invoking `run`;
* the producer owns execution resources while the runtime owns identity and
* lifecycle state.
*/
type JobStart = PlainJobStart | RecordingJobStart
```
`JobHooks.done` 会在生产方释放其资源后 resolve,而不是仅在工作完成时 resolve。可选的 `readOutput` 用来区分会消费输出的流式任务和仅有最终输出的任务。
```ts type-equiv
@@ -113,34 +118,38 @@ interface JobOutcome {
## 观测 record
声明 `record: true` 的生产方通过 starter 收到的 `RunningJob` 面把原始输出流入按 job 划分的有界环形缓冲;任意数量的观察者通过 `readRecord` 按绝对字节偏移读取保留块,不触碰消耗型的模型游标与通知状态。job 结算即封流并把保留量裁剪到结算上限——record 没有独立生命周期。`pumpJobOutput` 以有界节奏把生产方的偏移读取器(subprocess 的 `readFrom` 家族)复制进 record。浏览器经 [`dsh-api-job-controller`](../../packages/api/job-controller/README.zh.md) 的 Remote 流 `job.observe` 读取 record,其帧类型列在下方该控制器的 Cordis API 一节。
声明 `record: true` 的生产方通过 starter 收到的 `RecordingJob` 面把原始输出流入按 job 划分的有界环形缓冲;任意数量的观察者通过 `readRecord` 按绝对字节偏移读取保留块,不触碰消耗型的模型游标与通知状态。job 结算即封流并把保留量裁剪到结算上限——record 没有独立生命周期。`pumpJobOutput` 以有界节奏把生产方的偏移读取器(subprocess 的 `readFrom` 家族)复制进 record。浏览器经 [`dsh-api-job-controller`](../../packages/api/job-controller/README.zh.md) 的 Remote 流 `job.observe` 读取 record,其帧类型列在下方该控制器的 Cordis API 一节。
```ts type-equiv
/**
* Producer face of one registered job, handed to {@link JobStart.run} and
* valid for the job's whole life. All methods are synchronous. Writes staged
* inside the starter call are retained and become visible with the
* registration commit; after settlement — the producer's own outcome, a kill,
* or a registry-forced teardown end — both methods log and drop instead of
* throwing, so a producer's trailing flush cannot break its own teardown path.
* Producer face of one registered job, handed to `run` and valid for the
* job's whole life. All methods are synchronous. Writes staged inside the
* starter call are retained and become visible with the registration commit;
* after settlement — the producer's own outcome, a kill, or a registry-forced
* teardown end — writes log and drop instead of throwing, so a producer's
* trailing flush cannot break its own teardown path.
*/
interface RunningJob {
/** The registry-issued id (`<kind>-N`). */
readonly id: JobId
/**
* Replace the snapshot's status detail with a live progress line (`3/10`).
* @param detail - the new detail line.
*/
updateDetail(detail: string): void
}
```
```ts type-equiv
/** Producer face of a {@link RecordingJobStart}: {@link RunningJob} plus the record append. */
interface RecordingJob extends RunningJob {
/**
* Append one record chunk. Offsets advance by the chunk's UTF-8 byte
* length; an empty chunk is dropped without waking observers. Without a
* {@link JobStart.record} declaration the chunk is logged and dropped.
* length; an empty chunk is dropped without waking observers.
* @param text - the chunk text, exactly as produced.
* @param options - stream label and gap marker.
*/
append(text: string, options?: JobAppendOptions): void
/**
* Replace the snapshot's status detail with a live progress line (`3/10`).
* Works for every job, with or without a record.
* @param detail - the new detail line.
*/
updateDetail(detail: string): void
}
```
@@ -368,7 +377,6 @@ abstract wait(id: JobId, timeoutMs: number, caller?: Agent, signal?: AbortSignal
*/
abstract onJobDone(listener: JobDoneListener): () => void
/**
/**
* Register an effect-scoped observer of visible-set changes. It fires after
* every commit that changes what {@link list} returns for that owner —