Skip to content

feat: add bidirectional notebook sync#117

Merged
tomymaritano merged 11 commits into
developfrom
feature/notebook-sync
Mar 11, 2026
Merged

feat: add bidirectional notebook sync#117
tomymaritano merged 11 commits into
developfrom
feature/notebook-sync

Conversation

@tomymaritano

Copy link
Copy Markdown
Collaborator

Summary

  • Add notebook sync with cursor-based push/pull (same pattern as notes)
  • Server-side tree validation: depth ≤ 2, valid parentId, no circular refs
  • SQLite triggers for automatic change tracking (needs_sync, local_version)
  • Notebooks sync before notes (dependency order)
  • No encryption needed — notebooks are metadata only

Changes

  • API: notebookSyncLog table + GET/POST /sync/notebooks endpoints
  • Storage: Migration 015 with sync tracking columns and triggers
  • Desktop: SyncService now orchestrates notebook → note sync cycle
  • Shared: validateNotebookTree() extracted to @readied/sync-core

Test plan

  • 8 tree validation unit tests (depth, cycles, orphans, multi-device)
  • All 37 tests passing (12 sync-core + 25 desktop)
  • TypeScript typecheck clean (main, preload, renderer)
  • ESLint + Prettier clean
  • Manual test: create notebooks on two devices, sync, verify tree integrity

🤖 Generated with Claude Code

tomymaritano and others added 10 commits March 11, 2026 01:41
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notebooks now sync before notes in syncNow() to ensure note-notebook
dependencies are satisfied. Adds pullNotebooks/pushNotebooks methods
and applyRemoteNotebookChange for bidirectional notebook sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move validateNotebookTree from inline test definition to a shared module
so it can be reused by the API route and other consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 405559595b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +161 to +165
break;
}
}

this.state.notebookCursor = result.cursor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve notebook cursor on partial apply failures

If applyRemoteNotebookChange throws for any item, the loop breaks, but the code still advances notebookCursor to the server cursor. That acknowledges unapplied changes and prevents them from being retried, which can permanently desync notebooks after a single malformed or out-of-order change; this should track the last successfully applied version like pull() does for notes.

Useful? React with 👍 / 👎.

Comment on lines +214 to +216
notebookId: notebook.id,
operation: 'update' as const,
data: JSON.stringify({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit notebook delete operations in push payloads

The push payload forces every pending notebook change to operation: 'update', so local notebook removals are never sent as delete events. Because notebook deletes are hard deletes locally, removed rows cannot appear in getPendingChanges, so other devices keep notebooks that were deleted on this device.

Useful? React with 👍 / 👎.

Comment thread packages/api/src/routes/sync.ts Outdated
Comment on lines +375 to +377
if (!latestByNotebook.has(entry.notebookId) && entry.operation !== 'delete' && entry.data) {
const parsed = JSON.parse(entry.data);
latestByNotebook.set(entry.notebookId, { parentId: parsed.parentId, depth: parsed.depth });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude deleted notebooks from tree validation snapshot

This reconstruction iterates log entries newest-first, but when the latest entry is delete it does not mark that notebook ID as handled. An older non-delete entry for the same notebook is then added back, so tree validation can incorrectly treat previously deleted parents as existing and accept invalid parent references.

Useful? React with 👍 / 👎.

Comment on lines +326 to +329
const changes = await db
.select()
.from(notebookSyncLog)
.where(and(eq(notebookSyncLog.userId, userId), gt(notebookSyncLog.version, cursor)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add migration for notebook_sync_log before using it

These new routes query notebookSyncLog, but the commit does not add a corresponding SQL migration in packages/api/drizzle to create notebook_sync_log. In environments that provision schema via pnpm db:migrate, /sync/notebooks will fail at runtime with a missing-table error.

Useful? React with 👍 / 👎.

- Fix pullNotebooks() to only advance cursor to last successfully
  applied change (prevents skipping failed changes on retry)
- Fix tree validation snapshot to properly exclude deleted notebooks
  (prevents ghost parent references in validation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tomymaritano

Copy link
Copy Markdown
Collaborator Author

Codex Review Responses

P1: Cursor advancement on partial failure — Fixed in 043148c. pullNotebooks() now tracks lastAppliedCursor and only advances to server cursor when all changes succeed, matching the note pull() pattern.

P1: Notebook delete operations — Valid observation. Notebook deletes are hard deletes in SQLite, so deleted rows cannot appear in getPendingChanges(). This is a known architectural limitation shared with note sync. Proper fix requires a sync queue with soft-delete entries — tracked for a future PR.

P2: Deleted notebooks in tree validation — Fixed in 043148c. Tree validation snapshot now uses a processedIds Set. When the latest entry is a delete, it is marked as processed but not added to the tree, preventing older entries from resurrecting deleted notebooks.

P1: Missing migration — Not applicable. The API uses Drizzle with Turso and schema is managed via drizzle-kit push, not file-based migrations. The notebookSyncLog table is defined in packages/api/src/db/schema.ts and provisioned when pnpm db:push runs.

@tomymaritano tomymaritano merged commit e8250b8 into develop Mar 11, 2026
3 checks passed
@tomymaritano tomymaritano deleted the feature/notebook-sync branch March 11, 2026 13:10
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