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