Summary
src/bub/channels/handler.py’s BufferedMessageHandler can break per-session ordering guarantees in two ways:
- Concurrency bug: the same
session_id may have overlapping _handler(...) executions.
- Ordering/contract bug:
,command messages may bypass normal batching/ordering rules (depending on desired contract), leading to “later command replies before earlier normal messages”.
Both issues can corrupt session-level semantics (tape/state/output ordering).
Expected behavior (contract)
For a given session_id:
- Turns should be processed serially: downstream
self._handler(...) calls must not overlap.
- Debounce/max-wait batching should apply only to the relevant non-command message group, without being accidentally affected by later messages.
- Output order should be consistent with input order (at least within the same session).
- Session-level state/tape should be updated deterministically without race conditions.
Problem 1: Overlapping processing for the same session (concurrency)
In the original implementation, _in_processing can be cleared before the awaited handler completes. While await self._handler(...) is still running, new messages may observe _in_processing is None and start another _process() task.
Impact
- Out-of-order tape writes
- Stale state reads / state overwrite races
- Replies returned in unexpected order
- Hard-to-reproduce timing-dependent failures
Typical symptom
Send message A (slow to process), then quickly send message B in the same session; B’s turn may start before A finishes.
Problem 2: Command ordering semantics (“command may cut in line”)
The command path (messages whose content.startswith(",")) may be handled differently from normal messages. If commands bypass the serialized/batched lane, they can run while a previous normal turn is still in progress, producing the common user-visible behavior:
A later ,command gets a response before an earlier normal message.
Whether this is a bug depends on the intended contract. If the intended behavior is strict arrival-order serialization for all messages, then commands must be serialized too. If commands are intentionally allowed to “cut in line”, this should be documented explicitly and downstream code must tolerate out-of-order effects.
Reproduction ideas
-
Concurrency overlap
- Send a slow normal message in a session.
- Immediately send another normal message.
- Observe overlapping handler execution (via logs) and/or out-of-order state/tape/output effects.
-
Command cut-in
- Send a slow normal message.
- Immediately send a
,command.
- If commands are not serialized, the command response may arrive first.
Root cause (high level)
The handler mixes multiple pieces of state (_event, _timer, _in_processing, pending queue) in a way that can:
- prematurely release the “processing” guard, enabling re-entrancy, and/or
- allow special message types (commands) to bypass ordering/batching invariants.
Suggested fix direction
Refactor BufferedMessageHandler into a single per-session worker loop with a clear invariant:
- Exactly one worker task per session.
- The worker processes a queue in arrival order.
- Non-command messages are batched using debounce/max-wait without being influenced by later queue items.
- Commands are either:
- serialized through the same queue (strict ordering), or
- explicitly documented as “out-of-order allowed” (if that is the intended design).
Based on my understanding, the design is likely not intended to be asynchronous. If this behavior is actually by design, please close this issue.
Summary
src/bub/channels/handler.py’sBufferedMessageHandlercan break per-session ordering guarantees in two ways:session_idmay have overlapping_handler(...)executions.,commandmessages may bypass normal batching/ordering rules (depending on desired contract), leading to “later command replies before earlier normal messages”.Both issues can corrupt session-level semantics (tape/state/output ordering).
Expected behavior (contract)
For a given
session_id:self._handler(...)calls must not overlap.Problem 1: Overlapping processing for the same session (concurrency)
In the original implementation,
_in_processingcan be cleared before the awaited handler completes. Whileawait self._handler(...)is still running, new messages may observe_in_processing is Noneand start another_process()task.Impact
Typical symptom
Send message A (slow to process), then quickly send message B in the same session; B’s turn may start before A finishes.
Problem 2: Command ordering semantics (“command may cut in line”)
The command path (messages whose
content.startswith(",")) may be handled differently from normal messages. If commands bypass the serialized/batched lane, they can run while a previous normal turn is still in progress, producing the common user-visible behavior:Whether this is a bug depends on the intended contract. If the intended behavior is strict arrival-order serialization for all messages, then commands must be serialized too. If commands are intentionally allowed to “cut in line”, this should be documented explicitly and downstream code must tolerate out-of-order effects.
Reproduction ideas
Concurrency overlap
Command cut-in
,command.Root cause (high level)
The handler mixes multiple pieces of state (
_event,_timer,_in_processing, pending queue) in a way that can:Suggested fix direction
Refactor
BufferedMessageHandlerinto a single per-session worker loop with a clear invariant:Based on my understanding, the design is likely not intended to be asynchronous. If this behavior is actually by design, please close this issue.