Skip to content

Git subprocess diet: 161 → 133 spawns, 32 → 12 peak concurrency - #1062

Merged
arul28 merged 2 commits into
mainfrom
ade/t3-git-subprocess-diet
Aug 10, 2026
Merged

Git subprocess diet: 161 → 133 spawns, 32 → 12 peak concurrency#1062
arul28 merged 2 commits into
mainfrom
ade/t3-git-subprocess-diet

Conversation

@arul28

@arul28 arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Item 2 of three hygiene fixes from the 2026-08-09 t3code research (§3.4, git subprocess hygiene). Stacked on #1061 — the two share no files, so this retargets to main cleanly once #1061 lands.

Measured

Instrumented runGitOnce (the thing that actually spawns) and drove the real service graph against this checkout — 14 active lanes, real data, no mocks. Numbers are deterministic across five runs.

one full lane status refresh before after
git subprocesses 161 133
peak concurrent git processes 32 12
wall ~676 ms ~560 ms
git rev-parse main @ project root 14×
rev-parse HEAD per lane
duplicate groups (identical argv+cwd) 17 (46 procs) 2 (4 procs)

Peak concurrency was unbounded before — linear in lane count, so 40 lanes would have reached ~90 simultaneous git processes.

Correctness verified, not assumed: a cache-off control run in the same process returns byte-identical lane data — all 14 LaneSummary entries field-for-field including ahead/behind/dirty/rebaseInProgress, plus listSuggestions, listStatuses, and getBatchAssessment (ignoring its computedAt wall-clock stamp).

What changed

gitRepoCache.ts — repo-scoped cache keyed by the repository's common git dir, so every lane worktree of one repo shares an entry. That sharing is the entire point: keying per worktree would leave a 14-lane repo holding 14 copies of one answer. Two freshness classes — 1.5 s for resolved ref SHAs (long enough to collapse one refresh's duplicates; the lane list itself already caches for 10 s, so this is six times more conservative), 5 min for config-shaped answers. Concurrent misses share a single load, which is what actually collapses a parallel fan-out — a plain TTL check lets all 14 through before the first resolves.

Concurrency ceilingrunGit bounds concurrent git processes at 12. Mutations bypass the queue deliberately: the cap exists to bound read fan-out, while mutations are user-initiated, few, and some allow 300 s (rebase --continue). Queueing a Commit behind a bulk conflict assessment would be a priority inversion the unbounded code did not have — and in a runtime-backed build this is one gate shared by every project on the machine.

Invalidation lives in runGit, not at each mutation call site, so nothing has to remember — including code written later. It fires on failure as well as success: a rejected push can still have moved a remote-tracking ref, and an aborted rebase leaves HEAD moved.

Two review findings that were bugs, not polish

Undo was permanently breakable. git -c core.editor=true rebase --continue — the argv ADE actually ships at two call sites — had core.editor=true read as its verb, so a rebase that moved HEAD never invalidated. runLaneOperation then recorded a postHeadSha equal to the pre-rebase SHA, and undoLastHeadChange refuses to act when the recorded head disagrees with reality. Every rebase/merge continue faster than 1.5 s would have had Undo silently dead.

The changed-file badge double-counted. ChatGitToolbar.dirtyFileCount summed staged.length + unstaged.length; git status reports a file with both staged and unstaged edits as MM and the parser puts it in both lists, so one dirty file showed as two. (This is the double-count the research predicted — found by checking, not by taking it on faith.)

Both have regression tests verified to fail without their fix.

Not done, with evidence

t3's staged/unstaged --numstat merge does not apply to ADE. Empirically, in a scratch repo with f.txt staged +2/−1 then further edited +1/−0:

git diff --numstat          → 1  0  f.txt     (feeds changes.unstaged)
git diff --cached --numstat → 2  1  f.txt     (feeds changes.staged)
git diff HEAD --numstat     → 3  1  f.txt     (neither, and not decomposable)

Worse, a file whose worktree edit cancels its staged edit disappears from git diff HEAD entirely while git status still reports it MM — it would render as a staged file with 0 additions. ADE shows staged and unstaged as separate lists, so these are two different questions, not one asked twice. The two calls are already Promise.all'd, so they cost one spawn of wall time.

Follow-ups (measured, deliberately not in this PR)

  • git worktree list --porcelain already emits a HEAD <sha> line per worktree that the parser discards — wiring it through would remove the remaining 14 rev-parse HEAD spawns.
  • 2 duplicate groups remain (4 procs): rev-list --count / log -n 20 for two lanes sitting at the same HEAD, from rebaseSuggestionService.

Verification

  • /quality: 1 Blocker, 1 High, 5 Medium, 3 Low — all applied. The Blocker (the -c argv) was a defect in my own first cut.
  • Desktop: 1874 pass across the affected surface; 2 failures are pre-existing (verified identical with this branch stashed). ade-cli: 3188 pass, 1 machine-state flake that passes in isolation.
  • Typecheck + lint clean on both apps. Docs updated (ARCHITECTURE.md §9.2, lanes README, chat composer doc); validate-docs.mjs: 228 files.
  • Windows: --path-format=absolute was dropped so the probe no longer needs git ≥ 2.31; the returned path is resolved against cwd. All repo/cwd map keys go through the repo's canonical pathKey, so two spellings of one directory cannot become two caches with one missed invalidation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Git repository information loads faster through shared caching and controlled background processing.
    • Repeated repository lookups are handled more efficiently, including across worktrees.
  • Bug Fixes

    • Git data now refreshes promptly after repository changes, reducing stale results.
    • Improved consistency when Git commands fail.
    • Chat toolbar file counts now show distinct changed files without double-counting staged and unstaged changes.

Greptile Summary

The PR introduces a common-Git-directory cache and a global ceiling for concurrent read-only Git subprocesses while invalidating cached data after mutations.

  • Coalesces concurrent repository reads and applies separate freshness windows for refs and configuration.
  • Keeps worktree-specific identity and remote configuration isolated by worktree-keyed cache entries.
  • Correctly classifies remote branch history queries as read-only.
  • Deduplicates staged and unstaged paths in the chat toolbar’s changed-file count.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/git/git.ts Adds shared read concurrency control, common-Git-directory resolution, cached Git reads, and mutation-triggered invalidation.
apps/desktop/src/main/services/git/gitRepoCache.ts Implements repository-scoped TTL caching, concurrent-load coalescing, epoch-based invalidation, and Git command classification.
apps/desktop/src/main/services/git/gitOperationsService.ts Caches identity and origin reads with worktree-specific keys, resolving the previously reported cross-worktree contamination.
apps/desktop/src/main/services/conflicts/conflictService.ts Reuses cached ref resolution while preserving existing Git error shaping.
apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx Counts distinct changed paths across staged and unstaged collections.
apps/desktop/src/main/services/git/gitRepoCache.test.ts Covers command classification, cache TTLs, concurrent coalescing, failure caching, and invalidation races.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller["Lane and Git services"] --> RunGit["runGit"]
  RunGit --> Classify{"Ref-affecting mutation?"}
  Classify -->|No| Gate["12-process read ceiling"]
  Gate --> Spawn["Git subprocess"]
  Classify -->|Yes| Spawn
  Spawn -->|Mutation completes| Invalidate["Invalidate common-repo cache"]
  Caller --> CachedRead["runGitRepoCached"]
  CachedRead --> CommonDir["Resolve common Git directory"]
  CommonDir --> Cache["Repository cache"]
  Cache -->|Hit or shared in-flight load| Caller
  Cache -->|Miss| RunGit
Loading

Reviews (4): Last reviewed commit: "review: key config-derived cache entries..." | Re-trigger Greptile

Context used:

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Aug 10, 2026 5:37am

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Git execution now supports repository-scoped caching, read throttling, mutation invalidation, and common-directory memoization. Git consumers and tests use the new behavior. The chat Git toolbar now counts distinct changed paths.

Changes

Git repository caching

