Session.append pushes the event BEFORE notifying session/event listeners,
so a throwing listener leaves the event in the log while the line after
the append (a boolean flag) never runs. Both turn-balance decisions were
gated on such flags, so a throwing listener could strand an open turn or
skip a durability checkpoint.
- loop.ts: the outer catch decided "turn/end owed" from `turnStarted`.
A throwing listener on the turn/start append left turn/start logged but
the flag false → catch rethrew and skipped turn/end → permanently open
turn (violating ADR 0017). Now decided from the log (this turn's
turn/start present), so the turn is always balanced; only a genuine
pre-push failure (non-serializable trigger — turn/start never logged) is
rethrown to the runLoop backstop. Removed the now-dead `turnStarted`.
- agent.ts inject(): the idle one-shot-turn flush was gated on a
`turnRecorded` flag set after append('turn/end'); a throwing turn/end
listener skipped the flush, losing the balanced in-memory injection turn
on crash. Now the flush decision is read from the log, the synthetic
turn/end append contains a throwing listener (turn stays balanced), and
a failing idle flush is reported via agent/error (step 0 convention) AND
the logger — mirroring the loop's post-turn/end flush path — with a
throwing agent/error listener contained.
Rewrote the test that encoded the old (buggy) "turn/start listener throw
is rethrown, no turn/end" semantics to assert the balanced-turn contract,
and added regressions for the throwing-turn/end-listener flush and the
agent/error report. Updated Agent.inject JSDoc.