packages/runtime/src/session-context.ts:306-317 dispatchRaw does not check this.closed before parsing, deduping, validating, and invoking handlers on the inbound frame. The other side of the path is correct — send, emitErrorEnvelope, emitHeartbeat, and the back-pressure emit all early-return when this.closed || this.transport.closed is true (see lines 148, 233, 262, 285, 434, 470) — but the inbound side has no symmetric guard. Once terminate() has been called and the close handler is removed, any frame already buffered in the transport adapter (or one that races into the registered handler before unregistration completes) will be parsed, deduped against an idempotency window that the session no longer owns, and dispatched to a handler that may try to call send (silently dropped) or persist via the event log (still happens). The visible symptom is spurious idempotency cache pressure and the occasional warning log on a session that has already been torn down.
Fix prompt: add if (this.closed || this.transport.closed) return; as the first line of dispatchRaw in packages/runtime/src/session-context.ts:306. Add a test in packages/runtime/test/session-effect.test.ts (or a new session-context.test.ts) that constructs a SessionContext, calls terminate(...), then calls dispatchRaw(...) and asserts no inboundDispatched counter increments, no log lines fire, and no handler is invoked. Also remove the inbound transport onFrame registration as part of terminate so the race window is closed at the source rather than papered over inside dispatchRaw.
packages/runtime/src/session-context.ts:306-317dispatchRawdoes not checkthis.closedbefore parsing, deduping, validating, and invoking handlers on the inbound frame. The other side of the path is correct —send,emitErrorEnvelope,emitHeartbeat, and the back-pressure emit all early-return whenthis.closed || this.transport.closedis true (see lines 148, 233, 262, 285, 434, 470) — but the inbound side has no symmetric guard. Onceterminate()has been called and the close handler is removed, any frame already buffered in the transport adapter (or one that races into the registered handler before unregistration completes) will be parsed, deduped against an idempotency window that the session no longer owns, and dispatched to a handler that may try to callsend(silently dropped) or persist via the event log (still happens). The visible symptom is spurious idempotency cache pressure and the occasional warning log on a session that has already been torn down.Fix prompt: add
if (this.closed || this.transport.closed) return;as the first line ofdispatchRawinpackages/runtime/src/session-context.ts:306. Add a test inpackages/runtime/test/session-effect.test.ts(or a newsession-context.test.ts) that constructs a SessionContext, callsterminate(...), then callsdispatchRaw(...)and asserts noinboundDispatchedcounter increments, no log lines fire, and no handler is invoked. Also remove the inbound transportonFrameregistration as part ofterminateso the race window is closed at the source rather than papered over insidedispatchRaw.