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);
+ });
+});