Skip to content

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

Description

@os-tesla

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:

target.state = isError ? 'output-error' : 'output-available';

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:

  • useHitlInChat keys its index purely on pendingActionId, so after PR feat(types,app-shell): carry the tool approval envelope through hydration #9229 the invocation is indexed on this sub-path and decide() has a real id to POST. That half works.
  • 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.

Related

objectui#8442 · objectui#8426 · PR #9229


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions