Skip to content

fix: prevent emoji title loss and link duplicates#696

Open
maccman wants to merge 1 commit into
nextfrom
codex/fix-cross-device-wikilink-duplicates
Open

fix: prevent emoji title loss and link duplicates#696
maccman wants to merge 1 commit into
nextfrom
codex/fix-cross-device-wikilink-duplicates

Conversation

@maccman

@maccman maccman commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

On iOS/WebKit, a leading emoji or the space after it can still be a pending DOM mutation when the editor blurs. Reflect previously flushed the last ProseMirror callback buffer immediately, so the visible title 🧠 Business ideas could be persisted as # Business ideas (or briefly as # 🧠Business ideas). The filename correctly remained notes/business-ideas.md, but the saved H1 and a device's live title could diverge.

That divergence became destructive when another synced file contained [[Business ideas]]: a device with the stale/exact-only title index could treat the link as missing and silently create notes/business-ideas-2.md.

Before -> After

Scenario Before After
Emoji composition followed immediately by blur/backgrounding The previous editor snapshot could be saved without the emoji or its following space Pending composition and blur mutations are committed before every persistence flush
Git pull applies changed notes Sync could report idle before direct reindexing finished Remote-change indexing is an awaited sync barrier
[[Business ideas]] meets # 🧠 Business ideas while exact lookup lags Missing-link flow could create business-ideas-2.md Exact matches win; one normalized on-disk collision is reused; ambiguous matches do not create
A sync/create race claims the same note path Availability probe and write were separate, overwrite-capable operations Native note_create atomically claims one free path and returns collisions without replacing bytes

Changes

  1. Commit pending WebKit input before persistence

    • Added a narrow editor snapshot barrier that drains both ProseMirror's forceFlush() composition timer and its separate blur queue before serialization.
    • Wired the barrier into note-session flushes and editor unbinding, covering blur, backgrounding, navigation, and quit teardown.
    • Kept the ProseMirror internal reach-in isolated and guarded so an incompatible editor implementation degrades to the existing snapshot behavior.
  2. Make sync settlement mean index convergence

    • SyncEngineOptions.onRemoteChanges may now be async, and the engine awaits it before later Git work or an idle status.
    • The backup controller awaits applyIndexChanges and invalidates dependent queries only after the pulled files are indexed.
    • Stop, suppression, and error semantics remain explicit around the async barrier.
  3. Guard unresolved-link creation

    • Added a conservative fallback key that removes only a leading emoji sequence and normalizes Unicode whitespace. Normal foldKey remains unchanged, so emoji-distinct titles stay distinct.
    • resolveOrCreateNoteWithTitle checks the exact index, inspects the title-derived on-disk slug family (titles, frontmatter aliases, and derived subject aliases), accepts only one fallback match, and blocks ambiguous/unreadable matches with visible feedback.
    • Wiki-link autocomplete suppresses Create/contact actions when a known fallback collision exists, and its background create path uses the same guarded resolver.
  4. Make note creation no-clobber

    • Added generation-pinned native note_create, staged under .reflect/tmp and persisted with persist_noclobber.
    • Real files and iCloud eviction placeholders both count as collisions; collision outcomes do not emit local-write echoes.
    • Candidate collisions trigger index/disk re-resolution before a suffix is considered. The in-browser iOS development bridge implements the same contract.

Tests

  • Editor/session regressions cover delayed composition, the separate blur queue, stale-buffer replacement, unbind teardown, and exact emoji H1 persistence.
  • A cross-device seam test directly indexes # 🧠 Business ideas plus externally authored [[Business ideas]], forces the reported exact-index miss, and verifies the original path is reused with zero create calls and no -2 file.
  • Resolver tests cover exact precedence, emoji/whitespace aliases, unique fallback reuse, ambiguity and placeholders, index races, and atomic claim collisions.
  • Sync tests cover awaited reindexing, lifecycle suppression, async failures, stop behavior, and the backup-controller idle boundary.
  • Native tests prove create-if-absent never replaces an existing note or eviction placeholder and pin the Rust/TypeScript response shape.
  • A temporary Meowdown 0.42 WebKit harness reproduced both reported title shapes and passed 3/3 scenarios with the barrier, including serialization from the synchronous document-change callback.

Verification

  • pnpm check (passes; only the existing max-lines warnings in graph-provider.tsx and indexer.ts)
  • pnpm --filter @reflect/core test src/graph/commands.test.ts src/graph/create-note.test.ts src/graph/cross-device-title-resolution.test.ts src/markdown/keys.test.ts src/sync/engine.test.ts — 58 passed
  • pnpm --filter @reflect/desktop test src/dev/dev-bridge.test.ts src/mobile/mobile-screen.test.tsx src/editor/use-wiki-link-navigation.test.tsx src/editor/use-editor-autocomplete.test.tsx src/editor/wiki-autocomplete-entries.test.ts src/editor/pending-input.test.tsx src/editor/note-session.test.ts src/editor/use-note-document.test.tsx src/lib/backup-controller.test.ts — 171 passed
  • cargo fmt --check
  • cargo test -p reflect-open atomic_create — 2 passed
  • cargo test -p reflect-open outcome_serializes_for_the_typescript_boundary — 1 passed

