gastownhall/beads · warning · versioncontrolops.ErrPullBehindFastForwardable
%w: %w
Error message
%w: %w
What it means
This is the benign, retryable classification of the merged-nothing error: the local branch is a strict ancestor of the refreshed remote-tracking ref, so the remote simply moved ahead after the fetch. It wraps the detailed mergedNothing error with the sentinel versioncontrolops.ErrPullBehindFastForwardable, which bd sync treats as a push-race and retries rather than failing the tick.
Source
Thrown at internal/storage/dolt/store.go:4194
// retryable class, because its common ancestor is by definition neither tip.
// Distinguishing the benign race from a genuinely failed transport would
// still need the remote tip as of the transport's own fetch, which git-backed
// remotes never write into this database; the merge base is the best post-hoc
// split available. Classified before the display fallback below rewrites an
// empty localHash.
behindFastForwardable := localHash != "" && mergeBase.String == localHash
if localHash == "" {
localHash = "unknown"
}
mergedNothing := fmt.Errorf("pull from %s/%s reported success but merged nothing into %s: %s is at %s while %s is at %s "+
"(their common ancestor is %s), so the commits on the remote-tracking ref are not on the branch this "+
"database reads. Most often another client pushed after this pull fetched and a re-run will merge it; "+
"if the divergence survives repeated re-runs, the transport is not landing merges on this branch "+
"(for example the dolt CLI directory and the sql-server are serving different databases or branches)",
remote, s.branch, s.branch, s.branch, localHash, trackingRef, remoteHash, mergeBase.String)
if behindFastForwardable {
return fmt.Errorf("%w: %w", versioncontrolops.ErrPullBehindFastForwardable, mergedNothing)
}
return mergedNothing
}
// refreshTrackingRef fetches remote/s.branch into this database's
// remote-tracking refs over a dedicated long-timeout, credential-aware
// connection, so verifyPullLanded's comparison reads a tracking ref this
// database just wrote. It mirrors pullTransport's own network calls: a
// long-timeout connection (openLongTimeoutConn) wrapped in the remote's
// credential/S3 environment (withRemoteOperationEnv), which is what lets the
// refresh reach CLI-routed (git-protocol, credential, cloud-auth) remotes that
// the default s.db pool — short read timeout, no CLI credentials — cannot.
//
// Only the FETCH runs here, and DOLT_FETCH is branch-global: it advances
// remote-tracking refs and never touches the working branch, so the fresh
// connection's default-branch checkout — the be-b0am hazard that makes a merge
// on such a connection unsafe — does not apply. The containment reads in
// verifyPullLanded stay on s.db, where the short pool timeout is right: they areView on GitHub (pinned to 71377f2769)
Solutions
- Do nothing — bd sync's retry loop handles this sentinel automatically.
- If surfacing it yourself, check errors.Is(err, versioncontrolops.ErrPullBehindFastForwardable) and retry the pull after a short delay.
- Reduce sync concurrency or stagger sync schedules across clients if races are frequent.
- If retries keep hitting it, verify the remote actually accepts pushes (the peer may be pushing faster than you can pull).
Example fix
// before
if err := store.Pull(ctx); err != nil { return err }
// after
if err := store.Pull(ctx); err != nil {
if errors.Is(err, versioncontrolops.ErrPullBehindFastForwardable) {
time.Sleep(2 * time.Second)
return store.Pull(ctx) // benign race: re-pull fast-forwards
}
return err
} Defensive patterns
Strategy: retry
Type guard
func isPullBehindFastForwardable(err error) bool {
return errors.Is(err, versioncontrolops.ErrPullBehindFastForwardable)
} Try / catch
if err := store.Pull(ctx); err != nil {
if errors.Is(err, versioncontrolops.ErrPullBehindFastForwardable) {
time.Sleep(time.Second)
return store.Pull(ctx) // benign race, self-correcting
}
return err
} Prevention
- Stagger bd sync schedules across clients sharing a remote.
- Treat this sentinel as retryable, never as a hard failure.
- Bound retries to avoid spinning against a very chatty peer.
- Log the tracking-ref hashes to confirm progress over retries.
When it happens
Trigger: verifyPullLanded finds localHash != "", merge base == localHash, and remoteHash ahead — i.e., a peer client pushed new commits between this pull's fetch and its verification. Only reachable via Pull()/PullRemote() while another writer pushes to the same remote branch.
Common situations: Multiple machines/agents syncing the same beads database concurrently; CI running bd sync while a developer works; scheduled federation peers pulling in overlapping windows.
Related errors
- %w for %s
- failed to commit pending changes before pull: %w
- failed to pull from peer %s: %w
- failed to stage %s before commit: %w
- inspect dirty config before pull: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/82997fb53b01fcae.
Report an issue: GitHub.