Repository navigation
Conversation
…kyreader The Margin highlight import only ever added. Each poll now also reconciles every highlight linked to a Margin note: a note edited on Margin updates it, and one deleted there removes an imported highlight or unpublishes one made here. Absence is only trusted from a complete listing of the repo, for records older than a grace window, and never over a queued or in-flight local write. The poll also runs app-wide (open and tab return), not only on /highlights and review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
Track Margin note writes by rkey (in flight or started within a minute of the listing) and skip them, read the sync queue before and after the listing, date rkeys by the server's clock, stop before removals if the import was switched off or the account changed mid-poll, fall back to the highlight's own source URL for a note edit, and say in the docs what a Margin deletion takes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
disnet
marked this pull request as ready for review
October 27, 2026 22:54
…on't revive a deleted note Each highlight now remembers the note Margin's record was last known to hold (marginNote), set whenever Skyreader writes the record or takes Margin's version. A local note that differs from it is an unsent edit, so the sync leaves it alone on any device, rather than only where the write is queued. A note update on a record deleted on Margin now 404s instead of recreating it (which the next poll would import as a second highlight); the client unpublishes its highlight on that 404, direct or from the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
… real deletion - note_gone only when the PDS says the record isn't there, not for any definitive read failure (a refused token is not a deletion), and the client only treats a 404 carrying that code as one - an edit made here that never reached Margin (lost from a queue, cleared, made while a publish was queued) is sent again by the next poll instead of freezing Margin's side for good - a highlight linked before marginNote existed gets it recorded on the first poll where both sides agree, one write per item - unlink and sync bookkeeping act only while the highlight still links to the record the write was about; Undo drops marginNote with the link Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
- one MarginSyncControl, shared by Settings, the review deck's gear and a new line at the top of /highlights: the switch, plus Syncing… / Synced 5m ago / partial / lapsed-with-Allow-Margin-access - marginSync store: whether a poll is out, when it last finished (per device and account), whether it was partial, whether the grant lapsed - a toast, up long enough to read, when a sync removes or unpublishes highlights deleted on Margin - toggle renamed to Sync highlights with Margin; docs follow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
- A later sync replaces what the toggle said, instead of the note sticking - Say when a sync couldn't reach Margin - Render the bar at once (switch disabled until the permission check lands), so it doesn't push /highlights down - Only mention the permission check while the sync is on, and stop telling an online reader to come back online - Removals are reported by the toast alone; the toast ends with a period - Component test for the control; store tests for the failed state Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Tim · project thread
What
Skyreader already had a one-way Margin highlight import ("Bring in highlights from Margin"). It only ever added, so a note edited or a highlight deleted on Margin never reached Skyreader. This adds the sync half, and shows the reader that it is happening.
Each poll now also reconciles every highlight linked to an
at.margin.note, whether it was imported or published from here:Sync direction. Margin to Skyreader is a full reconcile (add, edit, delete), the same diff-against-what-we-hold idea as linked Shelf collections. Skyreader to Margin stays per-highlight "Publish to Margin". Edits and deletes of published highlights already propagate, and the poll now re-sends an edit that never got there. Auto-publishing every highlight would make private ones public, so this PR deliberately doesn't do it.
When it runs. The poll is still interval-gated (15 min) and shared by callers.
AppShellnow also triggers it app-wide, on load and when the tab becomes visible, not only on /highlights and the review deck.Showing the sync to the reader
Tim asked how a reader knows the sync is happening. Before this, the poll ran silently.
MarginSyncControl.svelte, shared by both places), with the same status line.marginSync.svelte.ts(new) holds syncing, failed, partial and lapsed state, and the last-synced time per account in localStorage.How conflicts are decided
Each linked highlight carries
marginNote: the note Margin's record was last known to hold. It's set when Skyreader publishes or updates the record, on import, and when the sync takes Margin's version. A local note that differs from it is an edit made here that hasn't reached Margin yet, on whatever device it was made. The sync never reads Margin's older note over it, and sends it again when nothing of it is still queued or in flight.Highlights linked before this field existed get it recorded on the first poll where both sides agree. That's one write per item. Until then, Margin wins for them, guarded as below.
Safety rails
A deletion is only read from a listing that can prove it:
rkeys(every note record listed, parseable or not),did, and a server-clocklistedAt. A record that still exists but no longer parses is left alone, and an older backend withoutrkeysnever triggers deletions.listedAt.The sync also skips highlights that changed while the listing was out:
marginWrites.ts);It stops before applying anything if the import was switched off, or the account changed, mid-poll.
A note update on a record that is gone now returns
404 note_goneinstead of recreating it. Recreating would undo the deletion, and the next poll would import the revived note as a second highlight. The client unpublishes on that 404, whether the update was sent directly or from the queue.note_goneonly fires when the PDS saysRecordNotFound. A refused token or similar is not a deletion and fails as before.Changes
backend/src/routes/integrations.ts:GET /api/integrations/margin/highlightsalso returnsrkeys,did,listedAt.PUT /api/integrations/margin/notes/:rkeyreturns 404note_gonefor a deleted note.frontend/src/lib/types:Highlight.marginNote.frontend/src/lib/utils/highlightAliases.ts:marginSyncedmutation (rkey-guarded); themarginmutation records and clearsmarginNote.frontend/src/lib/utils/marginHighlightImport.ts:planMarginHighlightSync(pure). Change kinds:note,synced,push,remove,unlink.frontend/src/lib/services/marginHighlightImport.ts: applies the plan after import and rekey, batching note bookkeeping per item. The outcome gainsupdated,removed,unpublished. It reports tomarginSyncStoreand toasts removals.frontend/src/lib/services/marginHighlights.tsandsync-queue.ts: recordmarginNoteon publish and update. Onnote_gone, unlink, only while the highlight still links to that record.pendingMarginNoteHighlightIds().frontend/src/lib/services/marginWrites.ts(new), wired intoapi.updateMarginNoteanddeleteMarginNote.frontend/src/lib/stores/marginSync.svelte.ts(new),components/settings/MarginSyncControl.svelte(new, extracted fromHighlightSettings.svelte),HighlightsPage.svelte(the bar),toast.svelte.ts(lingerMs).AppShell.svelte: the app-wide poll.highlightReview.ts: redeal an untouched deck after removals too.guide/saving-and-highlights.md.Tests
svelte-checkreports 0 errors. New tests cover the planner (conflicts, resend, legacy baseline, deletion rails), themarginSyncedmutation,marginWrites,pendingMarginNoteHighlightIds, the 404 handling inupdateHighlightNoteOnMargin, redeal-after-removal,marginSyncStore, and a mounted test ofMarginSyncControl.margin-highlights.spec.tspasses. It covers the listing fields, the 404 on a deleted note, no 404 on a refused read, and the merge onto a note that still exists.Review
Each round ran the code-review skill at high effort and an independent breaker subagent.
Round 1 (sync), fixed
Round 2 (cross-device fixes Tim asked to fold in), fixed
RecordNotFoundonly, and the client requires thenote_gonecode.marginNote.Round 3 (sync visibility UI), fixed
Left alone
shelf-link.ts,saveBacking,QueueBackingandSaveBackingPicker. Those came from Keep a linked collection's new saves out of the Queue's inbox #703's diff, so they're out of scope here.🤖 Generated with Claude Code
https://claude.ai/code/session_014mh1a5TEH18exKJYBvhJoC