fix(jobs): constant pump waits, quiescent client disposal, record flag on the roster

Review corrections on the observation record:

- `pumpJobOutput` subscribes to `done` once and keeps one pending timer;
  racing the same pending promise every poll round retained a reaction
  per round for the life of the job.
- The `ctx.jobOutput` effect disposer is async and awaits every open
  carrier's `dispose()`, so the fiber reports unloaded only once the old
  iterators are quiescent.
- `SessionJob.record: true` replaces `outputTotal`: `append` never commits
  a roster change, so the mirrored count was stale while output flowed;
  the live offsets ride the observation stream's `opened` anchor.
- The record is documented as a best-effort live preview whose
  cross-stream reordering is bounded by the poll interval.
- The web e2e scenarios hold their background job on a test-owned barrier
  file instead of a wall-clock sleep, and each is one test rather than a
  chain sharing a job id.
This commit is contained in:
Yichen Jiang committed 2026-09-03 11:23:41 +08:00
1 parent e42eb13655
commit 761282724b
36 files changed
+212 -102

No files matched your search

@@ -212,6 +212,6 @@ function jobView(job: JobSnapshot): SessionJob {
...(job.detail === undefined ? {} : { detail: job.detail }),
startedAt: job.startedAt,
...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
...(job.outputTotal === undefined ? {} : { outputTotal: job.outputTotal }),
...(job.outputTotal === undefined ? {} : { record: true as const }),
}
}
+5 -4
View File
@@ -489,11 +489,12 @@ export interface SessionJob {
readonly startedAt: number
readonly finishedAt?: number
/**
* Total UTF-8 bytes the job's observation record holds so far. Present
* exactly when the job declared a record — the row is observable through
* the job controller's `job.observe` stream.
* Present exactly when the job declared an observation record: the row is
* observable through the job controller's `job.observe` stream, whose
* `opened` anchor carries the live byte offsets. The roster refreshes on
* lifecycle commits only, so it mirrors no byte count.
*/
readonly outputTotal?: number
readonly record?: true
}
/** Complete live control baseline emitted once per control stream generation. */