Skip to content

write-only mounts set BootstrapComplete=true without mirroring → write-only→mirror flip silently skips backfill #262

Description

@khaliqgant

Summary

A mount running in sync=write-only calls markBootstrapComplete() without ever pulling the remote tree, leaving BootstrapComplete=true on a mount that was never mirrored. If that mount is later flipped to mirror, the restart fast-path trusts the (false) BootstrapComplete signal, seeds the events cursor to the feed tip, and skips the bootstrap full pull — so pre-existing remote content (history) never backfills; only post-restart events read down. Backfill then waits for the periodic FullPullEvery full pull (~100 min at the websocket-gated cadence) or never lands promptly.

Root cause (file:line, v0.8.19 internal/mountsync/syncer.go)

  • :2117 — the write-only branch in Reconcile:
    if s.writeOnly {
        if !s.state.BootstrapComplete {
            s.markBootstrapComplete()   // <-- sets BootstrapComplete=true with NO pull
        }
    }
  • :2640 — the restart fast-path's own documented invariant, which the above violates:

    "BootstrapComplete is set only by a full-tree/export pull that mirrored the whole remote, so it is the authoritative signal."

  • :2648 — the fast-path that then short-circuits on the false signal:
    if s.state.BootstrapComplete && !s.forceFullReconcile && len(s.state.Files) > 0 {
        cursor, err := s.resolveLatestEventCursor(ctx)   // seeds cursor to feed TIP
        ...
        s.logf("restart fast-path: ... skipping bootstrap full pull", ...)
        return nil   // <-- no full pull → no backfill
    }

So a mount that was ever write-only and is later switched to mirror (same localDir/state) inherits a "whole remote already mirrored" claim that was never true, and never backfills the history it never pulled.

Impact / how it surfaced

Discovered during the Pear Slack DM/top-level read-down investigation (2026-06-08). The /slack/users/<U>/messages + /slack/channels/<id>__<slug>/messages mounts were write-only (writeback command roots); flipping them to mirror to restore read-down would not backfill the already-missed messages because of this trap. Worked around operationally for that fix (pear#170) by clearState-ing the flipped mounts at restart so they bootstrap fresh — but that's a manual step that shouldn't be necessary, and it carries a writeback re-dispatch risk if outbound command files linger (mitigated there by verifying the dirs were clean).

Proposed fix (daemon-side)

Either:

  1. Don't call markBootstrapComplete() in the write-only branch (:2117) — a write-only mount has not mirrored anything, so the flag should stay false; a later mirror flip then correctly runs the bootstrap full pull. (Verify nothing else relies on write-only reporting BootstrapComplete=true — e.g. status/health surfaces; if so, gate those on a separate "writeOnly && ready" notion rather than BootstrapComplete.) OR
  2. Reset BootstrapComplete=false on a detected write-only → mirror syncMode transition, so the first mirror reconcile forces a real bootstrap.

Option 1 is cleaner (keeps BootstrapComplete meaning exactly what :2640 says).

Severity

Latent — only bites a write-only→mirror transition (or any consumer that flips syncMode on an existing state dir). Not blocking pear#170 (handled operationally via clearState). Filing so it's owned for the next daemon bump per slack-comms.

Read-only investigation finding; no code change attached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions