SIGN IN SIGN UP

fix(opencode): make the notice tracker's dispose terminal (#1540)

`stop()` aborts the controller and leaves it `undefined`, which is exactly the
state the watcher starts from, so aborting alone could not express "never
again": a `notifyUrl` that outlived `dispose()` re-opened a host event
subscription with no owner left to abort it — the leak #1536 added `dispose()`
to prevent. Both call sites dispose in a `finally` after the review returns and
`toastPlannotatorUrl` never awaits `notifyUrl`, so nothing in the type orders
the two. Unreachable on main today (the CLI child's pumps are torn down inside
`runCli`'s own `finally`), latent the moment notice delivery outlives the
command.

Add a `disposed` latch, set only by `dispose` and never by `settle`: the watcher
returns early and `admitted()` arms nothing, since after dispose no watcher is
left that could lower the flag again.

Also close the pre-existing arming window carried over from #1518. `pending` was
raised only after the un-awaited `session.synthetic` round-trip, so feedback
delivered while the notice was still posting read as not pending and queued
behind a notice that is then promoted alone as its own model turn — the #1515
symptom. `watch` becomes `posting`, which starts the watch AND arms
provisionally; the notifier wraps `synthetic` so a refusal calls the new
`rejected()` and lowers the arm again, which the existing "a rejected notice
leaves the feedback queued" test still pins.
M
Michael Ramos committed
11cbc94097d7df36b4823dc769cbbc6240399f08
Parent: 9f77a2b
Committed by GitHub <noreply@github.com> on 9/15/2026, 2:25:33 PM