You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@overeng/notion-md sync is bidirectional and remote-first. On the V2 engine, syncPage (src/sync.ts:968-1019) chooses direction at runtime from drift, and pullPage (src/sync.ts:543) unconditionally overwrites the local .nmd body + frontmatter from Notion; syncWorkspace (src/workspace.ts:408-520) walks the remote tree and reconciles each page two-way. There is no way to declare that a file (or tree) is generated locally and that Notion is a push-only render target whose edits are non-authoritative.
A downstream consumer generates a local .nmd tree (strict V2 JSON frontmatter, bound page_ids) and wants the repo to be the source of truth with Notion as a one-way render target. Today that requires a bespoke driver that force-pushes and then restores the local body to undo the lossy reconcile-pull (the blockquote-adjacent merge corruption; see also #744 for inline <page> corruption). We want to declare direction natively and drop the workaround.
The in-flight subtree prototype (#745) is already de-facto push-only — it only ever writes to Notion and uses a last-pushed body-hash as its noop oracle — but direction is implicit (a property of which command you ran), not selectable per file, and it runs on a separate, mutually-unreadable frontmatter format (lite title: / notion_page_id: YAML) from the V2 engine (strict notion_md JSON envelope).
Proposed config surface
New optional field on the V2 frontmatter (@overeng/notion-effect-client/src/nmd.ts:447, NmdFrontmatterV2.notion_md): sync_direction: 'push_only' | 'pull_only' | 'bidirectional' (absent ⇒ bidirectional, back-compat).
Optional default_sync_direction on the workspace manifest so a whole generated tree declares it once. Resolution: frontmatter.sync_direction ?? manifest.default_sync_direction ?? 'bidirectional'.
A one-shot --direction <mode> CLI override (not the durable source of truth) + --write-direction to stamp frontmatter.
push_only — local authoritative. Push local changes; never pull; remote-only edits reported in status, never reconciled. Retains the base snapshot (cheap; needed to re-establish a merge ancestor if direction is ever flipped back) — it simply skips the pull/merge/conflict branch. It is a guarded force: it still refuses when unresolved unknown/unsupported blocks are present unless an explicit destructive flag is set, and when it does drop them it reports what it bypassed (do not silently inherit a hardcoded allowDeletingContent: true).
pull_only — remote authoritative, local is a read-only mirror. Pull on remote change; refuse push (local edits surfaced, never sent — do not inherit pullPage's silent local clobber by default).
push_only is conflict-free by construction and structurally sidesteps the reconcile-pull round-trip corruption (nothing is ever pulled back into local).
Invariant: any frontmatter-rewrite path (page_id writeback, regeneration) MUST preserve sync_direction.
Out of scope (separate follow-up): converging the subtree engine's create-from-local / child-anchor / cross-ref writer onto the V2 frontmatter + sidecar. The two .nmd formats are currently mutually unreadable, so a shared sync_direction field across both is not free; that convergence should not block this V2-only increment and must not regress feat(notion-md): native directory-tree ↔ Notion-subtree sync (prototype) #745.
Minimal first increment
Add sync_direction to NmdFrontmatterV2 (default bidirectional) + implement push_only and pull_only in the V2syncPage (branch before the drift logic) and syncWorkspace (per-page guard before syncPage). This alone unblocks the generated-doc use case. Subtree convergence is a separate effort.
Acceptance criteria
sync_direction validated on NmdFrontmatterV2; absent ⇒ bidirectional; preserved across frontmatter rewrites (page_id writeback, regeneration).
push_only never invokes pull/merge (unit-asserted against a fake gateway); remote drift reported in status, not reconciled. syncWorkspace: a push_only doc edited on remote is not pulled (direct test).
push_only retains the base snapshot; flip bidirectional→push_only→bidirectional re-establishes the base via a pull before the next guarded push (no stale-base error).
push_only over a page with unresolved unknown / unsupported blocks refuses unless an explicit destructive flag is set, and reports exactly what it bypasses (no hardcoded allowDeletingContent).
pull_only never invokes a write (updateMarkdown / property / metadata); a local edit under pull_only is surfaced, not silently clobbered.
Problem
@overeng/notion-mdsync is bidirectional and remote-first. On the V2 engine,syncPage(src/sync.ts:968-1019) chooses direction at runtime from drift, andpullPage(src/sync.ts:543) unconditionally overwrites the local.nmdbody + frontmatter from Notion;syncWorkspace(src/workspace.ts:408-520) walks the remote tree and reconciles each page two-way. There is no way to declare that a file (or tree) is generated locally and that Notion is a push-only render target whose edits are non-authoritative.A downstream consumer generates a local
.nmdtree (strict V2 JSON frontmatter, boundpage_ids) and wants the repo to be the source of truth with Notion as a one-way render target. Today that requires a bespoke driver that force-pushes and then restores the local body to undo the lossy reconcile-pull (the blockquote-adjacent merge corruption; see also #744 for inline<page>corruption). We want to declare direction natively and drop the workaround.The in-flight subtree prototype (#745) is already de-facto push-only — it only ever writes to Notion and uses a last-pushed body-hash as its noop oracle — but direction is implicit (a property of which command you ran), not selectable per file, and it runs on a separate, mutually-unreadable frontmatter format (lite
title:/notion_page_id:YAML) from the V2 engine (strictnotion_mdJSON envelope).Proposed config surface
@overeng/notion-effect-client/src/nmd.ts:447,NmdFrontmatterV2.notion_md):sync_direction: 'push_only' | 'pull_only' | 'bidirectional'(absent ⇒bidirectional, back-compat).default_sync_directionon the workspace manifest so a whole generated tree declares it once. Resolution:frontmatter.sync_direction ?? manifest.default_sync_direction ?? 'bidirectional'.--direction <mode>CLI override (not the durable source of truth) +--write-directionto stamp frontmatter.Semantics
bidirectional— current behavior (drift-driven push/pull, base-snapshot 3-way merge, guarded-push conflict).push_only— local authoritative. Push local changes; never pull; remote-only edits reported instatus, never reconciled. Retains the base snapshot (cheap; needed to re-establish a merge ancestor if direction is ever flipped back) — it simply skips the pull/merge/conflict branch. It is a guarded force: it still refuses when unresolved unknown/unsupported blocks are present unless an explicit destructive flag is set, and when it does drop them it reports what it bypassed (do not silently inherit a hardcodedallowDeletingContent: true).pull_only— remote authoritative, local is a read-only mirror. Pull on remote change; refuse push (local edits surfaced, never sent — do not inheritpullPage's silent local clobber by default).push_only is conflict-free by construction and structurally sidesteps the reconcile-pull round-trip corruption (nothing is ever pulled back into local).
Explicit non-goals / caveats
page_id(nmd.ts:452); a never-pushed / unbound file cannot be loaded under push_only. Create-from-local remains the subtree prototype's job (feat(notion-md): native directory-tree ↔ Notion-subtree sync (prototype) #745).sync_direction..nmdformats are currently mutually unreadable, so a sharedsync_directionfield across both is not free; that convergence should not block this V2-only increment and must not regress feat(notion-md): native directory-tree ↔ Notion-subtree sync (prototype) #745.Minimal first increment
Add
sync_directiontoNmdFrontmatterV2(defaultbidirectional) + implementpush_onlyandpull_onlyin the V2syncPage(branch before the drift logic) andsyncWorkspace(per-page guard beforesyncPage). This alone unblocks the generated-doc use case. Subtree convergence is a separate effort.Acceptance criteria
sync_directionvalidated onNmdFrontmatterV2; absent ⇒bidirectional; preserved across frontmatter rewrites (page_id writeback, regeneration).push_onlynever invokes pull/merge (unit-asserted against a fake gateway); remote drift reported instatus, not reconciled.syncWorkspace: a push_only doc edited on remote is not pulled (direct test).push_onlyretains the base snapshot; flip bidirectional→push_only→bidirectional re-establishes the base via a pull before the next guarded push (no stale-base error).push_onlyover a page with unresolved unknown / unsupported blocks refuses unless an explicit destructive flag is set, and reports exactly what it bypasses (no hardcodedallowDeletingContent).pull_onlynever invokes a write (updateMarkdown/ property / metadata); a local edit under pull_only is surfaced, not silently clobbered.default_sync_direction+ frontmatter override resolution.statusreports per-pageeffectiveDirection..nmdtree round-trips with NO local-body restore workaround.Refs
<page>corruption — orthogonal round-trip bug)Posted on behalf of @schickling
agent_nameagent_session_idagent_toolagent_tool_versionagent_runtimeagent_modelworktreemachinetooling_profile