{"record":{"id":"0dfe051cfa539344","repo":"gitbutlerapp/gitbutler","slug":"push-failed-after-max-retries-attempts","errorCode":null,"errorMessage":"push failed after {MAX_RETRIES} attempts","messagePattern":"push failed after (.+?) attempts","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-agentlog/src/gitmeta/write.rs","lineNumber":301,"sourceCode":"        Err(git_meta_lib::Error::GitCommand(message))\n            if message.contains(\"couldn't find remote ref\") => {}\n        Err(err) => return Err(err).context(\"failed to pull GitMeta metadata\"),\n    }\n\n    let mut attempts = 0;\n    loop {\n        attempts += 1;\n        let output = gitmeta\n            .push_once(None)\n            .context(\"failed to push GitMeta metadata\")?;\n        if output.success {\n            return Ok(());\n        }\n        if !output.non_fast_forward {\n            bail!(\"push failed\");\n        }\n        if attempts >= MAX_RETRIES {\n            bail!(\"push failed after {MAX_RETRIES} attempts\");\n        }\n        gitmeta\n            .resolve_push_conflict(None)\n            .context(\"failed to resolve GitMeta push conflict\")?;\n    }\n}\n\nfn stored_text(kind: RecordKind, text: Option<&str>) -> Option<String> {\n    let text = text?;\n    Some(match kind {\n        RecordKind::ToolResult => redact_text(cap_tool_result_text(text).as_ref()),\n        _ => redact_text(text),\n    })\n}\n\nfn file_path_hashes_for_record(\n    repo_root: &Path,\n    tool_kind: Option<ToolKind>,","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-agentlog/src/gitmeta/write.rs#L283-L319","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: concurrent publishers race until exhaustion\npublish_session(&repo, &session)?;\n\n// after: backoff-and-retry at the caller level\nlet mut delay = Duration::from_secs(5);\nlet result = loop {\n    match publish_session(&repo, &session) {\n        Ok(()) => break Ok(()),\n        Err(e) if e.to_string().contains(\"after 5 attempts\") && delay < Duration::from_secs(60) => {\n            std::thread::sleep(delay);\n            delay *= 2;\n        }\n        Err(e) => break Err(e),\n    }\n};","handlingStrategy":"fallback","validationCode":"// Detect sustained contention cheaply before publishing in shared setups\nlet remote_head = gitmeta.remote_head_oid(None)?; // diverged head predicts conflict storms\nif locally_diverged_beyond(&remote_head, 3) {\n    eprintln!(\"metadata ref heavily diverged; run `but agentlog sync` first\");\n}","typeGuard":"fn conflicts_exhausted(err: &anyhow::Error) -> bool {\n    err.to_string().contains(\"push failed after 5 attempts\")\n}","tryCatchPattern":"match publish(&repo, &session) {\n    Ok(()) => {}\n    Err(err) if err.to_string().contains(\"after 5 attempts\") => {\n        // proceed without publishing now; re-publish later — dedup makes it safe\n        queue_for_retry_later(session);\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["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"],"tags":["gitmeta","push","non-fast-forward","concurrency","retry-exhausted","rust"],"backgroundTag":"git-push-conflict","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}