Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions src/session/optimized-context-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defined } from "../../testkit/defined.js";
import { withMockedModuleDuring } from "../../testkit/mock-module.js";
import { describe, test, expect } from "bun:test";
import fs from "node:fs";
import os from "node:os";
Expand Down Expand Up @@ -1726,4 +1727,50 @@ describe("createOptimizedContextStore prompt dedupe (CL-9026)", () => {
expect(tree).not.toContain(extraPrompt);
expect(turnTexts((await store.load()).turns)).toEqual(turnTexts(live));
});

test("one remove failure still unstages the other paths in one index write", async () => {
const dir = tempDir();
const store = await createOptimizedContextStore(dir);
const huge = "x".repeat(300_000);
const live = [turn(`live-${huge}`), turn("live-tail")];
await store.writeTurns([...live]);
await store.writePrompt([turn(`prompt-${huge}`), turn("prompt-tail")]);
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);
await store.commit({ message: "two prompt segments" });
const extraPrompt = segmentFileName(PROMPT_FILE, 1);
expect(await gitLsTree(dir)).toEqual(
expect.arrayContaining([PROMPT_FILE, extraPrompt]),
);

await store.writePrompt(cloneTurns(live));
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);

const removeCalls: (string | string[])[] = [];
await withMockedModuleDuring(
import.meta.resolve("isomorphic-git"),
(real: typeof import("isomorphic-git")) => {
const remove: typeof real.default.remove = async (args) => {
removeCalls.push(args.filepath);
const paths = Array.isArray(args.filepath)
? args.filepath
: [args.filepath];
if (paths.includes(extraPrompt)) {
throw new Error("index write failed");
}
return real.default.remove(args);
};
return { ...real, default: { ...real.default, remove } };
},
() => store.commit({ message: "drop prompt segments" }),
);

const tree = await gitLsTree(dir);
expect(tree).not.toContain(PROMPT_FILE);
expect(tree).toContain(extraPrompt);
const written = removeCalls.filter((filepath) => {
const paths = Array.isArray(filepath) ? filepath : [filepath];
return !paths.includes(extraPrompt);
});
expect(written).toEqual([[PROMPT_FILE]]);
});
});
32 changes: 26 additions & 6 deletions src/session/optimized-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1332,14 +1332,34 @@ export async function createSessionStores(
const remove = [...new Set(toRemove)].filter((p) => !add.includes(p));
extraPaths = [...new Set([...add, ...remove])];

for (const filepath of add) {
await git.add({ fs, dir, filepath });
// Array `git.add` / `git.remove` each write the index once.
// Array `remove` is isomorphic-git 1.43.0; array `add` is older.
// A mid-batch add failure leaves the index unwritten. Only the
// outer catch calls `resetIndexPaths`.
if (add.length > 0) {
await git.add({ fs, dir, filepath: add });
}
for (const filepath of remove) {
if (remove.length > 0) {
try {
await git.remove({ fs, dir, filepath });
} catch {
// Already absent from the index.
await git.remove({ fs, dir, filepath: remove });
} catch (cause) {
// `GitIndex.delete` does not throw when a path is absent, so
// this is an index lock or write. It aborts before the index
// write. Skip the failing path and write the rest once.
let unstaged = false;
if (remove.length > 1) {
for (let index = 0; index < remove.length; index++) {
const rest = remove.filter((_, other) => other !== index);
try {
await git.remove({ fs, dir, filepath: rest });
unstaged = true;
break;
} catch {
// This omission still aborted before the index write.
}
}
}
if (!unstaged) throw cause;
}
}
const committed = await base.commit(options, signal);
Expand Down
Loading