From eabec2a74c1f72c7ccb1926983732ec86384c41a Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 13:27:06 -0700 Subject: [PATCH] Fall back to isogit store when git is absent Harbor and minimal containers have no git binary; the optimized wrapper shells out via Bun.spawn and fails the first cycle commit. --- src/session/optimized-context-store.test.ts | 67 ++++++++++++++++++++- src/session/optimized-context-store.ts | 7 +++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/session/optimized-context-store.test.ts b/src/session/optimized-context-store.test.ts index 7d0ce6f2..68f4e8eb 100644 --- a/src/session/optimized-context-store.test.ts +++ b/src/session/optimized-context-store.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from "bun:test"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -446,3 +446,68 @@ describe("createOptimizedContextStore checkpoint", () => { expect(atHead).toHaveLength(total); }, 20_000); }); + +describe("createOptimizedContextStore without native git", () => { + const originalWhich = Bun.which.bind(Bun); + let whichSpy: ReturnType | undefined; + let spawnSpy: ReturnType | undefined; + + afterEach(() => { + whichSpy?.mockRestore(); + spawnSpy?.mockRestore(); + whichSpy = undefined; + spawnSpy = undefined; + }); + + test("returns the base isogit store and commits without spawning git", async () => { + whichSpy = spyOn(Bun, "which").mockImplementation((cmd: string) => { + if (cmd === "git") return null; + return originalWhich(cmd); + }); + spawnSpy = spyOn(Bun, "spawn"); + + const dir = tempDir(); + const store = await createOptimizedContextStore(dir); + + await store.writeTurns([turn("no-git")]); + await store.writeMetadata({ + pendingOperations: [], + tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 }, + }); + const commit = await store.commit({ message: "isogit fallback" }); + + expect(commit.hash.length).toBeGreaterThan(0); + expect(commit.message).toBe("isogit fallback"); + + const loaded = await store.load(); + expect(loaded.turns).toHaveLength(1); + expect((loaded.turns[0]!.content[0] as { text: string }).text).toBe("no-git"); + + const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => { + const argv = call[0]; + return Array.isArray(argv) && argv[0] === "git"; + }); + expect(gitSpawns).toHaveLength(0); + }); + + test("still wraps with native git when git is on PATH", async () => { + expect(originalWhich("git")).not.toBeNull(); + + const dir = tempDir(); + const store = await createOptimizedContextStore(dir); + spawnSpy = spyOn(Bun, "spawn"); + + await store.writeTurns([turn("with-git")]); + await store.writeMetadata({ + pendingOperations: [], + tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 }, + }); + await store.commit({ message: "native path" }); + + const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => { + const argv = call[0]; + return Array.isArray(argv) && argv[0] === "git"; + }); + expect(gitSpawns.length).toBeGreaterThan(0); + }); +}); diff --git a/src/session/optimized-context-store.ts b/src/session/optimized-context-store.ts index 3ab223d9..459386fe 100644 --- a/src/session/optimized-context-store.ts +++ b/src/session/optimized-context-store.ts @@ -361,8 +361,15 @@ async function reconcileSegmentStaging( * work per reactor checkpoint. Turns and prompt snapshots are written as rolling * segment files so `git add` re-hashes only the small active segment, and only * spilled tool-output blobs that are new since the last commit are staged. + * + * When native `git` is missing from PATH (Harbor / minimal containers), skip the + * wrapper and return the base isomorphic-git store — its commit path never shells out. */ export async function createOptimizedContextStore(dir: string): Promise { + if (Bun.which("git") === null) { + return await createIsogitStore(dir); + } + const base = await createIsogitStore(dir); const pendingBlobFilepaths = new Set(); const pendingSegmentPaths = new Set();