You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(app-shell): toUIMessages' tool-result merge rewrites state to output-available, so a rehydrated approval-requested never reaches the chat from the ModelMessage path #9233
Found while implementing objectui#8442 (PR #9229), as the end-to-end reading that card explicitly asked the next taker to verify rather than inherit. ⛔ Not claimed.
What
packages/app-shell/src/hooks/useChatConversation.ts, mergeToolResultsInto (called from toUIMessages), measured on origin/main @ b775500af.
The server persists conversations in ModelMessage format: a tool CALL lives on the assistant row, its RESULT on a separate tool row. mergeToolResultsInto merges the result back onto the call part — and, whenever it merges anything, it also overwrites the part's state:
unconditionally, for every merged result. There is no arm for the approval states.
⇒ On this sub-path a pending approval can never reach hydratedMessagesToChatMessages as approval-requested. The state is rewritten before the mapper ever sees the part, so the mapper's own pass-through of that state — pinned, and deliberately kept — is unreachable from here.
Why it matters, stated at the confidence it was measured at
This is the second half of objectui#8442's consequence 2, which that card reported at reading confidence and flagged as not verified end to end. Now read at the source, on both sides:
The awaiting-approval affordance in ChatbotEnhanced is gated on state === 'approval-requested' (isAwaitingApproval, and hidePendingPayload beside it). With the state rewritten to output-available, the card does not render, so there is no button to press even though the wiring behind it is now live.
So the honest answer to objectui#8442's open question is: the operator gets neither a working nor a dead Approve/Reject on this sub-path — they get no approval card at all. The AI-SDK-shaped sub-path (persisted UIMessage parts, no separate tool row to merge) is unaffected and does render.
PR #9229 pins today's reading as a reading, in AiChatPage.hydration.test.ts, so whoever changes this turns that line red instead of finding a stale sentence.
What a fix has to be careful about
⛔ Not "stop rewriting the state". The rewrite is load-bearing: it is how a dangling input-streaming / input-available becomes Completed instead of spinning forever, which is the incident the promotion logic in partToolState and in the live mapper both exist for. The narrow statement is that a merged result must not rewrite an approval state — the live mapper already models this correctly, promoting to approval-requested when it detects the pending envelope, and this merge step is the one place that has no such arm.
⚠️ Sequencing: objectui#8426 owns the authoring state union narrowing under the same ruling. This card is NOT blocked by it — the two touch different files and neither needs the other — but a seat taking both should read them together.
Confidence
Read from the source, not exercised in a browser. Measured: the unconditional assignment; the two gate expressions in ChatbotEnhanced; the useHitlInChat index predicate. NOT measured: the live /conversations endpoint's actual persistence shape, i.e. how much real traffic takes the ModelMessage sub-path versus the UIMessage-parts one. That ratio decides this card's priority and someone with a running backend should read it before costing a fix.
Dedup
REST /search/* is refused for this session by the egress proxy; one targeted search_issues call was used instead and is declared here. 6 hits, all read — objectui#8442 (the control, and the card this splits off from), objectui#5553, objectui#2829, objectui#2762, objectui#2698, objectui#4437. None names this merge step or the state rewrite.
Found while implementing objectui#8442 (PR #9229), as the end-to-end reading that card explicitly asked the next taker to verify rather than inherit. ⛔ Not claimed.
What
packages/app-shell/src/hooks/useChatConversation.ts,mergeToolResultsInto(called fromtoUIMessages), measured onorigin/main@b775500af.The server persists conversations in ModelMessage format: a tool CALL lives on the assistant row, its RESULT on a separate
toolrow.mergeToolResultsIntomerges the result back onto the call part — and, whenever it merges anything, it also overwrites the part's state:unconditionally, for every merged result. There is no arm for the approval states.
⇒ On this sub-path a pending approval can never reach
hydratedMessagesToChatMessagesasapproval-requested. The state is rewritten before the mapper ever sees the part, so the mapper's own pass-through of that state — pinned, and deliberately kept — is unreachable from here.Why it matters, stated at the confidence it was measured at
This is the second half of objectui#8442's consequence 2, which that card reported at reading confidence and flagged as not verified end to end. Now read at the source, on both sides:
useHitlInChatkeys its index purely onpendingActionId, so after PR feat(types,app-shell): carry the tool approval envelope through hydration #9229 the invocation is indexed on this sub-path anddecide()has a real id to POST. That half works.ChatbotEnhancedis gated onstate === 'approval-requested'(isAwaitingApproval, andhidePendingPayloadbeside it). With the state rewritten tooutput-available, the card does not render, so there is no button to press even though the wiring behind it is now live.So the honest answer to objectui#8442's open question is: the operator gets neither a working nor a dead Approve/Reject on this sub-path — they get no approval card at all. The AI-SDK-shaped sub-path (persisted UIMessage parts, no separate tool row to merge) is unaffected and does render.
PR #9229 pins today's reading as a reading, in
AiChatPage.hydration.test.ts, so whoever changes this turns that line red instead of finding a stale sentence.What a fix has to be careful about
⛔ Not "stop rewriting the state". The rewrite is load-bearing: it is how a dangling
input-streaming/input-availablebecomes Completed instead of spinning forever, which is the incident the promotion logic inpartToolStateand in the live mapper both exist for. The narrow statement is that a merged result must not rewrite an approval state — the live mapper already models this correctly, promoting toapproval-requestedwhen it detects the pending envelope, and this merge step is the one place that has no such arm.stateunion narrowing under the same ruling. This card is NOT blocked by it — the two touch different files and neither needs the other — but a seat taking both should read them together.Confidence
Read from the source, not exercised in a browser. Measured: the unconditional assignment; the two gate expressions in
ChatbotEnhanced; theuseHitlInChatindex predicate. NOT measured: the live/conversationsendpoint's actual persistence shape, i.e. how much real traffic takes the ModelMessage sub-path versus the UIMessage-parts one. That ratio decides this card's priority and someone with a running backend should read it before costing a fix.Dedup
REST
/search/*is refused for this session by the egress proxy; one targetedsearch_issuescall was used instead and is declared here. 6 hits, all read — objectui#8442 (the control, and the card this splits off from), objectui#5553, objectui#2829, objectui#2762, objectui#2698, objectui#4437. None names this merge step or the state rewrite.Related
objectui#8442 · objectui#8426 · PR #9229
Generated by Claude Code