Skip to content

ENG-2219 Add an asset copy stage to Roam node materialization - #1401

Open
maparent wants to merge 2 commits into
eng-2217-rewrite-asset-links-in-obsidian-origin-markdown-for-roamfrom
eng-2219-add-an-asset-copy-stage-to-roam-node-materialization
Open

ENG-2219 Add an asset copy stage to Roam node materialization#1401
maparent wants to merge 2 commits into
eng-2217-rewrite-asset-links-in-obsidian-origin-markdown-for-roamfrom
eng-2219-add-an-asset-copy-stage-to-roam-node-materialization

Conversation

@maparent

@maparent maparent commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Reviewer brief

Verification

Loom video

Scope check

  • Ran $scope-check against the ENG ticket and final diff.
  • Scope beyond Done When: Two additions.
    1. Guards against the new stage's irreversible uploads: the title-collision check was extracted into titleCollisionFailure and is now made in materializeSharedNode before the asset stage, and rewriteAssetLinks gained exported lookupCandidates/collectAssetTokens (with parseMatch) so the stage copies only references the rewriter can resolve.
    2. A refactor: getErrorMessage moved to a new apps/roam/src/utils/getErrorMessage.ts, replacing the copies in materializeSharedNode and in mirrorAssetToRoamStorage (an ENG-2218 file).
  • Required now:
    1. Yes. Roam exposes no API to list or delete an uploaded blob, so an upload made for a node that then fails on a title collision, or for a row whose token no longer appears in the fetched markdown, is a permanent charge on the user's storage. Both cases become reachable the moment this stage ships, so neither can be deferred.
    2. No. A module-local copy in importNodeAssets satisfied the ticket; the shared module is cleanup that also touches files outside it. But we now had four shared copies.
  • Anyone affected or consulted: no.
  • Decision: Not documented.

Local delegated full review

  • Ran a comprehensive review of the entire final diff in a subagent with a fresh context. Use $dg-delegated-full-review when no other full-review workflow is available.

https://linear.app/discourse-graphs/issue/ENG-2219/add-an-asset-copy-stage-to-roam-node-materialization

@linear-code

linear-code Bot commented Sep 6, 2026

Copy link
Copy Markdown

ENG-2219

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
discourse-graph Skipped Skipped Sep 6, 2026 11:00pm UTC

Request Review

@supabase

supabase Bot commented Sep 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from cd7452d to 9c38544 Compare September 6, 2026 16:01
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 9c38544 to 8f65942 Compare September 6, 2026 16:39
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 8f65942 to 4620a4f Compare September 6, 2026 17:06
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 4620a4f to 45d86a6 Compare September 6, 2026 18:20
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 45d86a6 to 06a464c Compare September 6, 2026 20:06
@maparent
maparent marked this pull request as ready for review September 6, 2026 20:10
@graphite-app

graphite-app Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

Comment on lines +408 to +410
// The asset stage never ran, so nothing was uploaded. A rejected import must leave no
// residue: a copy into Roam storage cannot be undone or even found afterwards.
expect(from).not.toHaveBeenCalledWith("my_file_references");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test assertion doesn't validate what the comment claims. Since importNodeAssets is mocked in beforeEach (lines 134-136), calling it would never invoke from regardless. This assertion always passes even if the asset stage runs.

// The asset stage never ran, so nothing was uploaded. A rejected import must leave no
// residue: a copy into Roam storage cannot be undone or even found afterwards.
expect(mockedImportNodeAssets).not.toHaveBeenCalled();
Suggested change
// The asset stage never ran, so nothing was uploaded. A rejected import must leave no
// residue: a copy into Roam storage cannot be undone or even found afterwards.
expect(from).not.toHaveBeenCalledWith("my_file_references");
// The asset stage never ran, so nothing was uploaded. A rejected import must leave no
// residue: a copy into Roam storage cannot be undone or even found afterwards.
expect(mockedImportNodeAssets).not.toHaveBeenCalled();

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +427 to +431
const assets = await importNodeAssets({
client,
sharedNode,
markdown: content.markdown,
}).catch((error: unknown) => ({ error }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Failed imports leave permanent uploads

When importNodeAssets succeeds before a later page operation fails, its uploads remain unreferenced and cannot be deleted. Retrying can consume more storage without completing the import.

Prompt for agents
The new asset stage runs before createImportedPage or updateImportedPage. Successful Roam uploads are irreversible, but either subsequent function can fail during page creation, block replacement, rename, or source-identity persistence. In those cases materializeSharedNode reports failure after leaving permanent uploads that no imported page references. Rework the transaction order or failure semantics so uploaded assets remain associated with a successful materialization. Account for retries and for both create and update paths. If uploads must remain first so rewritten links are available, preserve enough state to avoid orphaning or re-uploading bytes when later stages fail.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +150 to +155
const resolvable = new Set(
collectAssetTokens(markdown).flatMap(lookupCandidates),
);
const referenced = references.filter(({ filepath }) =>
resolvable.has(filepath),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Spaced attachment links remain broken

For ![](<my folder/diagram.png>), collectAssetTokens finds no token, so the recorded attachment is excluded. The imported page retains a vault-only path that Roam cannot render.

Prompt for agents
importNodeAssets filters reference rows through collectAssetTokens before mirroring. The LINK_PATTERN used by collectAssetTokens only accepts unwrapped destinations without whitespace. Valid Markdown angle-bracket destinations such as ![](<my folder/diagram.png>) therefore never match, even when FileReference.filepath contains my folder/diagram.png. Extend token parsing and rewriting together to support angle-bracket Markdown destinations, and add importNodeAssets and rewriteAssetLinks tests proving the row is copied and the link is rewritten.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +150 to +155
const resolvable = new Set(
collectAssetTokens(markdown).flatMap(lookupCandidates),
);
const referenced = references.filter(({ filepath }) =>
resolvable.has(filepath),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Encoded aliases create unused uploads

When rows exist for both an encoded token and its decoded spelling, resolvable selects both. rewriteAssetLinks uses only the raw match, leaving the other upload permanently unused.

Prompt for agents
The resolvable Set flattens every lookup candidate, so it cannot preserve rewriteAssetLinks' ordered choice. For markdown containing my%20folder/d.png and reference rows named both my%20folder/d.png and my folder/d.png, importNodeAssets mirrors both rows. rewriteAssetLinks then chooses the raw candidate first and never references the decoded row's upload. Select at most the first matching reference row per parsed token using the same candidate priority as rewriteAssetLinks, while still supporting multiple distinct tokens and deduplicating repeated references.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 06a464c to 1043e32 Compare September 6, 2026 22:32
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 1043e32 to 61bedb2 Compare September 6, 2026 22:42
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 61bedb2 to 4aa1ebe Compare September 6, 2026 22:55
@maparent
maparent force-pushed the eng-2219-add-an-asset-copy-stage-to-roam-node-materialization branch from 4aa1ebe to 09adb17 Compare September 6, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant