Queue messages sent to a thread that awaits user interaction - #1699
Queue messages sent to a thread that awaits user interaction#1699SawyerHood wants to merge 4 commits into
Conversation
A thread blocked on AskUserQuestion or an approval refused every send with 409 and persisted nothing, so only the sender learned that the message never arrived. The send route now queues such messages (and reports the queued outcome), and parent system messages defer and flush when the interaction settles. Fixes #1650 Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
|
🚨 I will check security, code quality, performance, architecture, and the end-to-end behavior. |
| // messages wait here and flush when the parent's interactions settle. Server | ||
| // memory only: a restart interrupts every pending interaction and the parent | ||
| // runtime is gone, so there is no later unblock to flush into. | ||
| const deferredParentSystemMessages = new Map< |
There was a problem hiding this comment.
🚨 slopcop/review — Deferred messages disappear after a server restart, and this queue has no limit.
The comment says that a restart interrupts every pending interaction. However, PendingInteractionLifecycle.start() interrupts only plugin interactions. Provider approvals and questions remain in SQLite. If a child finishes before a restart, this Map loses the child result. The parent can then wait for a result that BB already received.
This Map also has no item, byte, or age limit. A long approval can retain unlimited full prompt values. The flush then sends each entry separately and can cause a large database and host-command load.
Please use the durable queued-message path and preserve the system-message fields. A dedicated SQLite queue also works. Please add a restart test and a bounded-flush test.
There was a problem hiding this comment.
Fixed in bdcb910. Confirmed: start() interrupts only plugin interactions, so provider approvals survive a restart and the in-memory map would lose the child result. Deferred parent system messages now persist in a new deferred_parent_system_messages table (migration 0099). The settle listener flushes them, and a deferred-parent-system-message-flush periodic sweep re-drives rows a restart left behind, so nothing depends on process memory. Tests: parent-system-messages-deferred.test.ts covers settle-flush, sweep-while-blocked (row stays), and sweep-after-unblock (restart case). I did not add a size cap: the store is SQLite, and child notifications already batch upstream; each flush sends rows in order and logs failures.
| args: { payload: SendMessageRequest; thread: Thread }, | ||
| ): SendQueuedReason | null { | ||
| const { payload, thread } = args; | ||
| if (thread.status !== "active" || payload.mode === "start") { |
There was a problem hiding this comment.
🚨 slopcop/review — Idle threads with plugin input requests still reject messages instead of queueing them.
requestPluginInteraction() permits an idle thread. This guard returns before it checks for a pending interaction when the thread is idle. sendThreadMessage() then returns 409, and BB stores nothing. The new CLI and guide text says that a target awaiting user interaction will queue the message.
Please check the pending interaction first for each non-start mode. When an idle interaction settles, request queued-message delivery. Please add an idle plugin-interaction regression test.
There was a problem hiding this comment.
Fixed in bdcb910. Confirmed: requestPluginInteraction() allows an idle thread, and the old guard returned before the pending check. The route now checks the pending interaction first for every non-start mode, and createQueuedMessageForThread and sendNextQueuedMessageIfPresent skip auto-send while an interaction blocks the thread. When the interaction settles on an idle thread, the settle listener requests a queued-message auto-send. Regression test: "queues sends to an idle thread blocked by a plugin input request" in public-thread-interactions.test.ts, plus the blocked-idle auto send in the existing test now expects a queued row that does not auto-send.
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Plain English: This change stops messages from disappearing while a thread waits for a question or approval. User messages enter the existing queue and send after the thread becomes idle. Child results use a new temporary memory queue. The API, SDK, and CLI now report whether BB sent or queued the message.
I found two medium problems:
-
The parent-message queue loses child results after a server restart. Provider questions and approvals remain in SQLite, but the new process
Mapdisappears. The same map has no size or age limit. A long interaction can consume unlimited memory and cause a large flush load. -
Idle threads with plugin input requests still return 409. The pending-interaction check runs only after the active-status guard. This behavior conflicts with the new CLI and guide text, and BB still stores nothing.
The existing durable queued-message service is the best refactor target. It can remove the duplicate temporary queue if it also stores system-message fields. I found no other important duplication.
I found no security issue. Request validation, sender attribution, attachment checks, permission resolution, and dispatch controls remain intact.
Validation passed:
- Turbo type checks passed for
@bb/server,@bb/cli,@bb/sdk, and@bb/server-contract. - The focused server tests passed: 26 tests.
- The CLI tests passed: 10 tests.
- The SDK tests passed: 90 tests.
- A live server at the pull request SHA returned
delivery: "queued"and stored messages during a plugin question.
The browser check could not finish because the first Vite dependency optimization did not complete. The live API path and queue state completed successfully.
…le threads Address SlopCop review on #1699: - Deferred parent system messages now live in a SQLite table with a periodic sweep, so a server restart does not lose them and the queue is not unbounded process memory. - The send route checks for a pending interaction before the status guard, so an idle thread blocked by a plugin input request queues too. Queued auto-send skips blocked threads and resumes when the interaction settles. Co-Authored-By: Claude <noreply@anthropic.com>
Fixes #1650
Problem
A thread blocked on
AskUserQuestionor an approval refused everysendwith409 awaiting_user_interaction. The server persisted nothing, so only the sender learned that the message never arrived. An orchestrator held a question open for hours and concluded that its worker had gone silent. Child turn notifications to a blocked parent had the same gap:queueParentSystemMessagereturnedfalsewith no log and no retry.Change
POST /threads/:id/send: when an active thread awaits user interaction, queue the message instead of returning 409.queue-if-activeand manual-compaction sends still queue.startstill hits the existing conflict errors. The response now reports the outcome:{ ok, delivery: "sent" }or{ ok, delivery: "queued", queuedReason }.queued-message-auto-sendpath when the thread is next idle, and they stay visible in the thread queue meanwhile.bb thread tellprints the queued outcome (... is awaiting user interaction; message queued and delivers when the thread is next idle) and includesdelivery/queuedReasonin--json.PendingInteractionLifecycle.setThreadInteractionSettledListener.threads.sendreturnsSendMessageResponse; guide and bb-cli skill document the queued outcome.The 409 error text is unchanged on purpose; the sibling change for #1655 owns that surface.
Tests
public-thread-interactions.test.ts: asteer-if-activesend with asenderThreadIdto a blocked active thread returnsdelivery: "queued"and stores one queued row with the sender;startstill returns 409 and adds no row. (Replaces the PR Fix pending interaction queue send guard #112 assertion that queueing was refused.)parent-system-messages-deferred.test.ts: a parent system message to a blocked parent defers, then lands as aclient/turn/requestedsystem event after the interaction settles.bb thread tellhuman and--jsonoutput for the queued outcome.@bb/server,@bb/cli,@bb/sdk,@bb/server-contract, and the touched@bb/apptest. The only failing server test (internal-skill-treesfile mode 420 vs 436) also fails onmainin this environment.