gitbutlerapp/gitbutler · error
push failed after {MAX_RETRIES} attempts
Error message
push failed after {MAX_RETRIES} attempts What it means
The publish push loop retries non-fast-forward conflicts up to MAX_RETRIES (5), calling `resolve_push_conflict` between attempts. This bail means every attempt still hit a non-fast-forward rejection — the remote kept moving faster than conflicts could be resolved. It indicates continuous concurrent writing to the GitMeta remote, or conflict resolution that never converges.
Source
Thrown at crates/but-agentlog/src/gitmeta/write.rs:301
Err(git_meta_lib::Error::GitCommand(message))
if message.contains("couldn't find remote ref") => {}
Err(err) => return Err(err).context("failed to pull GitMeta metadata"),
}
let mut attempts = 0;
loop {
attempts += 1;
let output = gitmeta
.push_once(None)
.context("failed to push GitMeta metadata")?;
if output.success {
return Ok(());
}
if !output.non_fast_forward {
bail!("push failed");
}
if attempts >= MAX_RETRIES {
bail!("push failed after {MAX_RETRIES} attempts");
}
gitmeta
.resolve_push_conflict(None)
.context("failed to resolve GitMeta push conflict")?;
}
}
fn stored_text(kind: RecordKind, text: Option<&str>) -> Option<String> {
let text = text?;
Some(match kind {
RecordKind::ToolResult => redact_text(cap_tool_result_text(text).as_ref()),
_ => redact_text(text),
})
}
fn file_path_hashes_for_record(
repo_root: &Path,
tool_kind: Option<ToolKind>,View on GitHub (pinned to caf1f223d3)
Solutions
- Wait briefly and re-run `but agentlog sync` then the publish — record dedup makes the retry idempotent and the contention usually clears
- Stagger publishes (run one machine's agentlog at a time, or serialize capture/publish in CI) so writers stop racing
- If it never converges, inspect the GitMeta ref divergence (`git log` on the metadata remote ref) — a one-time manual reset of the local metadata ref to the remote head, then re-publish, breaks the cycle
- Report non-converging resolve_push_conflict behavior upstream with the metadata ref graphs if a manual reset also fails
Example fix
// before: concurrent publishers race until exhaustion
publish_session(&repo, &session)?;
// after: backoff-and-retry at the caller level
let mut delay = Duration::from_secs(5);
let result = loop {
match publish_session(&repo, &session) {
Ok(()) => break Ok(()),
Err(e) if e.to_string().contains("after 5 attempts") && delay < Duration::from_secs(60) => {
std::thread::sleep(delay);
delay *= 2;
}
Err(e) => break Err(e),
}
}; Defensive patterns
Strategy: fallback
Validate before calling
// Detect sustained contention cheaply before publishing in shared setups
let remote_head = gitmeta.remote_head_oid(None)?; // diverged head predicts conflict storms
if locally_diverged_beyond(&remote_head, 3) {
eprintln!("metadata ref heavily diverged; run `but agentlog sync` first");
} Type guard
fn conflicts_exhausted(err: &anyhow::Error) -> bool {
err.to_string().contains("push failed after 5 attempts")
} Try / catch
match publish(&repo, &session) {
Ok(()) => {}
Err(err) if err.to_string().contains("after 5 attempts") => {
// proceed without publishing now; re-publish later — dedup makes it safe
queue_for_retry_later(session);
}
Err(err) => return Err(err),
} Prevention
- Serialize publishes per metadata remote (one machine or a CI lock) when many agents run in parallel
- Sync before publish to minimize divergence
- If contention is chronic, stagger capture hooks so publishers rarely overlap
When it happens
Trigger: Multiple agents/machines running `but agentlog publish`/`sync` against the same GitMeta remote simultaneously, so each resolve-push-conflict cycle finds yet another newer remote state; or conflict resolution that cannot produce a fast-forwardable state (diverged histories).
Common situations: Several parallel agent sessions finishing and publishing at once in CI or a shared repo; two workstations with heavy agentlog activity syncing the same metadata remote; a long-lived divergence in the metadata ref that auto-resolution cannot merge.
Related errors
- push failed
- existing GitMeta key '{updated_at_key}' is not a string
- existing GitMeta key '{index_key}' is not a set
- existing GitMeta key '{associated_targets_key}' is not a str
- existing GitMeta key '{turns_key}' is not a list
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/0dfe051cfa539344.
Report an issue: GitHub.