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.
This commit is contained in:
Mohamed Boudra
2026-03-21 13:31:11 +07:00
parent 480af26b57
commit 84500cdcae
@@ -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;
}