Empty paragraphs make room projection non-canonical, failing document descriptions
lavaman131 opened ·
## Summary
If a document contains an empty paragraph, its generated description never refreshes. The `document-summary` background job fails both attempts with:
``` chopin: background job document-summary <id> attempt 1 failed - Error: Document source is not canonical. chopin: background job document-summary <id> attempt 2 failed - Error: Document source is not canonical. ```
The job ends as `failed` with reason `attempts-exhausted:attempt-error`. The failing check (`apps/server/src/jobs/document-summary.ts:338`) runs before any harness call, so this happens with every `HARNESS` value.
## Reproduction
1. Run with `AGENT=on BACKGROUND_JOBS=on`, open a document, and type a paragraph. 2. Leave an empty paragraph anywhere in the document. Pressing Enter at the end is enough, and so is inserting a `/research` card, which leaves an empty paragraph after it. 3. Wait for the description debounce (30 s).
The stored snapshot source ends with an extra blank line:
``` "The harp needs new strings before the spring recital. We have a budget for one octave.\n\n<Research id=\"d5a1b471-…\" />\n\n" ```
Running `serialize(parse(source))` returns the same text with a single trailing `\n`, so the job's canonical check rejects the source.
This was observed while verifying background workers under `HARNESS=atomic` (#210). The parent description job succeeded before the Research card was inserted and failed after it. The research jobs themselves completed.
## Cause
`$exportPlanTree` in `packages/dialect/src/convert.ts` exports each empty Lexical paragraph as an MDAST `paragraph` with no children. `serialize` writes that as an extra blank line. `parse` never produces an empty paragraph, so the room persists source that doesn't survive its own round trip. This violates the "projected to canonical MDX" invariant in AGENTS.md. It happens at the root, between blocks, and inside containers such as blockquotes (`> Quoted\n>\n>\n`).
Block addressing already ignores these paragraphs. `addressable()` in `apps/server/src/plan/room.ts` skips root-level empty paragraphs, and `digests()` goes through `parse`.
## Expected
Room projection is canonical, and `serialize(parse(project(room))) === project(room)` holds when the live tree contains empty paragraphs. Description jobs then succeed for these documents.
lavaman131assigned lavaman131last month on GitHub
lavaman131referenced this in Drop empty paragraphs from canonical document projection #212last month on GitHub
lavaman131referenced 908f1b773fdalast month on GitHub
lavaman131referenced e1fc8c359de2last month on GitHub
MaggieAppletonreferenced cd0242ecbcfblast month on GitHub
benjaminkittreferenced 0dc886377a58last month on GitHub
shanehollomanreferenced bae781859789last month on GitHub