fix(engine): normalize user message content to survive malformed session state

pi-coding-agent's _getUserMessageText calls content.filter(...) on user
messages; if a message lands in state.messages with content === undefined
(string or array expected), the library throws
"Cannot read properties of undefined (reading 'filter')", which gets
caught and stored on session.state.errorMessage and rethrown without a
stack. Fusion's existing message-content guard already normalized
assistant/toolResult messages — extend it to user messages as well, and
sweep state.messages once when the guard is installed so content loaded
from a session file is repaired before the first event fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 13:30:27 -07:00
parent 76961d41d3
commit c3fe4fd5b6

View File

@@ -477,12 +477,23 @@ function normalizeAssistantOrToolResultMessage(message: unknown): message is Rec
}
const role = (message as Record<string, unknown>).role;
if (role !== "assistant" && role !== "toolResult") {
if (role !== "assistant" && role !== "toolResult" && role !== "user") {
return false;
}
if (!Array.isArray((message as Record<string, unknown>).content)) {
(message as Record<string, unknown>).content = [];
const obj = message as Record<string, unknown>;
// `user` messages may carry content as a string (plain prompt) — leave those alone.
// For any other shape (undefined, null, object, etc.) coerce to an empty array so
// pi-coding-agent's _getUserMessageText (content.filter(...)) can't crash.
if (role === "user") {
if (typeof obj.content !== "string" && !Array.isArray(obj.content)) {
obj.content = [];
}
return true;
}
if (!Array.isArray(obj.content)) {
obj.content = [];
}
return true;
}
@@ -549,6 +560,16 @@ function installMessageContentGuard(session: AgentToolHookSession, sessionManage
return;
}
// Sweep any pre-existing state.messages (e.g. restored from a session file)
// so messages with malformed content can't crash pi-coding-agent's
// _getUserMessageText / similar array traversals before our event hooks fire.
const existingMessages = session.agent?.state?.messages;
if (Array.isArray(existingMessages)) {
for (const candidate of existingMessages) {
normalizeAssistantOrToolResultMessage(candidate);
}
}
if (typeof session.subscribe === "function") {
session.subscribe((event: unknown) => {
if (!event || typeof event !== "object" || (event as { type?: string }).type !== "message_end") {