Risk / Rollout

  • No schema migration or data backfill is required; Markdown remains the source of truth.
  • The pending-input barrier currently uses a guarded ProseMirror internal observer API because Meowdown does not expose a committed-snapshot method. If that internal shape changes, the guard safely skips it; an upstream Meowdown API remains the cleaner long-term boundary.
  • The disk fallback deliberately scans only the filename collision family. Arbitrary aliases on unrelated legacy filenames continue to rely on the now-awaited index, avoiding a full graph read on every unresolved click.
  • note_create is atomic for each candidate path, not a lock over the entire slug family or external filesystem providers. Normal Git pulls are covered by awaited indexing plus collision re-resolution; a different suffix arriving from an external provider in the exact successful-claim window remains a narrow residual race.

Note

Medium Risk
Touches note persistence, sync lifecycle, and create/link paths across Rust and TypeScript; the ProseMirror domObserver reach-in is guarded but depends on internal API shape.

Overview
Fixes WebKit/WebKit-style editors dropping leading emoji (and spacing) when blur or backgrounding races ProseMirror’s delayed DOM flush, and stops stale indexes from minting duplicate slug-family notes when links like [[Business ideas]] target an emoji-titled file.

Persistence: Before every save flush and editor unbind, the note editor commits pending native input by draining ProseMirror’s domObserver (forceFlush + flush) so composed text is in markdown before the session writes.

Creation & IPC: New generation-pinned note_create / createNoteIfAbsent uses staged persist_noclobber writes (iCloud placeholders count as collisions). createNoteWithTitle switches from overwrite-capable note_write to this path; dev bridge mirrors the contract.

Link resolution: resolveOrCreateNoteWithTitle re-checks the index, scans the on-disk slug family (titles, aliases, foldFallbackTitleKey for leading-emoji normalization), blocks ambiguous/unreadable matches with user-visible errors, and re-resolves after atomic collisions instead of blindly suffixing. Wiki navigation, [[ autocomplete, and Create-row suppression use the same guard.

Sync: onRemoteChanges is awaited through merge and backup indexing so idle sync means pulled notes are indexed, not just merged on disk.

Reviewed by Cursor Bugbot for commit ccd5e08. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe8419db-7818-4b74-a7b5-4e001538aa3a

📥 Commits

Reviewing files that changed from the base of the PR and between 912406c and ccd5e08.

📒 Files selected for processing (40)
  • apps/desktop/src-tauri/src/fs/io.rs
  • apps/desktop/src-tauri/src/fs/mod.rs
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/components/route-content.test.tsx
  • apps/desktop/src/dev/dev-bridge.test.ts
  • apps/desktop/src/dev/dev-bridge.ts
  • apps/desktop/src/dev/dev-file-store.ts
  • apps/desktop/src/editor/note-editor.tsx
  • apps/desktop/src/editor/note-session-state.ts
  • apps/desktop/src/editor/note-session-types.ts
  • apps/desktop/src/editor/note-session.test.ts
  • apps/desktop/src/editor/pending-editor-input.ts
  • apps/desktop/src/editor/pending-input.test.tsx
  • apps/desktop/src/editor/use-editor-autocomplete.test.tsx
  • apps/desktop/src/editor/use-editor-autocomplete.ts
  • apps/desktop/src/editor/use-note-document.test.tsx
  • apps/desktop/src/editor/use-note-document.ts
  • apps/desktop/src/editor/use-template-slash-items.test.ts
  • apps/desktop/src/editor/use-wiki-link-navigation.test.tsx
  • apps/desktop/src/editor/use-wiki-link-navigation.ts
  • apps/desktop/src/editor/wiki-autocomplete-entries.test.ts
  • apps/desktop/src/editor/wiki-autocomplete-entries.ts
  • apps/desktop/src/lib/attach-files.test.ts
  • apps/desktop/src/lib/backup-controller.test.ts
  • apps/desktop/src/lib/backup-controller.ts
  • apps/desktop/src/mobile/mobile-screen.test.tsx
  • apps/desktop/src/mobile/screens/tasks.test.tsx
  • packages/core/src/exports/platform.ts
  • packages/core/src/exports/sync-markdown-indexing.ts
  • packages/core/src/graph/commands.test.ts
  • packages/core/src/graph/commands.ts
  • packages/core/src/graph/create-note.test.ts
  • packages/core/src/graph/create-note.ts
  • packages/core/src/graph/cross-device-title-resolution.test.ts
  • packages/core/src/graph/schemas.ts
  • packages/core/src/markdown/index.ts
  • packages/core/src/markdown/keys.test.ts
  • packages/core/src/markdown/keys.ts
  • packages/core/src/sync/engine.test.ts
  • packages/core/src/sync/engine.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-cross-device-wikilink-duplicates

Comment @coderabbitai help to get the list of available commands.

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