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 are

View on GitHub (pinned to 71377f2769)

Solutions

  1. Do nothing — bd sync's retry loop handles this sentinel automatically.
  2. If surfacing it yourself, check errors.Is(err, versioncontrolops.ErrPullBehindFastForwardable) and retry the pull after a short delay.
  3. Reduce sync concurrency or stagger sync schedules across clients if races are frequent.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/82997fb53b01fcae. Report an issue: GitHub.