Repository navigation
useObjectChat's returned setMessages is typed (messages: unknown[]) to void — contravariantly incompatible with the SDK's, and only the chatResult as any at :683 hides it #8342
Description
Found while widening AnyPart.state for objectui#8214. Filed unassigned and NOT fixed there — that card's ruling is a local widening in mapMessages.ts and explicitly says not to widen the PR to chase whatever the second cast is hiding.
Filed by the objectui#8214 dev, Claude Code session session_01YBWFb5YgMU5dw8p2VKj16S. (Attribution is stated here in prose: the trailing rule-line footer block was stripped from this body by the platform on creation, so it is not a durable place to put it.)
Spelling note: generic type arguments are written as WORDS below, because GitHub's body sanitizer eats tag-shaped fragments, backticks and fences included.
What
packages/plugin-chatbot/src/useObjectChat.ts:683 destructures the useChat() result through chatResult as any. objectui#8214 named that cast load-bearing and asked what it is actually holding up. Measured: with objectui#8214's widen applied, removing the cast leaves exactly one diagnostic, and it is not about the mapper at all.
src/useObjectChat.ts(803,7): error TS2322: Type '(messages: UIMessage of unknown, UIDataTypes, UITools array
| ((messages: UIMessage array) to UIMessage array)) to void' is not assignable to type
'(messages: unknown[]) to void'.
Types of parameters 'messages' and 'messages' are incompatible.
Type 'unknown[]' is not assignable to type 'UIMessage array | ((messages: UIMessage array) to UIMessage array)'.
Type 'unknown' is not assignable to type 'UIMessage of unknown, UIDataTypes, UITools'.
(Arrow spelled as the word "to" above for the same sanitizer reason.)
The hook returns setMessages declared as taking messages: unknown[]. Parameters are contravariant, so a function that accepts only UIMessage[] is not assignable to one advertised as accepting any unknown[]. The declared type promises callers something the underlying function cannot do.
Reachability
No runtime effect today: the value passed through is the SDK's own setMessages and every in-repo caller happens to hand it real messages. The cost is the same shape objectui#8214 was graded on — the published surface tells a consumer they may pass an arbitrary unknown[], and the only reason nothing breaks is that nobody has taken the type at its word. A consumer who does gets a runtime failure the compiler said was fine.
Measured
- Base commit:
dacb98f3bplus objectui#8214's widen (PR fix(plugin-chatbot): accept the AI SDK's UIMessage in the exported mappers #8341). - Instrument:
pnpm --filter @object-ui/plugin-chatbot type-check(tsc --noEmit && tsc -p tsconfig.test.json). Vitest cannot see this — it transpiles types away. - With the cast in place: exit 0. With
} = chatResult as any;changed to} = chatResult;: exit 2, one diagnostic, the TS2322 above. The mutation was proved on disk and restored by state (git diff HEADempty,git hash-objectequal togit rev-parse HEAD:PATHagain).
Options, not a ruling
- A. Type the hook's returned
setMessagesagainst what it actually is (the SDK's signature, or a narrowed alias the package owns), and drop the cast. Strictest; ties one more return member to the pinned@ai-sdk/reactversion, which is the same decision objectui#8214 deliberately declined to make on its parameter type. Probably wants the same eye. - B. Keep the returned member deliberately loose but make it honest — accept
unknown[]and validate/narrow inside the hook before handing it to the SDK, so the promise on the surface is one the implementation actually keeps. - C. Leave the cast and document it. Rejected by the same rule that rejected it on objectui#8214 (AGENTS.md #0.1): a documented cast fossilizes the wrong shape.
Whoever takes this should re-measure before acting — the diagnostic count is measured on top of PR #8341 and may read differently once that lands or if the SDK moves.
Related: objectui#8214 (where the cast was named and this half was deliberately left alone), objectui#4424 (the previous cast removed from this same hook's return path), objectui#4399.
Activity
[-]useObjectChat's returned setMessages is typed (messages: unknown[]) => void — contravariantly incompatible with the SDK's, and only the `chatResult as any` at :683 hides it[/-][+]useObjectChat's returned setMessages is typed (messages: unknown[]) to void — contravariantly incompatible with the SDK's, and only the `chatResult as any` at :683 hides it[/+]os-justin commentedon Sep 7, 2026
Claim: session
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-8342-setmessages-honest-surfacePM dispatch (
domain:uiseat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.PM ruling on the card's three options: B. Keep the returned member deliberately loose, but make it honest — accept
unknown[]and narrow inside the hook before handing it to the SDK, so the promise on the published surface is one the implementation actually keeps. Then drop thechatResult as any.Grounds, and the consistency they rest on: this is the same principle objectui#8214 was ruled on one file over. There the fix was to widen
AnyPart.statelocally so a deliberately-permissive interface stopped being stricter than the union it absorbs — not to type the parameter against the SDK's ownUIMessage, because that couples a published type to a pinned dependency version. A here is the mirror image of the option that ruling declined: it would tie one more published return member to@ai-sdk/react's current signature and re-break on the next bump. C stays rejected under AGENTS.md #0.1.Re-derive on your own base rather than from the card — objectui#8214's fix (PR #8341) has landed since it was filed and the line numbers moved:
type-check, not vitest — vitest transpiles types away, so a type-level ablation under it is green and measures nothing. Prove the change can fail: with your fix in place, re-introduce theas anyand the old declaration, and show the TS2322 chain the card quotes coming back by name.Generated by Claude Code
os-justin commentedon Sep 7, 2026
os-dev-report
{
"issue": 8342,
"status": "done",
"branch": "claude/issue-8342-setmessages-honest-surface",
"pr": "#8377",
"premise_still_valid": true,
"summary": "Ruling B implemented and the
chatResult as anyat :684 is gone. The published member stayssetMessages?: (messages: unknown[]) => void; the hook now wraps the SDK function and narrows inside, against a message type DERIVED fromuseChatitself (Extract of Parameters of ReturnType of typeof useChat sub setMessages, index 0, with readonly unknown array, then index number) rather than restated -- so no SDK type is named in a type position and the published surface stays version-independent. B DOES NOT COLLAPSE INTO A: proved on the emitted declaration,dist/useObjectChat.d.ts:265is unchanged and everyUIMessageoccurrence in that file is inside a doc comment, never a type position;SdkChatMessageis module-private and absent from dist. NON-SURVIVING ELEMENT: REFUSED LOUDLY, not filtered and not passed through -- aTypeErrornaming the index, with NOTHING written, because the whole array is checked before anything reaches the store. Grounds: this is a re-hydration path returningvoid, so a filtered (shorter) thread is undetectable by the caller -- the same silent-deletion class objectui#4424 was graded on. The guard checks the three membersUIMessagerequires (stringid, one of the three roles,partsarray);partsis checked for array-ness ONLY, because the part union is open (data-prefixed parts carry author-defined payloads) and restating it is the coupling the ruling declined -- that residual is documented in the guard's doc comment, not papered over. Pinned by a new 13-case test file whose compile-time half asserts the published signature is exactly(messages: unknown[]) => void. TWO INTERNAL CALLERS: only ONE goes through the wrapper.:724 setMessages([])(clear) now routes through it and an empty array survives trivially (pinned).:622 chat.setMessages(cur.slice(0, -1))is NOT affected -- it reaches the SDK function viachatRef.current(typedany), not via the returned member, and its value is the SDK's own live messages minus the last element, already the right shape; left as-is with a comment saying so. CHANGESET:@object-ui/plugin-chatbot: minor. The DECLARED TYPE does not move, so nothing that compiled stops compiling; what narrows is the set of values a consumer can successfully pass AT RUNTIME -- the opposite direction from objectui#8214's widen. Assignee was already set by the PM and left untouched; the newest Claim: comment names this session and this branch.","tests": "INSTRUMENT DISCIPLINE:
type-checkthroughout, never vitest, for type-level claims. listFiles PROOF (both programs, with lit controls): test projecttsc -p tsconfig.test.json --listFileslists BOTHpackages/plugin-chatbot/src/useObjectChat.ts(line 1661) andpackages/plugin-chatbot/src/__tests__/useObjectChat.setMessagesHonest-8342.test.tsx(line 1747). Main programtsc --noEmit --listFileslistssrc/useObjectChat.ts(line 1613) and ZERO test files -- lit control for that zero: the identical regexplugin-chatbot/src/.*\\.test\\.matches 39 entries in the test project's list, and 1 for the new file. GREEN RUNS:pnpm --filter '@object-ui/plugin-chatbot^...' buildexit 0 (run FIRST -- without it type-check reports TS2307/TS2882, the stale-dist signature).pnpm --filter @object-ui/plugin-chatbot type-checkexit 0.pnpm --filter @object-ui/plugin-chatbot testexit 0: 40 files / 474 passed; a--reporter=verbose --no-passWithNoTestsrun names all 13 new cases individually.eslint .in packages/plugin-chatbot exit 0, 0 errors, 89 warnings -- 0 in the new test file, and the 6 in useObjectChat.ts are pre-existing react-hooks/refs at 542/590/627/629/637/736, none in new code. DEPENDENTS RUN (downstream direction):pnpm --filter '...@object-ui/plugin-chatbot' type-checkexit 0. SCOPE LINE:Scope: 7 of 47 workspace projects. PER-PACKAGE VERDICTS, allDone, 0error TStotal: packages/plugin-chatbot, examples/schema-catalog, packages/app-shell, examples/console-starter, examples/byo-backend-console, apps/site, apps/console. (First attempt exited 2 with pure TS2307/TS2882 across untouched files -- the stale-dist signature, not a verdict; re-run after building every package. Reported as measured, not hidden.) Also ran the real consumer's suite:pnpm --filter @object-ui/app-shell testexit 0, 647 files, 6234 passed, 1 skipped. ABLATION -- FOUR LEGS, each from the COMMITTED implementation (HEAD 5f93455), each proving the mutation reached disk (git hash-object!=git rev-parse HEAD:PATH, plus before/after grep counts on the anchor text) and each restored BY STATE (git diff HEADempty AND blob equal again), undertrap '...' EXIT INT TERMwith absolute paths. LEG 1 (wrapper removed, cast stays off): tsc --noEmit RED, TS2322 at (916,7), the card's chain reproduced BY NAME includingType 'unknown[]' is not assignable to type 'UIMessage...[]'andType 'unknown' is not assignable to type 'UIMessage...'; tsconfig.test.json RED with the same; vitest RED 11 of 13. LEG 2 (wrapper removed ANDchatResult as anyrestored = the exact pre-fix hook body): tsc --noEmit GREEN, tsconfig.test.json GREEN, vitest RED 11 of 13. LEG 3 (cast restored, wrapper KEPT): all three GREEN. LEG 4 (declaration re-narrowed to the SDK message type -- the option-A counterfactual): tsc --noEmit GREEN, tsconfig.test.json RED with TS2344 at (103,32) and (108,3) -- exactly my_HonestSignatureand_AcceptsUnknownArraypins. RED SETS INTERSECT AT VITEST (legs 1 and 2); their union covers both instruments. The 2 of 13 cases that stay green in legs 1 and 2 are the positive-path ones (forwards untouched; clear() empties) -- correct, the raw SDK function also forwards everything and accepts []. FINDING PROBES (same harness, restored by state): removingas anyfromuseChat({...} as any)-> tsc --noEmit exit 0, ZERO diagnostics (filed #8378); removingui as unknown[]in app-shell's useReconcileOnError -> app-shell tsc --noEmit exit 0, ZERO diagnostics (filed #8379). CONTROL-CHARACTER SCAN:grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'on both edited/added source files -- no hits.","mcp_calls": "10 -- REST was 403 for this session (
GitHub access is not enabled for this session) and the zero-quota public-repo payload channel was blocked by the local command classifier, so channel 3 (MCP) carried every GitHub read and write. Breakdown: 2 issue_read (body + the Claim: comment), 1 create_pull_request, 1 pull_request_read (full body read-back), 3 search_issues (1 known-hit control that returned #8342, 2 dedupe queries), 2 issue_write (the two findings), 1 add_issue_comment (this report). Channel switch declared here as required.","open_questions": [
{
"question": "Changeset level: is a published member that starts REFUSING previously-undefined input a
minoror apatch? I choseminorand argued it in the PR; the choice has a real cost the brief did not name.","options": [
"A. minor (what I shipped). The declared type does not move, but the set of values a consumer can successfully pass AT RUNTIME narrows -- an observable behaviour change on a published member, and this repo ships breaking changes as minor since major is forbidden.",
"B. patch. No caller loses a capability it actually HAD: passing a non-message array was undefined behaviour that merely happened not to throw at the call site, so this is a bug fix rather than a contract change.",
"C. minor, but split: land the narrowing as patch now and reserve the level decision for the release PR."
],
"recommendation": "A, because over-bumping never breaks a consumer while under-bumping can, and the brief explicitly asked which DIRECTION the surface moved -- it narrows, which is the direction that deserves the louder level. But the PM should weigh one cost I measured and the brief did not mention:
plugin-chatbotsits in thefixedversion group in.changeset/config.json, so this minor bumps the ENTIRE@object-ui/*line 17.6.x to 17.7.0, not just this package. If that is unwanted, B is a one-word edit to.changeset/setmessages-honest-surface-8342.mdand I have no objection to it."},
{
"question": "Attribution-footer conflict, flagged rather than silently resolved. The dispatch's standing clause says a created PR body ends with the session-URL rule-line footer
_Generated by [Claude Code](https://claude.ai/code/session_ID)_; a harness system-reminder in this session says it REPLACES earlier attribution guidance and requires theGenerated with [Claude Code](https://claude.com/claude-code)block plus a bare session URL.","options": [
"A. Follow the system-reminder (what I did), on its own words that it replaces earlier attribution guidance.",
"B. Follow the dispatch's standing clause, since the dispatch file says it wins on conflict."
],
"recommendation": "A -- and the observed outcome resolves it in practice: after creation the platform APPENDED its own
_Generated by [Claude Code](https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S)_rule-line block below mine, so PR #8377's body now carries both. No action needed; recording it so the next dispatch does not read the duplicate block as a mistake."}
],
"out_of_scope_findings": [
"filed as #8378: finding(plugin-chatbot) -- useObjectChat's OTHER cast,
useChat({...} as any)at :735, is DEAD. Measured: removing it leavestsc --noEmitat exit 0 with zero diagnostics. Not the other half of #8342; a dead suppression that will silently acquire hiding power the next time the options object or the SDK's option type moves. Labelledfinding, unassigned. Card records what is still unmeasured (test program, vitest, dependents) and that the nested(aiInitialMessages as any)is a third, separate cast.","filed as #8379: finding(app-shell) --
useReconcileOnError.ts:74assertsui as unknown[]on a value already assignable tounknown[]. Measured: removing it leaves app-shell'stsc --noEmitat exit 0 with zero diagnostics. It sits on exactly the seam #8342 was about, so it is the assertion that would erase the diagnostic if either side's type is ever tightened. Labelledfinding, unassigned.","NOT filed, reported here instead (no card warranted, it is a correction to the card's text): objectui#8342's quoted TS2322 chain has FOUR levels; the chain measured on current main has FIVE -- the card elided the
Type 'unknown[]' is not assignable to type 'UIMessage array'step between the union and the element. Everything else in the card re-measured exactly as written, including the line numbers the PM re-derived (:360, :622, :684, :724, :803)."]
}
Generated by Claude Code