fix(agents): bound project memory git remote lookup (#117346)

* fix(agents): bound project memory git remote lookup

* test(agents): prove project memory git timeout

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
zengLingbiao
2026-08-02 18:17:48 +08:00
committed by GitHub
parent 0aa262a795
commit 9045484aea
2 changed files with 50 additions and 16 deletions
+33
View File
@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { resolveProjectKey } from "./project-memory-scope.js";
const execFileAsync = promisify(execFile);
@@ -94,4 +95,36 @@ describe("project memory scope", () => {
Promise.all([resolveProjectKey(repo), resolveProjectKey(worktree)]),
).resolves.toEqual(["github.com/OpenClaw/OpenClaw", "github.com/OpenClaw/OpenClaw"]);
});
it.runIf(process.platform !== "win32")(
"falls back to the path key when the git lookup hangs",
async () => {
const parent = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-project-hang-"));
cleanup.push(parent);
const fakeBinDir = path.join(parent, "fake-bin");
await fs.mkdir(fakeBinDir);
const fakeGitPath = path.join(fakeBinDir, "git");
await fs.writeFile(
fakeGitPath,
`#!${process.execPath}\nsetInterval(() => {}, 1000);\n`,
"utf-8",
);
await fs.chmod(fakeGitPath, 0o755);
const repo = path.join(parent, "repo");
await fs.mkdir(repo);
await git(repo, "init");
await git(repo, "remote", "add", "origin", "https://github.com/OpenClaw/OpenClaw.git");
const started = Date.now();
const key = await withEnvAsync(
{ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ""}` },
() => resolveProjectKey(repo),
);
const elapsed = Date.now() - started;
expect(key).toBe(`path:${repo}`);
expect(elapsed).toBeLessThan(15_000);
},
20_000,
);
});
+17 -16
View File
@@ -1,10 +1,10 @@
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
import { runCommandWithTimeout } from "../process/exec.js";
import { parseGitUrl } from "./utils/git.js";
const execFileAsync = promisify(execFile);
const MAX_PROJECT_KEY_CACHE_ENTRIES = 128;
// Cheap git reads elsewhere bound at 4s (see detectGitRoot in infra/update-check.ts).
const GIT_CONFIG_TIMEOUT_MS = 4_000;
const projectKeyByRepoRoot = new Map<string, Promise<string>>();
@@ -32,20 +32,21 @@ function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit: number): void
async function resolveUncachedProjectKey(repoRoot: string): Promise<string> {
try {
const { stdout } = await execFileAsync(
"git",
["-C", repoRoot, "config", "--get", "remote.origin.url"],
{ encoding: "utf8" },
const result = await runCommandWithTimeout(
["git", "-C", repoRoot, "config", "--get", "remote.origin.url"],
{ timeoutMs: GIT_CONFIG_TIMEOUT_MS },
);
const source = parseGitUrl(`git:${stdout.trim()}`);
if (source) {
// Userinfo is deliberately folded out so SSH and HTTPS clones converge.
// This accepts a rare same-host, same-path collision across distinct SSH
// accounts; the tradeoff is relevance bleed within one operator's store.
// Preserve remote path case so case-sensitive hosts fail closed. Providers
// with case-insensitive slugs may miss boosts/digests across casing variants,
// but folding paths could cross-inject memory between distinct repositories.
return escapeProjectKeyForAnnotation(`${source.host.toLowerCase()}/${source.path}`);
if (result.code === 0) {
const source = parseGitUrl(`git:${result.stdout.trim()}`);
if (source) {
// Userinfo is deliberately folded out so SSH and HTTPS clones converge.
// This accepts a rare same-host, same-path collision across distinct SSH
// accounts; the tradeoff is relevance bleed within one operator's store.
// Preserve remote path case so case-sensitive hosts fail closed. Providers
// with case-insensitive slugs may miss boosts/digests across casing variants,
// but folding paths could cross-inject memory between distinct repositories.
return escapeProjectKeyForAnnotation(`${source.host.toLowerCase()}/${source.path}`);
}
}
} catch {
// Repositories without an origin intentionally use their canonical local root.