diff --git a/packages/snapcompact/src/prompts/snapcompact-summary.md b/packages/snapcompact/src/prompts/snapcompact-summary.md
index 182f23d0a..f74a4e7bc 100644
--- a/packages/snapcompact/src/prompts/snapcompact-summary.md
+++ b/packages/snapcompact/src/prompts/snapcompact-summary.md
@@ -1,6 +1,11 @@
Prior conversation history has been archived verbatim onto {{frameCount}} snapcompact frame{{#if multipleFrames}}s{{/if}} — the bitmap image{{#if multipleFrames}}s{{/if}} attached below{{#if multipleFrames}}, ordered oldest to newest{{/if}}.
-Reading a frame: monospace {{fontCell}} pixel font on a white background, {{#if docColumns}}typeset as two word-wrapped newspaper columns of {{cols}} characters by {{rows}} lines each — read the left column top to bottom, then the right column{{else}}{{cols}} characters per row, {{rows}} text rows per frame; read left to right, top to bottom. Text flows continuously with no word wrap, so words may break across row ends{{/if}}. Horizontal whitespace runs were collapsed to single spaces; line breaks print as a solid black cell (one character wide) — treat each as a newline. {{#if sentenceInk}}Ink color cycles through six colors, advancing at sentence boundaries — a color change marks a new sentence.{{else}}Glyphs are plain black ink.{{/if}}{{#if stopwordDimmed}} Common function words (the, of, and, …) are printed in dim gray; content words carry the full ink.{{/if}}{{#if dimmedToolResults}} Tool output is printed in dim gray ink — gray text is archived tool output, not conversation.{{/if}}{{#if lineRepeated}} Every text line is printed twice in a row — first on the white background, then repeated on a pale yellow band. The copies are identical: read each line once and use the duplicate only to double-check hard glyphs.{{/if}} Roles are tagged inline as [User]:, [Assistant]:, [Think]:, [Tool Call]:, and [Tool Result]:.
+Reading a frame: a grid {{cols}} cells wide and up to {{rows}} tall{{#if docColumns}}, set as two word-wrapped columns — read the left column top to bottom, then the right{{else}} — read left to right, top to bottom, with no word wrap, so words may break across rows{{/if}}. A solid black cell is a newline; runs of spaces collapse to one. Turns are headed # User ¶, # Assistant ¶, or # Tool call ¶; assistant reasoning is _italic_ and tool output sits in ….
+{{#if sentenceInk}}- Ink cycles six colors, one per sentence.
+{{/if}}{{#if stopwordDimmed}}- Function words are dim gray; content words keep full ink.
+{{/if}}{{#if dimmedToolResults}}- Text inside is dim gray; that gray is archived tool output, not conversation.
+{{/if}}{{#if lineRepeated}}- Each line is printed twice (white, then a pale-yellow band); the copies are identical.
+{{/if}}
{{#if mixedShapes}}
Older frames may use a different font, grid, or ink coloring than described above; the reading order is always the same (left to right, top to bottom, oldest frame first).
diff --git a/packages/snapcompact/src/snapcompact.ts b/packages/snapcompact/src/snapcompact.ts
index bfdd71fe1..1389f3acc 100644
--- a/packages/snapcompact/src/snapcompact.ts
+++ b/packages/snapcompact/src/snapcompact.ts
@@ -740,9 +740,11 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
for (const block of msg.content) {
if (block.type === "text") {
- pendingText.push(stripDimMarkers(block.text));
+ const text = stripDimMarkers(block.text);
+ if (text.trim()) pendingText.push(text);
} else if (block.type === "thinking") {
- pendingThinking.push(stripDimMarkers(block.thinking));
+ const thinking = stripDimMarkers(block.thinking);
+ if (thinking.trim()) pendingThinking.push(thinking);
} else if (block.type === "toolCall") {
if (uselessCallIds.has(block.id)) continue;
flushAssistant();
diff --git a/packages/snapcompact/test/snapcompact.test.ts b/packages/snapcompact/test/snapcompact.test.ts
index b4fb20b97..6cb6df38a 100644
--- a/packages/snapcompact/test/snapcompact.test.ts
+++ b/packages/snapcompact/test/snapcompact.test.ts
@@ -609,6 +609,22 @@ describe("serializeConversation", () => {
expect(out).toBe("# Assistant ¶\nbefore\nafter");
});
+ it("drops blank text/thinking blocks instead of emitting an empty assistant heading", () => {
+ const out = snapcompact.serializeConversation(
+ [
+ createAssistantMessage([
+ { type: "thinking", thinking: " " },
+ { type: "text", text: "" },
+ { type: "toolCall", id: "c1", name: "read", arguments: { path: "a.ts" } },
+ ]),
+ { ...createToolResultMessage("body"), toolCallId: "c1" } as Message,
+ ],
+ { dimToolResults: false },
+ );
+ expect(out).toBe('# Tool call ¶\nread(path="a.ts")\n\nbody\n');
+ expect(out).not.toContain("# Assistant ¶");
+ });
+
it("wraps tool-result bodies in dim toggles by default and strips stray toggles from content", () => {
const out = snapcompact.serializeConversation([
createUserMessage(`hello ${snapcompact.DIM_ON}world`),