fix(tasks): correct the change-feed contract and its documentation

Review found the `onTasksChanged` teardown reasoning inverted. The comment
claimed every registration is an effect on the registry's own fiber, so
listeners would be gone before service disposal empties the store — but the
traceable proxy rebinds `this.ctx` to the CALLER, which this package's own
HMR-safety test already proves. The only shipped consumer registers from the
api-proxy mux stream, so it was still listening and simply kept the rows it
last received. Service disposal now announces the emptied set, and teardown
announces its stopping transition immediately instead of leaving an observer
on `running` for however long a slow producer takes to release.

Two documentation claims were false in the opposite direction: the Agent Note
and the ui-task README both said an unowned task is invisible in the header,
while `list(caller)` returns unowned tasks to every caller, the carrier fans
their changes out to every subscribed session, and this PR's own test asserts
exactly that. The note even contradicted itself two sections earlier. Both
sides now state the real asymmetries — another session's tasks, and the
process-local registry emptying on restart.

The "no Web path calls the consuming `ctx.tasks.read()`" invariant claimed a
test that did not exist; the carrier suite's producer had no `readOutput` at
all, so a stray read would have failed nothing. Its producer now counts cursor
consumption and the lifecycle and baseline paths both assert zero.

Also: a session created after the mux opened now receives the task baseline it
missed, the popover samples its clock when it opens rather than at mount, and
a failed task's unbounded producer detail elides instead of widening the row.
This commit is contained in:
Yichen Jiang
2026-08-10 13:37:18 +08:00
parent 0a0a75730f
commit 15d452d7ea
21 changed files with 192 additions and 29 deletions

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/tasks.md
tasks.md: fd4041698de2fd19156c2a87d378d7fafdd1d15a
tasks.zh.md: 00c8aab2794f99678e9707d2dfb7c67bf411b24d
tasks.md: 2ee062e1c410f7a26af273ca9adc70a978f25563
tasks.zh.md: 9b6ea8260e439eca2ffffa3b50fb030f117c495c

View File

@@ -248,8 +248,14 @@ abstract onTaskDone(listener: TaskDoneListener): () => void
/**
* Register an effect-scoped observer of visible-set changes. It fires after
* every commit that changes what {@link list} returns for that owner —
* registration, the stopping transition, settlement, and owner-disposal
* removal — so an observer re-reads rather than accumulating deltas.
* registration, every stopping transition (including the one teardown
* performs before it awaits a slow producer), settlement, owner-disposal
* removal, and the emptying that service disposal commits — so an observer
* re-reads rather than accumulating deltas.
*
* The registration binds to the CALLING fiber, so an observer mounted outside
* this service still receives the disposal emptying; that is what stops a
* consumer from retaining rows after the registry unloads.
*
* This is not a superset of {@link onTaskDone}: that one delivers the terminal
* record under first-wins semantics a control surface couples to notice

View File

@@ -248,8 +248,14 @@ abstract onTaskDone(listener: TaskDoneListener): () => void
/**
* Register an effect-scoped observer of visible-set changes. It fires after
* every commit that changes what {@link list} returns for that owner —
* registration, the stopping transition, settlement, and owner-disposal
* removal — so an observer re-reads rather than accumulating deltas.
* registration, every stopping transition (including the one teardown
* performs before it awaits a slow producer), settlement, owner-disposal
* removal, and the emptying that service disposal commits — so an observer
* re-reads rather than accumulating deltas.
*
* The registration binds to the CALLING fiber, so an observer mounted outside
* this service still receives the disposal emptying; that is what stops a
* consumer from retaining rows after the registry unloads.
*
* This is not a superset of {@link onTaskDone}: that one delivers the terminal
* record under first-wins semantics a control surface couples to notice