fix(channels): safely parse non-string user message content - #43
fix(channels): safely parse non-string user message content#43arpan7sarkar wants to merge 1 commit into
Conversation
Guard user message content parsing with Array.isArray to prevent runtime TypeError exceptions when message content is undefined, null, or non-array objects.
14fa5a4 to
fb5341e
Compare
|
Closing this, with thanks for the tests — they're well-aimed and worth keeping. The reason: it patches the symptom in one projection ( |
Restoring a thread ended `stored as Message[]` — a cast, not a check — so whatever
the history store held reached `setMessages` and then every projection that draws
a transcript. A turn in another shape took the conversation down rather than
itself: a tool call written `{id, name, args}` instead of AG-UI's
`{id, type: "function", function: …}` reached a renderer that dereferenced
`toolCall.function.arguments`, and one bad turn made a thread unopenable.
Each turn is now parsed with `MessageSchema` from `@ag-ui/core` and one that does
not parse is left out. Checked where history enters the app rather than in a
projection, because there are several projections and one history.
A dropped turn is counted and shown. `readThreadMessages` returns the count
beside the messages and the conversation draws a line naming it, because a turn
that quietly disappears from a record people read back is worse than a visible
failure — it reads as a message nobody sent. That was the objection that closed
the earlier attempt at this, and it is the part worth getting right.
The original object is returned rather than `parsed.data`. Zod strips keys a
schema does not name, so handing back the parsed copy would turn a validation
step into a silent rewrite of every message that passed. The parse decides
whether a turn is well formed, not what it contains.
Failing closed to an open composer is unchanged: an unreadable history still lets
somebody type. Dropping the bad turns and keeping the good ones is strictly more
than casting everything was.
This is the browser half of what CopilotKit#199 reports as its third problem. The other
half — server-side run validation rejecting a whole run with `400 invalid_literal,
expected "function"`, which makes the thread unusable — is a separate change and
is not addressed here.
Diagnosis and the original test cases are Arpan Sarkar's, from CopilotKit#43; the shapes
asserted here are the ones that file found.
Closes CopilotKit#44
Restoring a thread ended `stored as Message[]` — a cast, not a check — so whatever
the history store held reached `setMessages` and then every projection that draws
a transcript. A turn in another shape took the conversation down rather than
itself: a tool call written `{id, name, args}` instead of AG-UI's
`{id, type: "function", function: …}` reached a renderer that dereferenced
`toolCall.function.arguments`, and one bad turn made a thread unopenable.
Each turn is now parsed with `MessageSchema` from `@ag-ui/core` and one that does
not parse is left out. Checked where history enters the app rather than in a
projection, because there are several projections and one history.
A dropped turn is counted and shown. `readThreadMessages` returns the count
beside the messages and the conversation draws a line naming it, because a turn
that quietly disappears from a record people read back is worse than a visible
failure — it reads as a message nobody sent. That was the objection that closed
the earlier attempt at this, and it is the part worth getting right.
The original object is returned rather than `parsed.data`. Zod strips keys a
schema does not name, so handing back the parsed copy would turn a validation
step into a silent rewrite of every message that passed. The parse decides
whether a turn is well formed, not what it contains.
Failing closed to an open composer is unchanged: an unreadable history still lets
somebody type. Dropping the bad turns and keeping the good ones is strictly more
than casting everything was.
This is the browser half of what CopilotKit#199 reports as its third problem. The other
half — server-side run validation rejecting a whole run with `400 invalid_literal,
expected "function"`, which makes the thread unusable — is a separate change and
is not addressed here.
Diagnosis and the original test cases are Arpan Sarkar's, from CopilotKit#43; the shapes
asserted here are the ones that file found.
Closes CopilotKit#44
Restoring a thread ended `stored as Message[]` — a cast, not a check — so whatever
the history store held reached `setMessages` and then every projection that draws
a transcript. A turn in another shape took the conversation down rather than
itself: a tool call written `{id, name, args}` instead of AG-UI's
`{id, type: "function", function: …}` reached a renderer that dereferenced
`toolCall.function.arguments`, and one bad turn made a thread unopenable.
Each turn is now parsed with `MessageSchema` from `@ag-ui/core` and one that does
not parse is left out. Checked where history enters the app rather than in a
projection, because there are several projections and one history.
A dropped turn is counted and shown. `readThreadMessages` returns the count
beside the messages and the conversation draws a line naming it, because a turn
that quietly disappears from a record people read back is worse than a visible
failure — it reads as a message nobody sent. That was the objection that closed
the earlier attempt at this, and it is the part worth getting right.
The original object is returned rather than `parsed.data`. Zod strips keys a
schema does not name, so handing back the parsed copy would turn a validation
step into a silent rewrite of every message that passed. The parse decides
whether a turn is well formed, not what it contains.
Failing closed to an open composer is unchanged: an unreadable history still lets
somebody type. Dropping the bad turns and keeping the good ones is strictly more
than casting everything was.
This is the browser half of what #199 reports as its third problem. The other
half — server-side run validation rejecting a whole run with `400 invalid_literal,
expected "function"`, which makes the thread unusable — is a separate change and
is not addressed here.
Diagnosis and the original test cases are Arpan Sarkar's, from #43; the shapes
asserted here are the ones that file found.
Closes #44
Problem
In toVisibleChatItems (app/src/components/channels/chat-messages.ts), non-string message.content was assumed to always be an array. If message.content was undefined, null, or an unexpected object format, calling .filter() threw an
unhandled TypeError that crashed the React chat transcript.
Solution
Verification
closes #44