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:
- 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
- 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.
Summary
A mount running in
sync=write-onlycallsmarkBootstrapComplete()without ever pulling the remote tree, leavingBootstrapComplete=trueon a mount that was never mirrored. If that mount is later flipped tomirror, the restart fast-path trusts the (false)BootstrapCompletesignal, 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 periodicFullPullEveryfull 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 inReconcile::2640— the restart fast-path's own documented invariant, which the above violates::2648— the fast-path that then short-circuits on the false signal:So a mount that was ever write-only and is later switched to
mirror(samelocalDir/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>/messagesmounts werewrite-only(writeback command roots); flipping them tomirrorto restore read-down would not backfill the already-missed messages because of this trap. Worked around operationally for that fix (pear#170) byclearState-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:
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.) ORBootstrapComplete=falseon a detectedwrite-only → mirrorsyncMode transition, so the first mirror reconcile forces a real bootstrap.Option 1 is cleaner (keeps
BootstrapCompletemeaning exactly what:2640says).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.