Layer / File(s) Summary
Repository cache core
apps/desktop/src/main/services/git/gitRepoCache.ts
Adds normalized repository cache state, command classification, TTLs, concurrent load sharing, invalidation epochs, and test reset support.
Git execution integration
apps/desktop/src/main/services/git/git.ts
Throttles read-only Git commands, invalidates caches after ref-affecting commands, memoizes common-directory discovery, and adds cached repository execution.
Cached Git consumers
apps/desktop/src/main/services/git/git.ts, apps/desktop/src/main/services/conflicts/conflictService.ts, apps/desktop/src/main/services/git/gitOperationsService.ts
Uses volatile caching for HEAD reads and stable caching for Git identity and origin remote lookups. Conflict HEAD errors now use formatted Git output.
Cache and execution validation
apps/desktop/src/main/services/git/gitRepoCache.test.ts, apps/desktop/src/main/services/git/git.test.ts
Tests command parsing, TTLs, concurrent loads, invalidation, cached HEAD refresh, option handling, and concurrent reads.

Chat Git toolbar

Layer / File(s) Summary
Unique dirty file count
apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx
Counts distinct paths across staged and unstaged changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • arul28/ADE#219: Related Git caching and ChatGitToolbar.tsx optimization work.

Suggested labels: desktop, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change and reports the measured reductions in Git subprocess count and peak concurrency.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/t3-git-subprocess-diet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/desktop/src/main/services/git/gitOperationsService.ts Outdated
Comment thread apps/desktop/src/main/services/git/gitRepoCache.ts Outdated
@arul28
arul28 changed the base branch from ade/t3-perf-hygiene-d09630c4 to main August 10, 2026 04:57
@arul28
arul28 force-pushed the ade/t3-git-subprocess-diet branch from bb83103 to 78b6093 Compare August 10, 2026 04:59
@arul28

arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78b60938b5

ℹ️ 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".

runGitRepoCached(
["remote", "get-url", "origin"],
{ cwd: lane.worktreePath, timeoutMs: 8_000 },
{ key: "remote-url:origin", cacheClass: "stable" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid sharing worktree-scoped config across lanes

When extensions.worktreeConfig is enabled, git config --worktree can give each lane a different remote.origin.url, user.name, or user.email—Git's git config -h explicitly describes this as a per-worktree config file. Because this result is keyed only by the common Git directory and remote-url:origin, whichever lane loads first supplies every other lane's remote for five minutes, potentially producing incorrect handoff/history links; the identity cache at line 1700 has the same problem. Keep effective-config queries worktree-aware rather than sharing them across the repository.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
apps/desktop/src/main/services/git/git.test.ts (2)

224-240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test does not assert the concurrency cap.

The assertions prove that 40 reads finish and that no result is lost. They pass equally well with an unbounded fan-out, so the 12-process ceiling, which is the headline behavior of this change, has no regression guard. Both refs also resolve to the same commit, so the comment about distinct refs is not verified by the assertions.

Export a small test seam from git.ts and assert the observed peak.

♻️ Proposed seam and assertion
// apps/desktop/src/main/services/git/git.ts
export function gitProcessConcurrencyStatsForTests(): { running: number; peak: number; max: number } {
  return { running: runningGitProcesses, peak: peakGitProcesses, max: MAX_CONCURRENT_GIT_PROCESSES };
}
     expect(results).toHaveLength(40);
+    const stats = gitProcessConcurrencyStatsForTests();
+    expect(stats.peak).toBeGreaterThan(1);
+    expect(stats.peak).toBeLessThanOrEqual(stats.max);
     for (const result of results) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/git/git.test.ts` around lines 224 - 240,
Export a gitProcessConcurrencyStatsForTests seam from git.ts that reports
runningGitProcesses, peakGitProcesses, and MAX_CONCURRENT_GIT_PROCESSES. Update
the “bounds concurrent read fan-out” test to reset or capture stats before
launching the 40 runGit calls, then assert the observed peak never exceeds the
configured max of 12 while preserving the existing result assertions.

Source: Coding guidelines


176-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cleanup and cache isolation to the fixture.

scratchRepo creates a temporary repository per test and never removes it, so each run leaves three directories behind. The tests also share the module-level caches in git.ts and gitRepoCache.ts. Isolation holds today only because mkdtempSync returns a fresh path each time. Reset both caches and remove the directories so the suite stays deterministic if a fixture path is ever reused.

♻️ Proposed fixture cleanup
 describe("repo cache invalidation through runGit", () => {
+  const scratchRepos: string[] = [];
+
+  beforeEach(() => {
+    resetGitRepoCacheForTests();
+    resetGitCommonDirCacheForTests();
+  });
+
+  afterEach(() => {
+    for (const dir of scratchRepos.splice(0)) {
+      fs.rmSync(dir, { recursive: true, force: true });
+    }
+  });
+
   function scratchRepo(): string {
     const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-git-repo-cache-"));
+    scratchRepos.push(repoRoot);

Import afterEach and beforeEach from vitest, resetGitCommonDirCacheForTests from ./git, and resetGitRepoCacheForTests from ./gitRepoCache.

The three detect-non-literal-fs-filename warnings on these lines are false positives. repoRoot comes from fs.mkdtempSync, not from external input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/git/git.test.ts` around lines 176 - 186,
Update the repo cache invalidation tests around scratchRepo to track each
mkdtempSync-created repository, remove those directories in an afterEach
cleanup, and reset both git caches before each test using
resetGitCommonDirCacheForTests and resetGitRepoCacheForTests. Import the
required Vitest hooks and reset helpers, while preserving the existing fixture
setup and treating the filename-lint warnings as safe for generated repoRoot
paths.

Source: Linters/SAST tools

apps/desktop/src/main/services/git/git.ts (1)

362-371: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

User-initiated index mutations are throttled as reads.

isRefAffectingGitCommand now decides two separate questions: whether to invalidate the cache, and whether to bypass the queue. The two answers are not the same. add, rm, mv, and clean do not move refs, so they are correctly absent from REF_AFFECTING_GIT_VERBS, and they therefore take the read path here. stageFile, stageAll, and discardFile in gitOperationsService.ts run those verbs, so a click on Stage can now wait behind up to 12 bulk-assessment reads. That is the priority inversion the comment above says the bypass exists to prevent.

Use a second predicate for the queue decision instead of reusing the ref predicate.

♻️ Proposed split of the two decisions
-  const mutating = isRefAffectingGitCommand(args);
-  if (!mutating) await acquireGitSlot();
+  const mutating = isRefAffectingGitCommand(args);
+  // Queue bypass is a wider question than cache invalidation: `add`, `rm`,
+  // `mv`, and `clean` never move a ref, but they are user-initiated and must
+  // not wait behind a bulk read fan-out.
+  const bypassQueue = mutating || isUserInitiatedGitWrite(args);
+  if (!bypassQueue) await acquireGitSlot();
   let result: GitRunResult;
   try {
     result = await runGitOnce(args, opts);
     if (await shouldRetryAfterIndexLock(result)) {
       result = await runGitOnce(args, opts);
     }
   } finally {
-    if (!mutating) releaseGitSlot();
+    if (!bypassQueue) releaseGitSlot();
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/git/git.ts` around lines 362 - 371, Use a
separate predicate for queue bypass in runGit instead of reusing
isRefAffectingGitCommand, so user-initiated mutations such as add, rm, mv, and
clean skip the read throttle while ref-affecting commands retain cache
invalidation behavior. Update the mutating check and its supporting command
classification without changing the existing ref-invalidation predicate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/services/git/git.ts`:
- Around line 507-513: The cache keys for HEAD and shared refs are built
inconsistently from raw paths. In
apps/desktop/src/main/services/git/git.ts:507-513, export a gitRefCacheKey(cwd,
ref) helper that uses pathKey(cwd) for HEAD and omits the path for other refs,
then use it in the runGitRepoCached call; in
apps/desktop/src/main/services/conflicts/conflictService.ts:278-282, replace the
inline key template with gitRefCacheKey(cwd, ref) so both readers share
normalized keys.
- Around line 430-459: Update the successful common-directory memo used by
gitCommonDirFor so it is invalidated whenever a worktree mutation runs through
runGit. Remove the corresponding cwd entry from gitCommonDirByCwd (and keep
related in-flight or retry state consistent as needed) before or after executing
the worktree command, ensuring later lookups do not reuse a repository path from
a previous worktree at the same absolute cwd.

In `@apps/desktop/src/main/services/git/gitOperationsService.ts`:
- Around line 1696-1701: Update the readConfig caching in the git operations
flow around readConfig so cache entries include lane.worktreePath (or an
equivalent unique worktree identifier) in addition to the config key. Preserve
the existing repository bucket and stable caching behavior while ensuring
identities from different lane worktrees cannot share cached results.

In `@apps/desktop/src/main/services/git/gitRepoCache.ts`:
- Around line 215-231: Update the cache write in the load path guarded by
repo.epoch === loadEpoch to preserve the deterministic nowMs anchor while
ensuring expiresAt remains at least one volatile TTL beyond the load completion
time. Add a named regression test in gitRepoCache.test.ts that resolves a load
after the volatile TTL elapses and verifies the following read is served from
cache.

---

Nitpick comments:
In `@apps/desktop/src/main/services/git/git.test.ts`:
- Around line 224-240: Export a gitProcessConcurrencyStatsForTests seam from
git.ts that reports runningGitProcesses, peakGitProcesses, and
MAX_CONCURRENT_GIT_PROCESSES. Update the “bounds concurrent read fan-out” test
to reset or capture stats before launching the 40 runGit calls, then assert the
observed peak never exceeds the configured max of 12 while preserving the
existing result assertions.
- Around line 176-186: Update the repo cache invalidation tests around
scratchRepo to track each mkdtempSync-created repository, remove those
directories in an afterEach cleanup, and reset both git caches before each test
using resetGitCommonDirCacheForTests and resetGitRepoCacheForTests. Import the
required Vitest hooks and reset helpers, while preserving the existing fixture
setup and treating the filename-lint warnings as safe for generated repoRoot
paths.

In `@apps/desktop/src/main/services/git/git.ts`:
- Around line 362-371: Use a separate predicate for queue bypass in runGit
instead of reusing isRefAffectingGitCommand, so user-initiated mutations such as
add, rm, mv, and clean skip the read throttle while ref-affecting commands
retain cache invalidation behavior. Update the mutating check and its supporting
command classification without changing the existing ref-invalidation predicate.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 67818252-9a27-4c26-bf5f-ab3895aae4c4

📥 Commits

Reviewing files that changed from the base of the PR and between 5973627 and 78b6093.

⛔ Files ignored due to path filters (3)
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
  • docs/features/lanes/README.md is excluded by !docs/**
📒 Files selected for processing (7)
  • apps/desktop/src/main/services/conflicts/conflictService.ts
  • apps/desktop/src/main/services/git/git.test.ts
  • apps/desktop/src/main/services/git/git.ts
  • apps/desktop/src/main/services/git/gitOperationsService.ts
  • apps/desktop/src/main/services/git/gitRepoCache.test.ts
  • apps/desktop/src/main/services/git/gitRepoCache.ts
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx

Comment on lines +430 to +459
export async function gitCommonDirFor(cwd: string): Promise<string> {
const cacheKey = pathKey(cwd);
const memo = gitCommonDirByCwd.get(cacheKey);
if (memo !== undefined) return memo;
const retryAt = gitCommonDirRetryAtByCwd.get(cacheKey);
if (retryAt !== undefined && retryAt > Date.now()) return "";
const existing = gitCommonDirInFlight.get(cacheKey);
if (existing) return await existing;

const request = (async () => {
const res = await runGit(["rev-parse", "--git-common-dir"], {
cwd,
timeoutMs: 8_000,
maxOutputBytes: 8 * 1024,
});
const raw = res.exitCode === 0 ? res.stdout.trim() : "";
if (!raw) {
gitCommonDirRetryAtByCwd.set(cacheKey, Date.now() + GIT_COMMON_DIR_RETRY_MS);
return "";
}
const resolved = path.resolve(cwd, raw);
gitCommonDirByCwd.set(cacheKey, resolved);
gitCommonDirRetryAtByCwd.delete(cacheKey);
return resolved;
})().finally(() => {
if (gitCommonDirInFlight.get(cacheKey) === request) gitCommonDirInFlight.delete(cacheKey);
});
gitCommonDirInFlight.set(cacheKey, request);
return await request;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

The successful common-dir memo is never dropped.

gitCommonDirByCwd keeps a resolved path for the process lifetime, and no worktree mutation clears it. If ADE removes a lane worktree and a later worktree of a different repository is created at the same absolute path, knownGitCommonDir returns the previous repository. Two consequences follow: runGitRepoCached reads land in the wrong repository bucket, and mutation invalidation drops the wrong bucket. The retry map is already cleared for failures, so only the success path is affected.

Drop the memo entry for a cwd when a worktree command runs through runGit, or key the memo on the worktree's git dir instead of the cwd.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/git/git.ts` around lines 430 - 459, Update the
successful common-directory memo used by gitCommonDirFor so it is invalidated
whenever a worktree mutation runs through runGit. Remove the corresponding cwd
entry from gitCommonDirByCwd (and keep related in-flight or retry state
consistent as needed) before or after executing the worktree command, ensuring
later lookups do not reuse a repository path from a previous worktree at the
same absolute cwd.

Comment on lines +507 to +513
// Shares the repo cache — and the key shape — with conflictService's own HEAD
// read, which is how the two spawns every lane used to pay become one.
const res = await runGitRepoCached(
["rev-parse", "HEAD"],
{ cwd: worktreePath, timeoutMs: 8_000 },
{ key: `rev-parse:${worktreePath}:HEAD`, cacheClass: "volatile" },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Both HEAD readers build the cache key from a raw path string. The repository bucket is normalized with pathKey, but neither key is. The documented collision between the two services therefore holds only when both callers pass byte-identical spellings of one path, and a repo-wide ref such as the shared base branch cannot share a single entry across lane worktrees of one repository. Add one shared key builder that normalizes the path and omits it for refs that are not worktree-specific.

  • apps/desktop/src/main/services/git/git.ts#L507-L513: export a gitRefCacheKey(cwd, ref) helper that returns rev-parse:${pathKey(cwd)}:HEAD for HEAD and rev-parse:${ref} for any other ref, then use it here.
  • apps/desktop/src/main/services/conflicts/conflictService.ts#L278-L282: replace the inline template key with the same gitRefCacheKey(cwd, ref) helper so the two services collide by construction instead of by convention.
📍 Affects 2 files
  • apps/desktop/src/main/services/git/git.ts#L507-L513 (this comment)
  • apps/desktop/src/main/services/conflicts/conflictService.ts#L278-L282
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/git/git.ts` around lines 507 - 513, The cache
keys for HEAD and shared refs are built inconsistently from raw paths. In
apps/desktop/src/main/services/git/git.ts:507-513, export a gitRefCacheKey(cwd,
ref) helper that uses pathKey(cwd) for HEAD and omits the path for other refs,
then use it in the runGitRepoCached call; in
apps/desktop/src/main/services/conflicts/conflictService.ts:278-282, replace the
inline key template with gitRefCacheKey(cwd, ref) so both readers share
normalized keys.

Comment thread apps/desktop/src/main/services/git/gitOperationsService.ts
Comment thread apps/desktop/src/main/services/git/gitRepoCache.ts
@arul28

arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b089f0a022

ℹ️ 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".

Comment on lines +509 to +512
const res = await runGitRepoCached(
["rev-parse", "HEAD"],
{ cwd: worktreePath, timeoutMs: 8_000 },
{ key: `rev-parse:${worktreePath}:HEAD`, cacheClass: "volatile" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep safety-critical HEAD reads out of the TTL cache

When HEAD changes outside runGit—for example, an agent or user commits in an ADE terminal—this cache can return the previous SHA for 1.5 seconds. undoLastHeadChange uses getHeadSha for its "head has changed" guard and then runs reset --hard; during that window the stale value can make the guard pass and reset past the external commit, while runLaneOperation can also record an incorrect preHeadSha. Keep operational snapshots and destructive-action guards on an uncached HEAD read, reserving this cache for status/prediction polling.

Useful? React with 👍 / 👎.

arul28 and others added 2 commits August 10, 2026 01:33
Measured on this checkout (14 lanes), one full lane status refresh spawned
**161 git processes with 32 peak concurrency**. 31 of those were
byte-identical argv at a byte-identical cwd: `git rev-parse main` ran
**14 times at the project root inside a single refresh**, and every lane
paid two separate `rev-parse HEAD` because conflictService and
rebaseSuggestionService each asked without knowing the other had.

After: **133 processes, 12 peak, ~676ms -> ~560ms.** Lane data verified
byte-identical against a cache-off control run in the same process — all
14 LaneSummary entries field-for-field, plus listSuggestions,
listStatuses, and getBatchAssessment (ignoring its `computedAt` stamp).

New `gitRepoCache.ts` is keyed by the repository's **common git dir**, so
every lane worktree of one repo shares an entry. That sharing is the whole
point: keying per worktree would leave a 14-lane repo holding 14 copies of
one answer. Two freshness classes — 1.5s for resolved ref SHAs (long
enough to collapse one refresh's duplicates, and less than a sixth of the
10s the lane list itself already caches for), 5 min for config-shaped
answers. Concurrent misses share one load, which is what actually collapses
a parallel fan-out; a plain TTL check lets all 14 through before the first
resolves.

`runGit` now bounds concurrent git processes at 12. Mutations bypass the
queue deliberately — the cap exists to bound read fan-out, while mutations
are user-initiated, few, and some allow 300s, so queueing a Commit behind
a bulk conflict assessment would be a priority inversion the unbounded
code did not have.

Invalidation lives in `runGit` itself rather than at each mutation call
site, so nothing has to remember — including code written later. It fires
on failure as well as success: a rejected push can still have moved a
remote-tracking ref, and an aborted rebase leaves HEAD moved.

Two findings from review that were bugs, not polish:

- `git -c core.editor=true rebase --continue` — the argv ADE actually uses
  — read `core.editor=true` as the verb, so a rebase that moved HEAD never
  invalidated. `runLaneOperation` then recorded a `postHeadSha` equal to
  the pre-rebase SHA, and Undo refuses to act when the recorded head does
  not match reality: Undo would have been permanently dead for every
  rebase/merge continue faster than 1.5s.
- `ChatGitToolbar`'s changed-file badge summed `staged.length +
  unstaged.length`. `git status` reports a file with both staged and
  unstaged edits as `MM` and the parser puts it in both lists, so one
  dirty file counted as two.

Both have regression tests verified to fail without their fix.

Not done, with evidence: t3's staged/unstaged `--numstat` merge does not
apply here. `git diff HEAD --numstat` reports the composite (a file staged
+2/-1 then edited +1/-0 shows `3 1`, which is neither side), and a file
whose worktree edit cancels its staged edit vanishes from the output while
`git status` still reports it `MM`. ADE shows the two as separate lists, so
the two calls are different questions, not one asked twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`git config` is repo-wide only until a repo enables
`extensions.worktreeConfig`, after which each lane can carry its own
`user.name`, `user.email`, or `remote.origin.url`. A repo-wide key served
whichever lane asked first to every other lane, mis-attributing commits.
ADE never enables the extension, but a user's repo can — and these two
reads are per-lane anyway, so the repo-wide key was saving nothing
measurable.

Also treat `git branch -r/-a` listings as read-only. ADE's actual history
query (`branch -r --contains <sha> <upstream>`) was already classified
correctly via `--contains`; a bare `-r` listing was not.

Raised independently by Greptile and Codex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@arul28
arul28 force-pushed the ade/t3-git-subprocess-diet branch from b089f0a to 43566e5 Compare August 10, 2026 05:37
@arul28
arul28 merged commit 00eca20 into main Aug 10, 2026
37 checks passed
@arul28
arul28 deleted the ade/t3-git-subprocess-diet branch August 10, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant