@@ -563,3 +563,30 @@ pinned strip the shell mock shows above the message list
563563(` @corbits/chat-ui ` 's ` PinnedStrip ` ) renders every currently-pinned message
564564as a jump-to chip; clicking one scrolls the timeline to that message's own
565565row (` messageDomId ` ).
566+
567+ ### Question answers notify at most once (CL-7192)
568+
569+ A question block's answer is relayed into the workbench as the
570+ responding user's own message, and the asking agent is turned on it —
571+ but only once per question, ever. ` upsertBlockResponse `
572+ (` packages/chat/src/block-responses.ts ` ) is a plain "second answer
573+ overwrites the first" write; the one-time send-and-dispatch is gated
574+ separately by ` claimBlockResponseNotification ` , a guarded UPDATE on the
575+ same row (` notifiedAt ` /` notificationClaimToken ` ) that at most one
576+ submission for a given (tenant, workbench, message, block, principal)
577+ can ever win. A changed answer or a double-click that beats the UI's
578+ disable still lands its own row and its own ` block.response ` timeline
579+ event, but never wins a second claim — re-answering after the first
580+ notification updates the stored payload without ever notifying the
581+ agent again.
582+
583+ A claim a submission wins but fails to act on (the send into the
584+ workbench throws) is released so a retried submission can still reach
585+ the agent; ` POST /workbenches/:id/messages/:id/blocks/:id/responses `
586+ answers that case with an explicit ` notify_failed ` 500 telling the
587+ caller their answer was saved and to retry, rather than leaving the row
588+ reading "answered" with the agent silently never turned. The
589+ ` block.response ` event is always posted — from the upsert's own
590+ returned row, never the request's local payload — before this claim is
591+ even attempted, so an agent dispatched off a won claim always finds its
592+ own correlation event already on the timeline.
0 commit comments