Repository navigation
feat: consume all queued messages in one turn - #801
Open
AlexanderZ-Band wants to merge 139 commits into
Open
AlexanderZ-Band wants to merge 139 commits into
AlexanderZ-Band wants to merge 139 commits into
Conversation
Batching is an execution policy, so SessionConfig owns it: off by default, with a ceiling of 10 messages per turn validated from 1 to the platform's page size. Enabling it with a non-default model retry count is a configuration error rather than a silent override. The page size and retry default each get one named definition in runtime/types.py. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The prompts now embed band-sdk-core's band_no_reply description, which asks the model to consider every current input together and decline only when the turn as a whole needs no answer. SendMessageInput no longer restates a latest-message decline rule, and the turn-completion guidance refers to the current inputs. A conformance test pins the canonical text on each prompt surface and the advertised tool description. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ycle mark_processed_result and mark_failed_result perform the single logical processed/failed write and classify it as ACCEPTED, UNCONFIRMED, NOT_FOUND or REJECTED. mark_processed and mark_failed become explicit-ACCEPTED projections with unchanged bool, STOP and cancellation behavior. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT
…ent owner The live, backlog and control paths each repeated claim, admission, run and acknowledgement bookkeeping. A single attempt runner now owns it for a tuple of originals carried on the TurnScope: claims are taken together, every original is marked processing in order, and the scope is admitted only after the last acceptance, so a control that lands earlier consumes nothing and leaves the messages recoverable. The terminal write each original owes is stored in the claim registry at the synchronous point where the handler's outcome is known, before any cleanup or acknowledgement await, and is written from that stored intent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ion-to-consume-all-queued-messages-in-one-turn
Terminal acknowledgement recovery now reads the intent stored in the claim registry and dispatches on it: a processed intent writes processed, a failed intent writes failed with its stored error, and neither is ever inferred from the other. The runtime branches on the detailed settlement result. An accepted write completes the original; a proven 404 or a validated no-processing-attempt 422 parks the unchanged intent so it stops blocking unrelated work without being replayed; an unconfirmed write keeps its intent however many retries pass, and ordinary execution in that room waits behind it. A reconnect or rejoin unparks refused writes so each is retried once. A recovery pass attempts every pending original, so one bad write cannot starve its siblings. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
… STOP Cursor binds session-less notifications to the active turn, so one provider turn runs per adapter. The waiting turn lock queued a second room's input behind the first and let a detached turn outlive STOP, retirement or shutdown. The active CursorTurn now owns the slot: a synchronous check-and-reserve refuses an ordinary input with InputNotAccepted while it is held. The turn owns its provider task; STOP, INTERRUPT, room retirement and adapter shutdown cancel it through ACP's CancelledError path, once. The slot is released only when the task actually terminates, including cancellation before its first step, and a stale turn never unbinds or cancels its successor. The ACP client adapter's own error and timeout paths use a non-joining _teardown_room, so a turn's cleanup cannot wait on itself. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A turn can now carry several original messages. AgentInput keeps each original with its own sender, and a singleton turn is the default. Adapters opt in through supports_batch_input and on_messages; consume_all_queued refuses, at start, an adapter or preprocessor that cannot run a batch, and one-shot invocation refuses it outright. The terminal-write settlement result moves to band.core.settlement so the lifecycle and runtime share it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ion-to-consume-all-queued-messages-in-one-turn
With consume_all_queued the room merges the ascending actionable page with the originals its WebSocket delivered into one bounded, chronological prefix and runs it as a single attempt. A retained original the list omits (a thread reply) runs alone once the scan reaches its position, a local non-message event ends the segment, durable FAILED deliveries are never selected, and a page of skipped inputs continues the scan without waiting for the periodic resync. Refused or stopped originals now keep their payload in both modes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The release carries the terminal-intent retention and parked-ack APIs the room runtime uses. Settlement results map to core's ParkedReason explicitly. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Adds the optional CapacityWaitingAdapter contract. Cursor keeps one FIFO of per-room capacity requests tied to a room generation; only the first waiter may take a free provider slot, and withdrawal is identity-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Add the user guide for consume_all_queued and extend the lifecycle docs with admission order, stored terminal intents and settlement results, queued-message selection and event ordering, the actionable-page read, delivery_status preservation through the pinned REST client, and Cursor's provider-slot ownership and cancellation. Doc snippets now run their coroutines for real. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A message the adapter would handle as a control is recognized read-only, before it is claimed or marked processing, so an ordinary candidate probed during recovery receives no write. Recovery scans the deferred head and retained WebSocket originals as well as the REST page, and each candidate is classified after the previous control ran. In batch mode a recognized control ends the turn before it and runs alone; a control the handler then declines runs as an ordinary input. An adapter that overrides the handler without a recognizer is still probed for every message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ion-to-consume-all-queued-messages-in-one-turn
…ion-to-consume-all-queued-messages-in-one-turn
History for a batch stopped before the first original only when that original was already stored; a later original leaked into history when the first was not, then arrived again as a current input. Drop every original from history, and share the room-context fixture the batching suites use. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
SimpleAdapter gains on_turn (the single model-input builder an adapter overrides), judged_turn (the one outcome judge, now behind run_judged_turn and on_messages alike) and convert_history. on_messages runs a judged turn over the originals once an adapter declares supports_batch_input. format_current_inputs renders each original as its own JSON record with message id, sender id/name/type and timestamp, separated from history. The Anthropic adapter is the reference: on_message delegates to on_turn and it declares batch support, proven by a probe-driven conformance suite with a drift guard for declaring adapters. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A room whose ordinary input is refused only for shared adapter capacity keeps one request in the adapter's line, tagged with the scheduler's own generation. Admission failure, terminal-write backpressure, a claim held elsewhere, STOP, shutdown and a cleared deferred head withdraw it; a replacement room's request can never be cancelled by the room it replaced. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ion-to-consume-all-queued-messages-in-one-turn
The baseline suite rejects any collected test that requests a fixture named peer without its adapter marker, which failed bare collection in CI. The settlement peer fixture is renamed, and the new guide is formatted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
LangGraphAdapter routes on_message and the batch entry point through one on_turn builder that renders every current input with format_current_inputs and runs the graph once. Batch support is proven by a probe that drives a compiled graph and reads back the state it received. The two bootstrap tests that asserted the legacy single-message prompt now expect the current-input rendering. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
PydanticAIAdapter routes on_message and the batch entry point through one on_turn builder that hands the agent a single prompt rendered by format_current_inputs. Batch support is proven by a probe that runs the real agent on a scripted FunctionModel and reads back what the model received. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
AgnoAdapter routes on_message and the batch entry point through one on_turn builder whose run input ends with a single user message rendered by format_current_inputs. Batch support is proven by a probe that runs a real Agno agent on a recording model. The Agno tests that asserted the legacy single-message prompt now expect the current-input rendering, and the private run helpers take the turn's message ids and originals instead of one message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
StrandsAdapter routes on_message and the batch entry point through one on_turn builder whose prompt is rendered by format_current_inputs, with the participant and contact updates still folded into the same user message so Bedrock's alternating-role rule holds. Batch support is proven by a probe that runs a real Strands agent on a recording model. The Strands tests that asserted the legacy single-message prompt now expect the current-input rendering. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
on_message becomes a turn of one through on_turn, which renders the current inputs with format_current_inputs and runs one function-calling loop, so a singleton and a batch share it. The adapter declares supports_batch_input, proven by a registered probe that reads the conversation the real adapter sends to generate_content. A later-turn test pins that earlier originals stay in the room's conversation once and in order. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
on_message becomes a turn of one through on_turn, which renders the current inputs with format_current_inputs into ADK's single user message after the replayed transcript. After the turn each original joins the room transcript as its own entry, in the converter's own `[name]: content` shape, so a later turn replays every member once and in order. The adapter declares supports_batch_input, proven by a registered probe that reads the request ADK's real runner hands its model. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
on_message becomes a turn of one through on_turn, which hands the room agent one respond call whose message argument is the current inputs rendered with format_current_inputs. The respond docstring the model reads no longer says there is exactly one message. The adapter declares supports_batch_input, proven by a registered probe that reads what NOOA's real agent loop sends its model. Existing NOOA tests asserted the old singleton `[Alice]: text` line; they now read the current inputs the model was shown, which also pins their order. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…nly control recognition OpenCode now builds one prompt for a singleton and a batch through on_turn and declares supports_batch_input, proven by a batch probe over the prompt actually submitted to opencode serve. Each current input is rendered with its own sender, the per-turn system prompt drops the single requester fields, and approval notices go to the distinct senders of the admitted input for the life of the turn. is_control_message reads pending asks through the same route the control handler uses, so a pending question makes free text an answer and a pending approval recognizes only its own vocabulary, without creating room state, claiming an ask or stopping its expiry timer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
INTERRUPT bumps the control revision without stopping the room, and the drain read every revision change as the end of the pass. An attempt the INTERRUPT consumed, or an INTERRUPT landing during a page read, therefore stranded the work queued behind it until the 60 to 120 second idle resync, where singleton mode runs the next event at once. A consumed attempt now continues the drain, and a stale page is read again while the room still runs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A control that was consumed, interrupted or had failed for good was returned as deferred, which made the settled delivery the deferred head. Input arriving afterwards then only ran control scans and waited for the idle resync, and the head kept competing for shared capacity. Consumed heads were never cleared either. A settled control now releases its payload, clears the head and its capacity request, a deferred one withdraws its capacity request, and a scan that cleared the head drains the input retained behind it in both modes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ngleton mode Singleton mode retained an original only when its attempt was deferred. A STOP mid-turn, before the model ran, or an arrival while the room is stopped left the original unretained, so after PLAY neither the list nor /next could bring back a thread reply and it stayed processing for good. Every non-advancing outcome now keeps the payload, as batching already does; PLAY finds it through the retained line. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…trol leaves the line Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…o-consume-all-queued-messages-in-one-turn Main split runtime/types.py into owner modules and made SessionConfig a settings class. The batching settings move onto that class: the ceiling is a bounded field that rejects a bool, and the retry rule is a model validator. A numeric string or integral float now coerces like every other setting, which keeps the ceiling configurable from the environment. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Three tests and the decision-audience suite typed '/cursor select <token> allow-1' and '/cursor decisions' inline although select_command and ROOM_COMMAND already own that wire text. Route them through the kit so a change to the command vocabulary has one place to land. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The package docstring claimed every framework import stays inside its probe function, but the Anthropic probe imports at module top because every lane venv installs it. State the actual rule: only a framework some lanes lack is deferred. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ared turn input The batching kit only imported AGENT_ID and ROOM_ID from tests/runtime/helpers, yet twelve suites reached them through the kit. Each now imports them from helpers directly, so the kit is no longer a second path to the same values. The agent-input test drops its private AgentInput builder for the invocation kit's turn_input, rebuilt without originals through dataclasses.replace. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A refused input stays at the head of the line. When handling a later control changes what that head classifies as, the next arrival must offer the head to control handling again, even when only the room's retained original carries it because the platform list omits it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…edicates Both predicates already check for a MessageEvent, yet four callers repeated the isinstance test only to narrow the type. Typing them as TypeGuard[MessageEvent] lets the callers rely on the predicate alone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…essage The backlog path checked a message's processed status before claiming it, and the attempt then checked it again. The attempt's check also reads the hydrated context, so it is the single owner. A message another claim holds now defers instead of completing, as it did before batching. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
None carried 'another pass is due' across the batch step, boundary, empty-scan and selection returns, so a stray return None silently continued the drain. ResyncOutcome now has a PROGRESS member the drain loops on, and those steps can no longer return None. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A repeated selection, a retained original released as skipped and a head deferred behind a held claim each ended a pass silently, so a reply that never came could not be explained from the logs. Each now logs its message ids and disposition once at DEBUG. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The refusal was caught without its exception, so a host that is busy and one that reserved its slot for a room waiting longer looked identical in the logs. The not-accepted line now carries the refusal's message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Selection already used band.core.timestamps.assume_utc; the attachment expiry check, Letta's time-ago formatter, the desktop room-view cursor and message timestamp, and both CrewAI flow timestamp coercions each retyped the same "naive means UTC" one-liner. They now call the helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
Removing the UTC normalization from MessageKey.of_message passed every batching test, because the only naive fixtures went through of_event. The REST message path is the one the singleton drain compares against retained WebSocket originals. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
DefaultPreprocessor and the ACP client adapter each cut history at the first original and then filtered the turn's other originals out of the prefix, because messages_before took a single id. It now takes the turn's ids and cuts at the first one stored, so neither caller filters and format_history_for_llm keeps its single-id exclusion for the runtime callers. When the first original is not stored yet but a later one is, entries after that later original are now excluded as well, as they already were when the first is stored: they are pending turns of their own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
AlexanderZ-Band
disabled auto-merge
October 14, 2026 19:35
A host that refuses an input leaves it pending, and the room's next periodic pass offers it again; the new test flips the host to accepting after the first refusal and expects the turn at the next interval. The batching host now keeps each refused offer so a test can wait for the refusal instead of for a side effect of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
No adapter implements CapacityWaitingAdapter since Cursor binds one provider process per room, so the optional protocol, the scheduler's register/withdraw calls, the per-incarnation generation counter and the capacity parameter threaded through Agent, PlatformRuntime and AgentRuntime served nothing. A refused input still stays pending and is offered again on the next arrival, periodic pass or control, exactly as before; the register/withdraw calls only informed the adapter and never scheduled anything. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
tests/runtime/test_deferral.py held ten module-level test functions; each concern now lives in its own `Test...` class under tests/runtime/deferral/, with the shared approval adapter and wire envelope in kit.py. The 15 test cases are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The live case of the approval-beyond-first-page test only asserted `paged or not must_page`, which accepts a live approval that pages. Asserting `paged is must_page` failed about one run in fifteen: with a 10 ms periodic wake, a due wake scanned the room before the approval was taken from the queue (or before B was retried), left the scan cursor at the end of page one, and the approval's own scan then paged. The live case now has no periodic wake and queues the scheduler pass that retries B right behind the approval, ahead of ordinary traffic, so the first scan reads the first page and the retained approval releases B. The recovery cases keep the periodic wake and must page. The assertion is now exact. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ked and run once An INTERRUPT that lands while a room hydrates, before any original is admitted, must write nothing for the attempt, keep a WebSocket-only original in memory and refund the charge, so the next pass marks and runs each original exactly once. Covers the batching and singleton shapes, listed and WebSocket-only. The admission kit gains the /next and actionable-page reads as scriptable holds and one shared definition of the queued pair and the singleton shapes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
INTERRUPT bumps the control revision without stopping the room, and the recovery pass still read every revision change as the end of the pass. An INTERRUPT landing during the startup or recovery /next read or during a retried terminal write, or one that consumed a singleton attempt, stranded the queue behind it until the 60 to 120 second idle resync. A STOP still ends the pass. The same guard also skipped the deferred head of a superseded singleton attempt, so an original deferred by a pre-admission INTERRUPT could be overtaken by a later live arrival. A deferred attempt now keeps its place at the head. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A room whose host also handles control messages scans every arrival for them, which the parking harness could not exercise. The harness can now serve such a host, start a room under an outage of every terminal write, and list the terminal writes the platform received more than once. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…ion interval Every entrance that meets a pending terminal intent retried it at once, so during an outage each arrival cost one write per pending original: the control scan wrote them per arrival and again inside every reconciliation pass, and a deferred live arrival with no deferred head ran a full reconciliation per arrival, growing with the number of pending intents. An unconfirmed write now waits out idle_resync_seconds before any entrance retries it. A due reconciliation and a requested resync (reconnect, room start, PLAY) lift the wait and retry every pending write once, so recovery needs no arrival and a reconnect still retries at once. The control probe and a live duplicate retry only after the interval, and otherwise leave the write to the next pass. Parked intents, settlement results and the stored intent are unchanged; no model retry is added. Tests that pinned an immediate entrance retry now wait the interval or assert the paced behavior. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
The retry now waits a full reconciliation interval, so the test advances sixty virtual seconds. Polling every 10 ms there never advances the virtual clock on Windows with Python 3.12 or older, where a timer inside the 15.6 ms clock resolution fires without a jump, and the loop spins until the test timeout. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
A control's scope is admitted before its processing write, so an INTERRUPT that lands afterwards consumes it, which holds while its write or handler is pending. During the hydration between the two, the probe instead read any revision change as a deferral and left the control in processing, to be handled again later. Only a change no control caused still defers; an INTERRUPT is settled by the cycle's own unwinding and a STOP leaves the control recoverable. The admission kit can now serve a host that recognizes and handles controls. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
…l write The room keeps ordinary work deferred while a terminal write is unconfirmed, which every path does by making the attempt's first original the deferred head. The live path with batching off never did: a delivered original whose write stayed unconfirmed returned to the loop without a head, so each further arrival ran the model again during an outage. The attempt that finds its write pending now sets the head itself, as a refused input already does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT Signed-off-by: Alexander Zaikman <alexander.zaikman@band.ai>
This branch has not been deployed
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.
Why
A room busy with one turn accumulates messages, and the SDK then runs each as its own model turn. The agent answers stale requests one by one instead of seeing the whole queue at once (INT-1770). Retaining terminal outcomes also had gaps: a turn cancelled before its pending write was stored, or a lost ACK, could re-run the model or lose the outcome.
What
SessionConfig.consume_all_queued(off by default) andmax_queued_messages(1–100, default 10). This is an execution policy, not aCapability. Batch mode rejects a non-default retry count, hosts or adapters that cannot batch, and one-shot invocation, all at start.mark_processingis accepted and ownership is rechecked, so an earlier INTERRUPT or shutdown consumes nothing. This applies to singleton turns too.SettlementResult(ACCEPTED,UNCONFIRMED,NOT_FOUND,REJECTED) is exposed through additivemark_*_resultmethods; the boolean methods are unchanged. A proven 404 or a validated no-processing-attempt 422 parks the intent until reconnect. While a write is unconfirmed the room defers ordinary work, including after a failed turn.(inserted_at, id)into a bounded prefix. A WebSocket-only original (a thread reply REST omits) runs alone at its position, and a non-message event ends the segment. Durable FAILED deliveries are never selected, and a page of skipped inputs continues the scan immediately.is_control_messageadapter hook and run alone under their own delivery and authorization, never inside a batch.AgentInput.messagescarries each original with its own sender;on_turnis the single model-input builder for singleton and batch turns. Current inputs render as one JSON record per original, so the Anthropic singleton prompt shape changed too. SDK-sent relays and notices mention each distinct sender once, and the ACP/OpenCode singular "Current requester" fields are gone.band_no_replyguidance covers the whole turn.CursorTurn; STOP/INTERRUPT/teardown cancel its task, and refused rooms wait in a first-come capacity line.docs/consume-all-queued.md,docs/turn-outcome.md,docs/acp.md. Requiresband-sdk-core2.9.0.Scope
Off by default; with it off, singleton turns behave as before apart from the shared admission ordering and the lifecycle corrections above. Delivery stays at-least-once: a crash between processing and the terminal write can repeat work, and failed inputs are never retried automatically in batch mode. Batching passes each original's existing content and adds no attachment hydration. Terminal intents are held in memory, so a process exit can lose them. TypeScript is INT-1783 and platform changes are out of scope.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FiE99Cx5avJ8qa2aXE6onT