Found during the binding-actor consolidation (PR #2005) and documented in openspec/specs/channel-binding-parity/spec.md ("Pipeline reinitialize keeps each channel's cursor discipline").
The divergence. On pipeline reinitialize while a turn is in flight, Slack discards the pending message cursor (ChannelOutputEngine.DiscardPendingCursor() — only Slack calls it). Discord and Mattermost keep theirs.
Why the Discord/Mattermost behavior looks like a message-loss bug. The pending cursor belongs to a turn the reinitialize just abandoned. If a later TurnCompleted commits it, the cursor advances past messages the session never actually processed — they are then excluded from every future gap hydration. Slack's discard means the interrupted turn's messages are re-included in the next hydration instead.
Proposed fix. Align Discord and Mattermost to Slack's discard: call DiscardPendingCursor() in their ResetForPipelineReinitialize path, add a cross-channel contract scenario (reinitialize mid-turn, then complete a turn, assert the abandoned cursor was not committed and the interrupted messages hydrate), and update the parity spec scenario to a single SHALL. Small change; the engine hook already exists.
Why not fixed in #2005. That change was zero-behavior-change by contract; this is a deliberate behavior decision, so it is tracked here for a product call.
Found during the binding-actor consolidation (PR #2005) and documented in
openspec/specs/channel-binding-parity/spec.md("Pipeline reinitialize keeps each channel's cursor discipline").The divergence. On pipeline reinitialize while a turn is in flight, Slack discards the pending message cursor (
ChannelOutputEngine.DiscardPendingCursor()— only Slack calls it). Discord and Mattermost keep theirs.Why the Discord/Mattermost behavior looks like a message-loss bug. The pending cursor belongs to a turn the reinitialize just abandoned. If a later
TurnCompletedcommits it, the cursor advances past messages the session never actually processed — they are then excluded from every future gap hydration. Slack's discard means the interrupted turn's messages are re-included in the next hydration instead.Proposed fix. Align Discord and Mattermost to Slack's discard: call
DiscardPendingCursor()in theirResetForPipelineReinitializepath, add a cross-channel contract scenario (reinitialize mid-turn, then complete a turn, assert the abandoned cursor was not committed and the interrupted messages hydrate), and update the parity spec scenario to a single SHALL. Small change; the engine hook already exists.Why not fixed in #2005. That change was zero-behavior-change by contract; this is a deliberate behavior decision, so it is tracked here for a product call.