BaseObserver.on_loop_cancelled exists precisely because "cancellation bypasses on_loop_end". But the drain in notify_observers is keyed on on_loop_end alone:
if method == "on_loop_end":
await drain_background_observers()
Consequence: on the cancellation path, passive tasks already scheduled — trajectory writes, metric flushes, event-sink appends — are never awaited. When the enclosing task tree is cancelled and the event loop closes, they are dropped: exactly the side effects the hook is meant to release.
Fix direction: drain on on_loop_cancelled too. Interacts with #2 — the drain should become loop-scoped in the same change rather than twice.
Provenance: pre-existing in ApodexHarness/miroharness/core/loop_types.py, ported verbatim by #1 — not introduced by the refactor. ApodexHarness carries the same code and needs the same change until it consumes AgentCore.
BaseObserver.on_loop_cancelledexists precisely because "cancellation bypasseson_loop_end". But the drain innotify_observersis keyed onon_loop_endalone:Consequence: on the cancellation path, passive tasks already scheduled — trajectory writes, metric flushes, event-sink appends — are never awaited. When the enclosing task tree is cancelled and the event loop closes, they are dropped: exactly the side effects the hook is meant to release.
Fix direction: drain on
on_loop_cancelledtoo. Interacts with #2 — the drain should become loop-scoped in the same change rather than twice.Provenance: pre-existing in
ApodexHarness/miroharness/core/loop_types.py, ported verbatim by #1 — not introduced by the refactor. ApodexHarness carries the same code and needs the same change until it consumes AgentCore.