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
62 changes: 62 additions & 0 deletions src/session/optimized-context-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,68 @@ describe("createOptimizedContextStore checkpoint", () => {
expect(tree).not.toContain("partial.jsonl");
expect(tree).not.toContain("untracked-junk.txt");
});

test("commits an on-disk extra turn segment that is not in HEAD without writeTurns", async () => {
const dir = tempDir();
const store = await createOptimizedContextStore(dir);
await store.writeTurns([turn("a")]);
await store.writePrompt([turn("p0")]);
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);
const first = await store.commit({ message: "base" });

const extraTurns = segmentFileName(TURNS_FILE, 1);
const extraPrompt = segmentFileName("prompt.jsonl", 1);
fs.writeFileSync(path.join(dir, extraTurns), jsonl([turn("b")]));
fs.writeFileSync(path.join(dir, extraPrompt), jsonl([turn("p")]));

const crashed = await createOptimizedContextStore(dir);
const second = await crashed.commit({ message: "heal extra segments" });
expect(second.hash).not.toBe(first.hash);

const tree = await gitLsTree(dir);
expect(tree).toContain(extraTurns);
expect(tree).toContain(extraPrompt);
});

test("pending extra turn segment prevents empty-checkpoint skip", async () => {
const dir = tempDir();
const store = await createOptimizedContextStore(dir);
const turns: ConversationTurn[] = [];
const big = "x".repeat(20_000);
for (let i = 0; i < 14; i++) {
turns.push(turn(`${i}-${big}`));
await store.writeTurns([...turns]);
}
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);
const first = await store.commit({ message: "base" });

for (let i = 14; i < 18; i++) {
turns.push(turn(`${i}-${big}`));
await store.writeTurns([...turns]);
}
const extraTurns = segmentFileName(TURNS_FILE, 1);
expect(fs.existsSync(path.join(dir, extraTurns))).toBe(true);

const second = await store.commit({ message: "pending extra" });
expect(second.hash).not.toBe(first.hash);
expect(await gitLsTree(dir)).toContain(extraTurns);
});

test("staged compact with unchanged disk still commits instead of returning HEAD", async () => {
const dir = tempDir();
const store = await createOptimizedContextStore(dir);
await store.writeTurns([turn("keep-a"), turn("keep-b"), turn("drop-me")]);
await store.writeMetadata(EMPTY_CHECKPOINT_METADATA);
const first = await store.commit({ message: "published original" });

await store.writeTurns([turn("[Compacted prior context]"), turn("keep-b")]);
const second = await store.commit({ message: "publish compact" });
expect(second.hash).not.toBe(first.hash);
expect(turnTexts((await store.load()).turns)).toEqual([
"[Compacted prior context]",
"keep-b",
]);
});
});

describe("createSessionStores", () => {
Expand Down
47 changes: 25 additions & 22 deletions src/session/optimized-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,28 +742,6 @@ export async function createSessionStores(
const headBefore = stagedRewrite === null ? null : await headOid(dir);
let extraPaths: string[] = [];

// Empty managed checkpoints must not create a new commit or stage
// session junk such as partial.jsonl. A staged unpublished rewrite
// is still unpublished on disk — skip would swallow the compact
// without writing it, so that path always goes through commit.
if (stagedRewrite === null) {
const managedFilepaths = [
...VENDOR_COMMIT_ROOT_FILES,
TOOL_OUTPUT_DIR,
EVIDENCE_ARCHIVE_DIR,
...pendingSegmentPaths,
...pendingBlobFilepaths,
];
if (!(await managedPathsDiffer(dir, managedFilepaths))) {
const [head] = await base.log(1);
if (head !== undefined) {
pendingBlobFilepaths.clear();
pendingSegmentPaths.clear();
return head;
}
}
}

try {
if (stagedRewrite !== null) {
await writeSegmented(writeTurnsSegmented, stagedRewrite);
Expand All @@ -789,6 +767,31 @@ export async function createSessionStores(
toAdd.push(EVIDENCE_ARCHIVE_DIR);
}

// Empty managed checkpoints must not create a new commit or stage
// session junk such as partial.jsonl. Discover extra turn/prompt
// segments first so a crash that left them untracked cannot skip.
// A staged unpublished rewrite is still unpublished on disk — skip
// would swallow the compact without writing it.
if (stagedRewrite === null) {
const managedFilepaths = [
...VENDOR_COMMIT_ROOT_FILES,
TOOL_OUTPUT_DIR,
EVIDENCE_ARCHIVE_DIR,
...pendingSegmentPaths,
...pendingBlobFilepaths,
...toAdd,
...toRemove,
];
if (!(await managedPathsDiffer(dir, managedFilepaths))) {
const [head] = await base.log(1);
if (head !== undefined) {
pendingBlobFilepaths.clear();
pendingSegmentPaths.clear();
return head;
}
}
}

const add = extraCommitPaths([...new Set(toAdd)]);
const remove = extraCommitPaths([...new Set(toRemove)]).filter(
(p) => !add.includes(p),
Expand Down
Loading