🤖 fix: stop background status fetch from converting repos into partial clones - #3965
🤖 fix: stop background status fetch from converting repos into partial clones#3965ibetitsmike wants to merge 5 commits into
Conversation
…clones GIT_FETCH_SCRIPT fetched with --filter=blob:none, which registers remote.origin.promisor + partialclonefilter on first use and leaves every subsequently fetched commit without blobs. git worktree add (workspace creation) then lazy-fetches blobs mid-checkout and any transient network drop aborts with 'could not fetch <oid> from promisor remote'. - GIT_FETCH_SCRIPT: --filter=blob:none -> --no-filter (also overrides the persisted filter in already-converted repos so they heal going forward) - SSH warm path: strip promisor keys in the fused preamble (parity with ensureBaseRepo hygiene) - classify promisor lazy-fetch failures as repairable missing-object checkout failures (warm-path case + isMissingObjectCheckoutFailure) - extend warm-heal integration test to assert promisor keys are stripped
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6210c403b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…timeout Codex P1: --no-filter alone neither removes persisted promisor config nor backfills blobs omitted by earlier filtered fetches, and up-to-date repos early-exit before fetching. GIT_FETCH_SCRIPT now heals poisoned repos (partialclonefilter=blob:none) before the early exit: git fetch --refetch backfills, then promisor keys are unset only after success. mkdir lock with 1h staleness prevents concurrent sibling refetches and throttles retries. Codex P2: raise GitStatusStore background fetch timeout 30s -> 300s for the larger unfiltered transfers and the one-time refetch. Share PROMISOR_CONFIG_KEYS between the script and SSHRuntime; add behavioral heal test covering the early-exit path.
|
Addressed both review findings in 71eeb39: P1 (persisted promisor state / no backfill / early exit): Correct — P2 (30s timeout too small for unfiltered transfers): Raised the background fetch timeout in @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71eeb39293
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…leness Codex P1: --refetch only re-sends objects reachable from the remote's current refs; blobs referenced solely by local refs into upstream-deleted branches can remain missing. Verify completeness via rev-list --missing=print --all before unsetting promisor config; when incomplete, keep the config (lazy-fetch fallback preserved) and throttle refetch retries to daily via xum.promisorHealIncompleteAt. Codex P2: run the heal before ls-remote/primary-branch gating so a stale origin/HEAD (renamed/deleted upstream default branch) cannot skip it forever. Codex P3: replace find -maxdepth/-mmin staleness probing with a timestamp file inside the mkdir lock; failed refetches keep the lock so retries wait out the 1h window. Add behavioral test for the incomplete-heal path.
|
Addressed all three round-2 findings in d452138: P1 (refetch completeness): Correct — P2 (heal unreachable behind ls-remote gating): The heal now runs at the top of the script, before primary-branch detection and the P3 (find portability): Dropped @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d452138228
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: d452138228
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
Codex P2a: git 2.31-2.35 (e.g. Ubuntu 22.04) lacks --refetch, so healing would fail forever there. The heal now backfills in two stages: (1) batch fetch of exactly the missing OIDs via git fetch --stdin (git >= 2.30, only downloads the gaps, same protocol-v2 OID-want capability lazy fetch uses), then (2) feature-detected --refetch fallback for servers that refuse OID wants. Codex P2b: completeness enumeration now includes --reflog so commits displaced by upstream force-pushes (reachable only via the remote-tracking reflog) keep the lazy-fetch fallback instead of being stranded by an unsafe config unset. Retry semantics: incomplete-but-reachable -> daily marker; all fetches failed (offline/auth) -> lock kept, 1h staleness window paces retries. Tests: server-side gc added to the incomplete case (so stage 1 cannot resurrect orphaned blobs); new force-push/reflog behavioral test.
|
Addressed both round-3 findings in 6216de3: P2 (git < 2.36 support): The heal now backfills in two stages. Stage 1 enumerates missing objects and batch-fetches exactly those OIDs via P2 (reflog reachability): The completeness enumeration (used both to build the stage-1 OID list and to decide whether unsetting is safe) now includes @codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6216de39de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ore strip Codex P2: rev-list can exit 128 without output when a local ref names a missing commit; that must read as 'not proven complete', never 'nothing missing'. xum_missing_objects now captures rev-list status and prints an enumeration-failed sentinel; stage 1 skips it, stage 2 still runs (a refetch can restore a missing commit), and the unset never fires on it. Codex P2: the SSH warm path stripped promisor keys unconditionally, which could strand a poisoned base repo without its lazy-fetch fallback and let worktree add silently fall back to the stale bundle ref. It now mirrors the fetch script's heal: gated on partialclonefilter=blob:none, batch-fetches exactly the missing OIDs (after originPreamble sets the URL), and strips only when enumeration proves the object store complete. Add enumeration-failure behavioral test.
|
Addressed round-4 findings in 79b6abd: P2 (enumeration failure treated as complete): Fixed. P2 (SSH warm path strips before backfilling): Fixed. The warm-path strip now mirrors the fetch script's heal: gated on Security P2 (bound unfiltered background fetches): Responding with rationale rather than a code change, as the suggested quota would reintroduce the bug class this PR fixes:
Happy to revisit if a concrete bounded strategy exists that doesn't resurrect the partial-clone poisoning. @codex review |
This comment has been minimized.
This comment has been minimized.
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
The background git-status fetch (
GIT_FETCH_SCRIPT) fetched with--filter=blob:none, which permanently converts the repo into a promisor/partial clone. Every commit fetched by that loop then lacks blobs, so a latergit worktree add(workspace creation) must lazy-fetch blobs from upstream mid-checkout — and any transient network drop aborts creation withfatal: could not fetch <oid> from promisor remote. This PR switches the status fetch to--no-filter, adds an explicit one-time heal (backfill viagit fetch --refetch, then unset the promisor config) for already-poisoned repos, strips promisor config keys in the SSH warm path, and classifies promisor lazy-fetch failures as repairable missing-object checkout failures.Background
Root-cause chain:
--filter=blob:noneto the background status fetch for speed. Git registersremote.origin.promisor=true+remote.origin.partialclonefilter=blob:noneon the first filtered fetch, and the persisted filter then applies to every subsequent plaingit fetchin that repo.git worktree addchecks out one of those commits and lazy-fetches the missing blobs from the remote mid-checkout.could not fetch <oid> from promisor remote— a failure mode that did not route to the existing repair-from-local/bundle-ref fallback.#3290 already stripped promisor keys in
ensureBaseRepo(), but that never stuck: sibling worktrees running the status fetch re-poisoned the shared base repo immediately, and the warm (fused preamble) path never stripped the keys at all.Implementation
GIT_FETCH_SCRIPT(src/common/utils/git/gitStatus.ts):--filter=blob:none→--no-filterstops poisoning healthy repos, but it does not remove persisted promisor config or backfill blobs that earlier filtered fetches omitted. So the script now also runs an explicit one-time heal, placed before any ls-remote/primary-branch gating (a staleorigin/HEADafter an upstream default-branch rename would otherwise skip it forever): whenremote.origin.partialclonefilteris exactly theblob:nonethis script used to write, it backfills in two stages and unsets the promisor config keys only after verifying completeness. Stage 1 enumerates missing objects (git rev-list --objects --missing=print --all --reflog) and batch-fetches exactly those OIDs viagit fetch --stdin— downloads only the gaps, works on git ≥ 2.30 (e.g. Ubuntu 22.04's 2.34, which lacks--refetch), and uses the same protocol-v2 OID-want capability the repo's lazy fetch already depends on. Stage 2, feature-detected and only if gaps remain, runsgit fetch --refetch(git ≥ 2.36) for servers that refuse OID wants. The completeness check includes--reflogbecause an upstream force-push strands the displaced blobless commit in the remote-tracking reflog — unsetting the config would breakgit reset --hard origin/main@{1}recovery. If gaps remain after both stages: with a reachable server, the promisor config is kept (preserving the lazy-fetch fallback) and axum.promisorHealIncompleteAtmarker throttles retries to daily; if every fetch failed (offline/auth), themkdirlock is left in place so its 1h staleness window paces retries. The lock (timestamp file inside — portable, nofind -mmin/statmtime probing) also prevents sibling worktrees from concurrent backfills.src/browser/stores/GitStatusStore.ts): background fetch timeout raised 30s → 300s, since unfiltered transfers (and the one-time refetch) can be much larger than the old blob-filtered ones; killing a slow-but-progressing fetch wastes the transfer and leaves ahead/behind stale behind retry backoff.src/node/runtime/SSHRuntime.ts): the fused preamble now heals the shared base repo the same way — when it carries theblob:nonepartial-clone config, it batch-fetches exactly the missing OIDs (after the origin URL is configured) and unsetsBASE_REPO_PROMISOR_CONFIG_KEYSonly when enumeration proves the object store complete. Stripping unconditionally would remove the lazy-fetch fallback while blobs are still missing and letgit worktree addsilently fall back to the stale bundle ref despite origin being reachable.rev-list --missing=printcan exit 128 with no output when a local ref names a missing commit; both heal sites treat enumeration failure as "not proven complete" (sentinel value) so the promisor config is never unset on bad data.could not fetch … from promisor remoteis now matched byisMissingObjectCheckoutFailureand by the warm-pathwt_reason=missing-objectscase, so it routes to the existing repair-from-local/bundle-ref fallback instead of failing workspace creation.Validation
--filter=blob:nonefetch converts a plain clone into a promisor clone and that subsequent plain fetches stay blobless; confirmed--no-filteravoids conversion.gitStatus.fetch.test.ts): (1) poisons a real repo with a filtered fetch, then asserts the script heals it even on theLOCAL_SHA = REMOTE_SHAearly-exit path — promisor config removed, previously missing blobs backfilled, heal one-shot on a second run; (2) pins a blobless commit with a local branch, deletes its branch upstream + GCs the server, and asserts the heal keeps the promisor config, sets the daily-retry marker, and skips the backfill on the next run; (3) force-pushes over a bloblessly-fetched commit + GCs the server, and asserts the reflog-aware completeness check keeps the promisor config even though no plain ref reaches the displaced commit anymore; (4) points a local ref at a nonexistent object so enumeration itself fails, and asserts the heal refuses to unset the promisor config.tests/runtime/runtime.test.ts); Docker-only, runs in CI.Risks
Touches the background fetch and workspace-creation hot paths. Severity is low:
--no-filteris a strict superset fetch (more data, same semantics), and the promisor-key strip is best-effort (|| true). One-time costs per poisoned repo: the heal's stage-1 backfill downloads exactly the missing blobs; only when the server refuses OID wants does the stage-2--refetchdownload the full object set (roughly a clone-sized transfer, temporarily duplicating pack data on disk until git's auto-gc repacks). Known collateral: a repo the user intentionally created as a blobless (--filter=blob:none) clone is indistinguishable from one we poisoned and will be converted to a full clone once; other filters (e.g.tree:0) are left untouched. Repos on git 2.30–2.35 heal through the OID backfill stage; older than that, heal attempts fail and keep today's behavior. Repos with blobless objects that upstream no longer serves (deleted/force-pushed + GC'd) keep their promisor config permanently — also today's behavior — at the cost of one backfill attempt per day.Generated with
xum• Model:coder:anthropic-wif/claude-mythos-5• Thinking:xhigh• Cost:$249.39