diff --git a/src/cli/components/UserMessageRich.tsx b/src/cli/components/UserMessageRich.tsx index aaa316f..d11d80f 100644 --- a/src/cli/components/UserMessageRich.tsx +++ b/src/cli/components/UserMessageRich.tsx @@ -49,7 +49,7 @@ const COMPACT_PAD = 1; * System-reminder blocks are identified by ... tags. * Returns array of { text, isSystemReminder } objects in order. */ -function splitSystemReminderBlocks( +export function splitSystemReminderBlocks( text: string, ): Array<{ text: string; isSystemReminder: boolean }> { const blocks: Array<{ text: string; isSystemReminder: boolean }> = []; @@ -69,6 +69,17 @@ function splitSystemReminderBlocks( break; } + // Find the closing tag + const closeIdx = remaining.indexOf(tagClose, openIdx); + if (closeIdx === -1) { + // Malformed/incomplete tag - treat the whole remainder as literal user text. + const literal = remaining.trim(); + if (literal) { + blocks.push({ text: literal, isSystemReminder: false }); + } + break; + } + // Content before the tag is user content if (openIdx > 0) { const before = remaining.slice(0, openIdx).trim(); @@ -77,17 +88,6 @@ function splitSystemReminderBlocks( } } - // Find the closing tag - const closeIdx = remaining.indexOf(tagClose, openIdx); - if (closeIdx === -1) { - // Malformed - no closing tag, treat rest as system-reminder - blocks.push({ - text: remaining.slice(openIdx).trim(), - isSystemReminder: true, - }); - break; - } - // Extract the full system-reminder block (including tags) const sysBlock = remaining.slice(openIdx, closeIdx + tagClose.length); blocks.push({ text: sysBlock, isSystemReminder: true }); diff --git a/src/cli/helpers/backfill.ts b/src/cli/helpers/backfill.ts index b54b3e8..8186949 100644 --- a/src/cli/helpers/backfill.ts +++ b/src/cli/helpers/backfill.ts @@ -41,41 +41,16 @@ function normalizeLineEndings(s: string): string { return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); } -/** - * Truncate system-reminder content while preserving opening/closing tags. - * Removes the middle content and replaces with [...] to keep the message compact - * but with proper tag structure. - */ -function truncateSystemReminder(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - - const openIdx = text.indexOf(SYSTEM_REMINDER_OPEN); - const closeIdx = text.lastIndexOf(SYSTEM_REMINDER_CLOSE); - - if (openIdx === -1 || closeIdx === -1 || closeIdx <= openIdx) { - // Malformed, just use regular clip - return clip(text, maxLength); - } - - const openEnd = openIdx + SYSTEM_REMINDER_OPEN.length; - const ellipsis = "\n...\n"; - - // Calculate available space for content (split between start and end) - const overhead = - SYSTEM_REMINDER_OPEN.length + - SYSTEM_REMINDER_CLOSE.length + - ellipsis.length; - const availableContent = maxLength - overhead; - if (availableContent <= 0) { - // Not enough space, just show tags with ellipsis - return `${SYSTEM_REMINDER_OPEN}${ellipsis}${SYSTEM_REMINDER_CLOSE}`; - } - - const halfContent = Math.floor(availableContent / 2); - const contentStart = text.slice(openEnd, openEnd + halfContent); - const contentEnd = text.slice(closeIdx - halfContent, closeIdx); - - return `${SYSTEM_REMINDER_OPEN}${contentStart}${ellipsis}${contentEnd}${SYSTEM_REMINDER_CLOSE}`; +function removeSystemReminderBlocks(text: string): string { + return text + .replace( + new RegExp( + `${SYSTEM_REMINDER_OPEN}[\\s\\S]*?${SYSTEM_REMINDER_CLOSE}`, + "g", + ), + "", + ) + .trim(); } /** @@ -119,25 +94,16 @@ function renderAssistantContentParts( return out; } -/** - * Check if text is purely a system-reminder block (no user content before/after). - */ -function isOnlySystemReminder(text: string): boolean { - const trimmed = text.trim(); - return ( - trimmed.startsWith(SYSTEM_REMINDER_OPEN) && - trimmed.endsWith(SYSTEM_REMINDER_CLOSE) - ); -} - function renderUserContentParts( parts: string | LettaUserMessageContentUnion[], ): string { - // UserContent can be a string or an array of text OR image parts - // Pure system-reminder parts are truncated (middle) to preserve tags - // Mixed content or user text uses simple end truncation + // UserContent can be a string or an array of text OR image parts. + // Backfill should hide system-reminder blocks entirely. // Parts are joined with newlines so each appears as a separate line - if (typeof parts === "string") return parts; + if (typeof parts === "string") { + const normalized = normalizeLineEndings(parts); + return clip(removeSystemReminderBlocks(normalized), CLIP_CHAR_LIMIT_TEXT); + } const rendered: string[] = []; for (const p of parts) { @@ -145,13 +111,9 @@ function renderUserContentParts( const text = p.text || ""; // Normalize line endings (\r\n and \r -> \n) to prevent terminal garbling const normalized = normalizeLineEndings(text); - if (isOnlySystemReminder(normalized)) { - // Pure system-reminder: truncate middle to preserve tags - rendered.push(truncateSystemReminder(normalized, CLIP_CHAR_LIMIT_TEXT)); - } else { - // User content or mixed: simple end truncation - rendered.push(clip(normalized, CLIP_CHAR_LIMIT_TEXT)); - } + const withoutSystemReminders = removeSystemReminderBlocks(normalized); + if (!withoutSystemReminders) continue; + rendered.push(clip(withoutSystemReminders, CLIP_CHAR_LIMIT_TEXT)); } else if (p.type === "image") { rendered.push("[Image]"); } diff --git a/src/tests/cli/backfill-system-reminder.test.ts b/src/tests/cli/backfill-system-reminder.test.ts new file mode 100644 index 0000000..d3c6796 --- /dev/null +++ b/src/tests/cli/backfill-system-reminder.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from "bun:test"; +import type { Message } from "@letta-ai/letta-client/resources/agents/messages"; +import { createBuffers } from "../../cli/helpers/accumulator"; +import { backfillBuffers } from "../../cli/helpers/backfill"; +import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants"; + +function userMessage( + id: string, + content: string | Array<{ type: "text"; text: string }>, +): Message { + return { + id, + message_type: "user_message", + content, + } as unknown as Message; +} + +describe("backfill system-reminder handling", () => { + test("hides pure system-reminder content parts", () => { + const buffers = createBuffers(); + const history = [ + userMessage("u1", [ + { + type: "text", + text: `${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}`, + }, + { type: "text", text: "Real user message" }, + ]), + ]; + + backfillBuffers(buffers, history); + + const line = buffers.byId.get("u1"); + expect(line?.kind).toBe("user"); + expect(line && "text" in line ? line.text : "").toBe("Real user message"); + }); + + test("removes system-reminder blocks from string content while preserving user text", () => { + const buffers = createBuffers(); + const history = [ + userMessage( + "u2", + `${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}\n\nKeep this text`, + ), + ]; + + backfillBuffers(buffers, history); + + const line = buffers.byId.get("u2"); + expect(line?.kind).toBe("user"); + expect(line && "text" in line ? line.text : "").toBe("Keep this text"); + }); + + test("drops user rows that are only system-reminder content", () => { + const buffers = createBuffers(); + const history = [ + userMessage( + "u3", + `${SYSTEM_REMINDER_OPEN}\nInjected context\n${SYSTEM_REMINDER_CLOSE}`, + ), + ]; + + backfillBuffers(buffers, history); + + expect(buffers.byId.get("u3")).toBeUndefined(); + expect(buffers.order).toHaveLength(0); + }); +}); diff --git a/src/tests/cli/userMessageRich.test.ts b/src/tests/cli/userMessageRich.test.ts new file mode 100644 index 0000000..75df921 --- /dev/null +++ b/src/tests/cli/userMessageRich.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; +import { splitSystemReminderBlocks } from "../../cli/components/UserMessageRich"; + +describe("splitSystemReminderBlocks", () => { + test("treats unmatched system-reminder opener as literal user text", () => { + const text = "like the etc included."; + const blocks = splitSystemReminderBlocks(text); + + expect(blocks).toEqual([{ text, isSystemReminder: false }]); + }); + + test("still detects well-formed system-reminder blocks", () => { + const blocks = splitSystemReminderBlocks( + "before\n\ncontext\n\nafter", + ); + + expect(blocks.some((b) => b.isSystemReminder)).toBe(true); + expect(blocks.some((b) => b.text.includes("before"))).toBe(true); + expect(blocks.some((b) => b.text.includes("after"))).toBe(true); + }); +});