Skip to content

Empty paragraphs make room projection non-canonical, failing document descriptions #211

Description

@lavaman131

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions