fix(host): drain the dispatcher queue to empty when a step throws - #405
Merged
Conversation
`createDrainingDispatcher` reset `draining` in a `finally` but neither cleared nor re-drained `queue`, so an event enqueued re-entrantly before a `step` throw sat there until the NEXT external dispatch — a stale replay, arbitrarily later, against a state it was never computed for. Decide the policy explicitly: continue-draining. Each step is caught, the drain runs to an empty queue, and only then does the failure reach the caller — a lone error rethrown as-is (identity and triage payload intact), several as an `AggregateError`. Clearing the queue was rejected because `applyEditSettled` is the write lock's only release site, so dropping one strands the lock and the side channels deferred behind it; documenting the "effects never dispatch synchronously" invariant was rejected because nothing mechanically enforces it. The throw still escapes to the caller, so the unhandled-rejection reasoning in `effect-executor.ts` is unchanged — only its timing moves to the end of the drain; both comments asserting "try/finally and NO catch" are updated. `host-session-step.ts`'s residue baseline test, which asked to be updated consciously if the policy changed, now pins the new one.
…message - host-session-step.test.ts: fix stale PR #404 reference to the actual PR (#405) that introduced the drain-residue fix - host-session-core.ts: scope the createDrainingDispatcher JSDoc's 'applyEditSettled is the write lock's ONLY release site' claim to a still-alive panel, and note the disposed arm's teardown-only release; correct the citation from edit-settled-barrier.ts (which only consumes isWriteLockHeld) to isWriteLockHeld itself and effect-executor.ts's header - host-session-core.test.ts: assert the AggregateError message string in the multi-throw drain test, which no existing assertion pinned
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
createDrainingDispatcherhad no failure policy for a throwingstep: it resetdrainingin afinallybut neither cleared nor re-drainedqueue, so an event enqueued re-entrantly before the throw sat there until the NEXT external dispatch — a stale replay, arbitrarily later, against a state it was never computed for. This decides the policy explicitly: continue-draining.Changes
host-session-core.ts— the dispatcher catches per step, drains the queue to EMPTY, and only then lets the failure reach the caller: a lone error rethrown as-is (identity and triage payload intact, falsy values included), several as anAggregateError.drainingis still released in afinally, before the rethrow, so a caller that dispatches from its own catch handler starts a fresh drain.applyEditSettledis the write lock's only release site (edit-settled-barrier.ts), so dropping one strands the lock and the side channels deferred behind it; DOCUMENTING the "effects never dispatch synchronously" invariant leaves it unenforced by anything. It also states what continuing does NOT do (it is scheduling, not transactional recovery: the failed event's remaining effects stay abandoned, and a throwingapplyEditSettledtransition still strands the lock — a cost owned byHostSessionStepDeps.commitTransition), and that liveness is unchanged (no effect may re-dispatch unconditionally).effect-executor.ts— the two comments asserting "createDrainingDispatcherhastry/finallyand NOcatch" now say it rethrows once the drain is empty. Their substance is unchanged: the throw still escapes to the caller, so the unhandled-rejection reasoning holds; only its timing moves to the end of the drain.createDrainingDispatchertraces (drain-to-empty + error identity + no residue on the next dispatch,AggregateErrorfor multiple failures, a falsy throw not read as a clean drain, and no throw when every step succeeds).host-session-step.test.ts's residue pin, which explicitly asked to be updated consciously if the policy changed, now pins the new behaviour.Related
createDrainingDispatcherhas no failure policy for a throwingstep(out-of-scope finding from the /review-plan rounds on the settle-effects-throw barrier fix).Test Plan
pnpm compile— cleanpnpm test:unit— 5367 passedseenstops at["a"]; the multi-failure case throws the firstErrorinstead of anAggregateError)pnpm lint— clean for the touched filespnpm build+pnpm package(vsix audit: 22 entries, no violations) +code --install-extension --force