…nd delete the last cast on useChat
`useObjectChat`'s API-mode builder declared its part array as
`Array<Record<string, unknown>>` and pushed plain objects into it. That is not
the chat runtime's part union, and the mismatch was absorbed by an `as any` on
the `messages` option instead of being reported. Re-measured 2x2 on this branch,
each leg mutated on disk with hash proof and restored by state: only the
both-casts-removed leg is red, with one TS2322 in each of the two type-check
programs.
The builder now CONSTRUCTS each part, so the option is checked and the cast is
gone. Per the ruling on this chain (director seat, decision batch #86,
2026-09-08, option A, contract-first) the fix lands at the producer, never as a
wider cast at the consumer.
- The three approval states are reachable: they require the runtime's
`approval` envelope alongside them, `ChatToolInvocation` gained it in the
first clause of this chain, and the builder now constructs those arms from it.
- The legacy authoring states `partial-call` / `call` / `result` are folded onto
the lifecycle arms they mean instead of passing through as states the
round-trip reader refuses.
- The dead `toolName` member is no longer written onto a `tool-*` part: only the
dynamic-tool arm declares one, and the reader derives the name off `type`.
- `UseObjectChatOptions.initialMessages`' optional `parts` narrows to the store's
own part array. Breaking for an external host that passes pre-built parts;
nothing in this repo sets it.
An invocation claiming an approval state with no envelope to back it is not
constructible, and no envelope is invented for it: the state is derived from the
data it does carry and the producer is told once. An ObjectStack HITL approval
is deliberately not reported, since it is carried by `pendingActionId` and the
mapper re-promotes the state from the tool result.
Also lifts the `approval` envelope in the mapper's tool-invocation extraction,
closing the disagreement where the hydrated path carried the envelope and the
live path dropped it. The lift lands in the same round as its first reader.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #8426
The API-mode parts builder in
useObjectChatdeclared its part array asArray(Record(string, unknown))and pushed plain objects into it. That is not the chat runtime's part union, and the mismatch was absorbed by anas anyon themessagesoption rather than reported. This makes the builder construct the discriminated parts, so the option is checked and the cast is gone.Ruling carried in, not re-decided: director seat, decision batch #86, 2026-09-08, option A — contract-first. Its first clause landed as objectui#9229; this is the remaining one. The three shortcuts the dispatch forbids by name (widen the declared type to
any; keep the cast with a comment; re-litigate A) are all absent.The 2x2, re-run on this branch before repairing
Each leg mutated on disk through an anchor that must hit, with blob-hash proof, and restored by state (
git diff HEADempty after every leg). Two type-check programs per leg:tsc --noEmitandtsc -p tsconfig.test.json.} as any)(aiInitialMessages as any)main, PR #8401)All four legs reproduce the card's table. The red leg's diagnostic, verbatim:
(angle brackets transliterated to round ones above so the body survives sanitizing.)
After: both casts gone, both programs green
Program membership proved with a lit control rather than assumed — main program 1626 files:
src/useObjectChat.ts1,src/mapMessages.ts(positive control) 1,useObjectChat.honestMessages.test.tsx(negative control) 0, and the same negative pattern returns 1 against the test program's file list.Dependents, on a freshly built union closure (
Scope: 36 of 47built, thenScope: 7 of 47type-checked): all seven pass, zeroerror TS—plugin-chatbot,app-shell,console,site,example-schema-catalog,example-console-starter,example-byo-backend-console. That is the measurement behind the claim that the published-input narrowing breaks nothing in this repository.Reverse verification — the green is not vacuous
Both legs run from the committed fix, mutated through an anchor that must hit, restored to a byte-identical blob, and the restore leg re-run.
toolNameback on a constructed tool arm. Predicted red; measured red:exit 2,TS2353: Object literal may only specify known properties, and 'toolName' does not exist in type .... Restored: exit 0.Tests 1 failed | 11 passed (12), the failure beingsays NOTHING when the invocation is an ObjectStack HITL approval. Restored: 12 passed.What changed
approval-requested,approval-respondedandoutput-deniedrequire the runtime'sapprovalenvelope beside them;ChatToolInvocationgained it in objectui#9229 and the builder now constructs those arms from it. The per-arm shapes differ (approvedis forbidden on the outstanding request, required on the response, pinned tofalseon the denial) so each is built rather than spread.tool-${toolName}is expressible. The dynamic member the card feared was the blocker is not one: the runtime's tool set is open, so the mapped type collapses to an index signature and the discriminant is a template. Pinned by a test.partial-call/call/resultare not runtime states; passing them through left the round-trip reader refusing them, so the invocation came back stateless. Each test pairs the folded subject with a control asserting the authored spelling is still rejected by the runtime's ownvalidateUIMessages.toolNameexcess property is dropped. Only the dynamic-tool arm declares one, and the reader derives the name offtype, so this is behaviour-preserving.UseObjectChatOptions.initialMessages' optionalpartsnarrows to the store's own part array. Breaking for an external host that passes pre-built parts;minorper this repo's version policy, with the breaking note carried in the changeset.mapMessages' tool-invocation extraction liftsapproval. Assigned to this card when objectui#9229 shipped the additive half, and deliberately held until its first reader existed — lifting it earlier would have minted a declared-but-unread key. It closes the recorded asymmetry where the hydrated path carried the envelope and the live path dropped it.An invocation claiming an approval state with no envelope to back it is not constructible, and no envelope is invented for it (inventing
approval.idis the fabrication AGENTS.md #0.1 forbids): the state is derived from the data the invocation does carry, and the producer is warned once, by name. An ObjectStack HITL approval is deliberately not warned about — it is carried bypendingActionIdand apending_approvalresult, and the mapper re-promotes the state from that result on the way back out, so the approval card survives the round trip.Acceptance notes
stateunion should shed the three runtime-only approval states so that "a schema-authored invocation cannot claimapproval-requestedwithout an envelope". That union lives onChatToolInvocationin@object-ui/types, which objectui#9229 left alone and whose own doc records the narrowing as this card's. The dispatch fencespackages/typesout with "if you find it incomplete, report; do not extend it here". So the conflict is reported rather than resolved by me: the narrowing is what would delete the un-backed-approval branch added here by construction, and it wants its own card or an explicit widening of this one's fence.Out of scope, noted, not filed:
(aiMessages[idx] as any)?.metadatain the same file is untouched and unrelated to this card's call. Successor: the next card on this file's message-output path.mergeToolResultsIntorewritingstatetooutput-availablefor every merged result) is recorded on this card as possibly changing what "done" means here. It is unchanged by this PR: building the arms does not by itself put an approval card in front of an operator on that sub-path. Successor: objectui#9233 itself, which is open.approval) is likewise untouched and open. Successor: objectui#9232.Gates run locally
check:control-bytes0 ·check:new-line-citations0 (0 new citation(s)) ·check:changeset-claims0 ·check:pending-changeset-literals0 ·check-changeset-presence0 (1 changeset(s)) ·check-changeset-no-major0 ·check-governed-queue-guard --test— NOT GOVERNED ·eslint --no-inline-configon the four changed source files exit 0, 9 pre-existing warnings, none from this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Generated by Claude Code