Skip to content

bug(git-sync): the auto-sync heartbeat pushes without fetch or rebase — on a shared branch it fails non-fast-forward forever after the first foreign push #3011

Description

@vybe

Summary

_run_auto_sync_once is git add -A → git status --porcelain → git commit → git push origin HEAD. No fetch, no rebase, no lease, no branch check. The moment anyone else pushes to the branch the agent is on, every subsequent heartbeat fails non-fast-forward, the agent's own commits pile up locally, and nothing on the agent side ever reconciles. The path only works cleanly in working-branch mode on a branch nobody else writes — which is exactly the mode the create paths do not expose (see the working-branch mode reachable issue on the private tracker).

Scope: Trinity 1.0 · project:tandem (operator ruling 2026-09-24: the repository is the agent; the container is a cache of it). Precondition 2 of 3 for defaulting auto-sync on (with the toggle fix and #2107).

Mechanism (verified at origin/dev cec2a64b, 2026-09-24)

  • docker/base-image/agent_server/routers/git.py:747–800 — the cycle. The docstring is explicit: "Intentionally minimal — heavy conflict handling stays in the operator-initiated sync_to_github endpoint. Auto-sync is a heartbeat, not a rescue." That was the right call when auto-sync was opt-in on a branch the agent owned. It is not a heartbeat any more once it is the durability mechanism for agents whose humans also push.
  • The failure is not silent — a failed push writes sync-state.json (git.py:102–160, Sync health observability (S1) #389 S1a), the backend poller (services/sync_health_service.py, 60 s) increments consecutive_failures, and a sync_failing operator-queue item fires at three (ALERT_THRESHOLD = 3). But the alert is the only outcome: the divergence itself is never repaired, and the local commits stay on the container disk.
  • The rescue path exists only on the operator endpoint: sync_to_github with the pull-first strategy (fetch + rebase, conflict handling). The heartbeat never calls it.
  • add -A stages every untracked file the fleet-wide .gitignore does not exclude (services/git_service/gitignore.py), so a rebase-on-reject retry must also be safe against a commit that just swept in something the agent should not have committed — the staging policy stays the agent's .gitignore (root cause refactor: agent git repo root is $HOME — 22 gitignore patterns exist only to compensate, and every new home-writing feature repeats the dance #1703).

Seam history (closed): #389 (heartbeat), #2742 (sync-state atomic writes), #1595/#1596 (worker thread, repack), #1808 (freeze enforced on three failures).

Acceptance Criteria

  • Before pushing, the heartbeat runs git fetch origin <branch> and, if behind > 0, rebases the local commits onto origin/<branch> (git rebase --autostash); a clean rebase then pushes.
  • On rebase conflict: abort the rebase, leave the working tree exactly as it was, record last_error_summary = "diverged: rebase conflict on <branch>" in sync-state.json, and let the existing sync_failing path raise the operator-queue item with that reason. Never resolve a conflict automatically, never overwrite the remote, never reset.
  • Shared-branch refusal: if the branch the agent is on is the repo's default branch and the agent is in source mode (pull-only by contract), the heartbeat refuses to push and records refused: source-mode on <branch> — rather than pushing agent commits straight to main of a repo that deploys on push. Fork-to-own agents (they own their fork's main) are not refused.
  • After a successful rebase the push is lease-protected against the ref that was fetched (the lease form of push, never the bare forced form); a plain push otherwise.
  • sync-state.json gains last_successful_push_at and behind_after_fetch so the sync-health row can persist them (consumed by the divergence-age issue on the private tracker).
  • Tests: foreign commit on the branch → next cycle rebases and pushes; conflicting foreign commit → abort, state recorded, tree untouched; source-mode agent on main → refused, no push; the operator sync_to_github path unchanged.

Technical Notes

Journey Impact: existing — agent work reaches the repo on the heartbeat (docs/memory/feature-flows/github-sync.md); the flow now states what happens when the branch moved underneath.

Siblings — the git-sync set, filed 2026-09-24 (operator rulings relayed by corbin)

Platform half of the repository is the agent; the container is a cache of it (Trinity 1.0 · project:tandem · epic abilityai/trinity-enterprise#497). Fleet-convention half: corbin's canon/protocols/agent-git-sync.md.

Re-prioritised to P1 for 1.0 the same day: #2105 · #2107. Linked, unchanged: #1703 · #2938 · abilityai/trinity-enterprise#142 · abilityai/trinity-enterprise#230.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions