action: extend shallow-unreachable deepen-retry to checkout_pr fetches (#734)

extracts the deepen-retry helper from `GitFetchTool` into shared
`$gitFetchWithDeepen` and applies it to every fetch in `checkoutPrBranch`
(baseRef, pull/N/head, before_sha temp branch). on shallow clones with
deep PR ancestry — the failure mode behind ~10 of 51 `heuristic:very-slow`
runs in 24h on `remotion-dev/remotion` — the baseRef fetch was throwing
`Could not read <sha>` to the agent before the compare-api deepen block
could run. agents then burned 10+ minutes retrying `checkout_pr` and
falling back to ad-hoc shell `git fetch --deepen` workarounds.

also splits the analyzer's `heuristic:git-error-recovered` into
`heuristic:git-shallow-unreachable` and `heuristic:git-shallow-lock`
buckets so future audits surface this without manual log-grep.

closes #656.
This commit is contained in:
Colin McDonnell
2026-05-14 03:44:08 +00:00
committed by pullfrog[bot]
parent 2960d51493
commit 4ad649ebb9
3 changed files with 81 additions and 42 deletions
+2 -32
View File
@@ -2,7 +2,7 @@ import { regex } from "arkregex";
import { type } from "arktype";
import type { StoredPushDest } from "../toolState.ts";
import { log } from "../utils/cli.ts";
import { $git } from "../utils/gitAuth.ts";
import { $git, $gitFetchWithDeepen } from "../utils/gitAuth.ts";
import { executeLifecycleHook } from "../utils/lifecycle.ts";
import { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
@@ -510,21 +510,6 @@ const GitFetch = type({
depth: type.number.describe("Fetch depth (for shallow clones)").optional(),
});
// when an agent-supplied depth is too shallow to reach the merge base, git
// surfaces "Could not read <sha>" and "remote did not send all necessary
// objects". detect both wordings so a single deepen retry can recover before
// the error reaches the agent (issue #564). git emits the full OID via
// oid_to_hex, so the bound is 40 (SHA-1) or 64 (SHA-256).
const SHALLOW_UNREACHABLE_PATTERNS: RegExp[] = [
/Could not read [a-f0-9]{40,64}/,
/remote did not send all necessary objects/,
];
// large enough to clear the merge base on most real-world PRs without
// downloading the full history; matches the fallback used by checkoutPrBranch
// when the compare API is unavailable.
const DEEPEN_RETRY_DEPTH = 1000;
export function GitFetchTool(ctx: ToolContext) {
return tool({
name: "git_fetch",
@@ -538,22 +523,7 @@ export function GitFetchTool(ctx: ToolContext) {
if (params.depth !== undefined) {
fetchArgs.push(`--depth=${params.depth}`);
}
try {
await $git("fetch", fetchArgs, { token: ctx.gitToken });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
const isShallow =
isShallowUnreachable &&
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
if (!isShallow) throw err;
log.info(
`» git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
);
await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
token: ctx.gitToken,
});
}
await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
return { success: true, ref: params.ref };
}),
});