fix(ai-chat): persist tool approval state across page refresh#922
Merged
Conversation
🦋 Changeset detectedLatest commit: ccd70e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
threepointone
force-pushed
the
sp/fix-approval-persistence
branch
2 times, most recently
from
February 17, 2026 10:24
692f1a1 to
2f72407
Compare
Handle `tool-approval-request` and `tool-output-denied` stream chunks in the server-side message builder. Previously only handled client-side, so the server never persisted the approval-requested state. When a tool enters approval-requested state during streaming, persist the streaming message to SQLite immediately. On stream completion, update the early-persisted row in place. Fixes the bug where refreshing during a tool approval prompt would show "Running..." instead of the Approve/Reject UI. Co-authored-by: Cursor <cursoragent@cursor.com>
threepointone
force-pushed
the
sp/fix-approval-persistence
branch
from
February 17, 2026 10:28
2f72407 to
ccd70e3
Compare
Merged
taowen
pushed a commit
to taowen/agents
that referenced
this pull request
Feb 17, 2026
…lare#922) Handle `tool-approval-request` and `tool-output-denied` stream chunks in the server-side message builder. Previously only handled client-side, so the server never persisted the approval-requested state. When a tool enters approval-requested state during streaming, persist the streaming message to SQLite immediately. On stream completion, update the early-persisted row in place. Fixes the bug where refreshing during a tool approval prompt would show "Running..." instead of the Approve/Reject UI. Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Problem
Three related bugs in the tool approval (
needsApproval) flow:Approval UI doesn't survive page refresh. The server-side message builder didn't handle
tool-approval-requestchunks from the AI SDK, so the tool part stayed ininput-availablestate on the server. And streaming messages aren't persisted until the stream completes — so when the stream pauses for approval, a refresh reloads from SQLite where the tool was never saved asapproval-requested.Duplicate approval UI. The initial fix used
persistMessages()for early persistence, which broadcasts to all clients. Since the client already has the data from the SSE stream, the approval UI rendered twice.InvalidPromptErrorafter approving a tool._applyToolApprovalsetapproval: { approved }, replacing the entire object and losing theidfield. On the continuation stream,convertToModelMessagesreadsapproval.idto produceapprovalIdin thetool-approval-requestcontent part. Withidgone,approvalIdisundefined, and schema validation fails.Fix
message-builder.tstool-approval-request— New case: transitions tool part toapproval-requestedstate withapproval.id.tool-output-denied— New case: transitions tool part tooutput-deniedstate.approvalIdtoStreamChunkDatatype.index.ts— Early silent persistenceWhen
applyChunkToPartsprocesses atool-approval-requestchunk, the streaming message is immediately written to SQLite via a direct SQL upsert — no broadcast. The client already has the data from the SSE stream; broadcasting would cause the approval UI to render twice._approvalPersistedMessageIdtracks the early-persisted ID so stream completion can update in place.index.ts—_applyToolApprovalpreservesapproval.idChanged from
approval: { approved }toapproval: { ...part.approval, approved }. This merges the approval response with the existing data, preserving theidfield that was set duringapproval-requested. When starting frominput-available(no priorapprovalobject), the spread ofundefinedis empty, so behavior is unchanged.index.ts— Cleanfinallyhandling_approvalPersistedMessageIdis captured into a localearlyPersistedIdin thefinallyblock and immediately cleared from the instance field. No stale state on error paths.Notes for reviewers
Silent SQL write vs
persistMessages(): The early persistence writes directly to SQLite without callingpersistMessages(). This meansthis.messagesis NOT updated — only the DB row exists. On stream completion, theearlyPersistedIdpath callspersistMessages()which reloads from DB, so everything converges. The intermediate state wherethis.messagesis out of sync with SQLite is brief and doesn't cause issues because the stream is paused._approvalPersistedMessageIdis per-instance, not per-stream. Filed #921 to track making it concurrent-safe if we ever support multiple simultaneous streams.The
tool-output-deniedchunk handler is the counterpart totool-approval-request. Without it, a rejected tool stays inapproval-requestedstate on the server even though the client shows it as denied.Hibernation safe. The SQLite write survives both page refresh and DO hibernation. When the DO wakes from a
CF_AGENT_TOOL_APPROVALmessage,_applyToolApprovalalready handles the transition fromapproval-requested.earlyPersistedIdandcontinuationare mutually exclusive. Early persistence only happens on the initial stream (before approval). The continuation stream starts fresh after approval —earlyPersistedIdis always null.Tests (5 new, all passing)
applyChunkToParts: tool-approval-request > transitions tool part from input-available to approval-requestedapplyChunkToParts: tool-approval-request > does nothing if tool part not foundapplyChunkToParts: tool-output-denied > transitions tool part to output-denied stateTool approval persistence across reconnect > persisted messages include approval-requested state after approval-request chunkapproval.idis preserved through the approval flowTest plan
npm run buildpassesnpm run checkpasses (42 projects typecheck)