From 84500cdcae68e79589e6e0413db487cd4eda7cc0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 21 Mar 2026 13:31:11 +0700 Subject: [PATCH] fix(server): make mapBlocksToTimeline role-aware to prevent user text emitting as assistant_message User messages with array content were routed through mapBlocksToTimeline which hardcoded all text blocks as assistant_message. This caused user interrupt text to appear as assistant output in paseo logs and the app UI. Add textMessageType option so callers declare the role explicitly. When set to "user_message", text blocks coalesce into a single item matching extractUserMessageText semantics. --- .../server/agent/providers/claude-agent.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index fcd948ee0..67cd7386a 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -3662,7 +3662,9 @@ class ClaudeAgentSession implements AgentSession { provider: "claude", }); } else if (Array.isArray(content)) { - const timelineItems = this.mapBlocksToTimeline(content); + const timelineItems = this.mapBlocksToTimeline(content, { + textMessageType: "user_message", + }); for (const item of timelineItems) { if (item.type === "user_message" && messageId && !item.messageId) { events.push({ @@ -4181,30 +4183,40 @@ class ClaudeAgentSession implements AgentSession { private mapBlocksToTimeline( content: string | ClaudeContentChunk[], options?: { + textMessageType?: "assistant_message" | "user_message"; suppressAssistantText?: boolean; suppressReasoning?: boolean; } ): AgentTimelineItem[] { - const suppressAssistant = options?.suppressAssistantText ?? false; + const textMessageType = options?.textMessageType ?? "assistant_message"; + const suppressText = + textMessageType === "assistant_message" && + (options?.suppressAssistantText ?? false); const suppressReasoning = options?.suppressReasoning ?? false; if (typeof content === "string") { if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) { return []; } - if (suppressAssistant) { + if (suppressText) { return []; } - return [{ type: "assistant_message", text: content }]; + return [{ type: textMessageType, text: content }]; } const items: AgentTimelineItem[] = []; + const userTextParts: string[] = []; for (const block of content) { switch (block.type) { case "text": case "text_delta": if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) { - if (!suppressAssistant) { + if (textMessageType === "user_message") { + const trimmed = block.text.trim(); + if (trimmed) { + userTextParts.push(trimmed); + } + } else if (!suppressText) { items.push({ type: "assistant_message", text: block.text }); } } @@ -4237,6 +4249,14 @@ class ClaudeAgentSession implements AgentSession { break; } } + + if (textMessageType === "user_message" && userTextParts.length > 0) { + items.unshift({ + type: "user_message", + text: userTextParts.join("\n\n"), + }); + } + return items; }