can1357/oh-my-pi · warning · HeadDriftError
HEAD changed since preflight ({expected_head[:12]} → {head[:
Error message
HEAD changed since preflight ({expected_head[:12]} → {head[:12]}); aborting push. What it means
`push` re-checks HEAD with `rev_parse_head` immediately before pushing; if it differs from the `expected_head` captured during preflight, it raises `HeadDriftError` (a GitCommandError subclass with code 128) and aborts the push. This guards against pushing a commit someone else (or another step) added to the local branch after the preflight snapshot — preventing unintended or unsafe pushes.
Source
Thrown at python/robomp/src/git_ops.py:808
history rewrites (e.g. the agent doing `git commit --amend --reset-author
--no-edit` to fix author identity) while still refusing the push if origin
has moved since our last fetch — i.e. it never clobbers work the bot
didn't see.
When `expected_head` is supplied, this verifies the *local* HEAD matches
before pushing — anything else means an unexpected commit raced in inside
our own worktree between the orchestrator's preflight and this call, and
the push is aborted with `HeadDriftError`. This is a separate concern from
`--force-with-lease`, which compares against the remote ref.
"""
slot_kwargs = _slot_subprocess_kwargs(slot_uid)
git_safe_directory = safe_directory
if git_safe_directory is None and slot_kwargs:
git_safe_directory = repo_dir
head = rev_parse_head(repo_dir, safe_directory=git_safe_directory, **slot_kwargs)
if expected_head and head != expected_head:
raise HeadDriftError(
["git", "push"],
128,
"",
f"HEAD changed since preflight ({expected_head[:12]} → {head[:12]}); aborting push.",
)
# Probe the local remote-tracking ref. Missing → first push; we pin the
# lease to the empty value so the push only succeeds if origin still has
# no `<branch>`. Present → pin to that SHA.
probe = _run_git(
["rev-parse", "--verify", "--quiet", f"refs/remotes/origin/{branch}"],
cwd=repo_dir,
token=None,
safe_directory=git_safe_directory,
**slot_kwargs,
)
expected_remote = probe.stdout.strip() if probe.returncode == 0 else ""
if remote_url is None:
push_extra_env: dict[str, str] | None = NoneView on GitHub (pinned to 9690622007)
Solutions
- Re-run the push operation from the start so preflight captures the current HEAD
- Check `git log`/`git reflog` to see what moved HEAD — if the new commit is unwanted, reset it before retrying
- Ensure no concurrent processes (other tool runs, IDE auto-commit) operate on the repo during push
- If the drift is expected and safe, pass/refresh the expected_head to the current HEAD value
Example fix
// before expected = rev_parse_head(repo_dir) ...long work... push(repo_dir, expected_head=expected) # HeadDriftError // after expected = rev_parse_head(repo_dir) push(repo_dir, expected_head=expected) # preflight and push adjacent; retry whole flow on drift
Defensive patterns
Strategy: retry
Validate before calling
current = rev_parse_head(repo_dir)
if expected_head and current != expected_head:
# refresh preflight instead of pushing
expected_head = current Try / catch
from tenacity import retry, retry_if_exception_type, stop_after_attempt
@retry(retry=retry_if_exception_type(HeadDriftError), stop=stop_after_attempt(2))
def safe_push(repo_dir):
expected = rev_parse_head(repo_dir)
push(repo_dir, expected_head=expected) Prevention
- Minimize the window between preflight and push — capture expected_head as late as possible
- Don't run concurrent tooling on the same checkout; use per-run worktrees
- Check git reflog after drift to identify what moved HEAD before retrying
- Treat drift as a signal: re-run the full preflight rather than overriding expected_head blindly
When it happens
Trigger: Another process/IDE/hook commits or amends on the current branch between the preflight HEAD capture and the push call; an auto-formatter or pre-commit hook creating commits; concurrent runs of the tooling on the same repo.
Common situations: Long gap between preflight and push while the developer works in the repo, CI job sharing a checkout with another job, hooks that amend or create fixup commits automatically.
Related errors
- cli_message(command, exit_code, stdout, stderr)
- OAuth refresh ownership was lost before persistence
- No credential with id=${id}
- session header changed during stats cleanup: ${session.path}
- Daemon ${spec.name} is already starting
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0fc792ee17464f87.
Report an issue: GitHub.