authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-05 04:12:01-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 04:12:01-05:00
logc843c19294bcea9a4f5cf19632b135459a16d214
tree3c934b371d436916f54cc2eaa747a6b5e0305181
parentab0933a41ae3a69632ea68809700a040bde26c22
signature Signed by PGP key B5690EEEBB952194

fix(server): resume checkpointing after git init (#10078)


2 files changed, 182 insertions(+), 77 deletions(-)

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+142-42
......@@ -292,6 +292,7 @@ describe("CheckpointReactor", () => {
292292 async function createHarness(options?: {
293293 readonly hasSession?: boolean;
294294 readonly seedFilesystemCheckpoints?: boolean;
295 readonly initializeGit?: boolean;
295296 readonly projectWorkspaceRoot?: string;
296297 readonly threadWorktreePath?: string | null;
297298 readonly threadBranch?: string | null;
......@@ -303,6 +304,9 @@ describe("CheckpointReactor", () => {
303304 readonly pullRequestRefreshCalls?: Array<string>;
304305 }) {
305306 const cwd = createGitRepository();
307 if (options?.initializeGit === false) {
308 NodeFS.rmSync(NodePath.join(cwd, ".git"), { recursive: true });
309 }
306310 tempDirs.push(cwd);
307311 const provider = createProviderServiceHarness(
308312 cwd,
......@@ -1158,52 +1162,148 @@ describe("CheckpointReactor", () => {
11581162 ).toBe(true);
11591163 });
11601164
1161 it("appends capture failure activity when turn diff summary cannot be derived", async () => {
1162 const harness = await createHarness({ seedFilesystemCheckpoints: false });
1163 const createdAt = "2026-01-01T00:00:00.000Z";
1164
1165 await Effect.runPromise(
1166 harness.engine.dispatch({
1167 type: "thread.session.set",
1168 commandId: CommandId.make("cmd-session-set-missing-baseline-diff"),
1165 effectIt.effect("captures a checkpoint without a summary when the baseline is missing", () =>
1166 Effect.gen(function* () {
1167 const harness = yield* Effect.promise(() =>
1168 createHarness({ seedFilesystemCheckpoints: false }),
1169 );
1170 harness.provider.emit({
1171 type: "turn.completed",
1172 eventId: EventId.make("evt-turn-completed-missing-baseline"),
1173 provider: ProviderDriverKind.make("codex"),
1174 createdAt: "2026-01-01T00:00:00.000Z",
11691175 threadId: ThreadId.make("thread-1"),
1170 session: {
1171 threadId: ThreadId.make("thread-1"),
1172 status: "ready",
1173 providerName: "codex",
1174 runtimeMode: "approval-required",
1175 activeTurnId: null,
1176 lastError: null,
1177 updatedAt: createdAt,
1178 },
1179 createdAt,
1180 }),
1181 );
1182
1183 harness.provider.emit({
1184 type: "turn.completed",
1185 eventId: EventId.make("evt-turn-completed-missing-baseline"),
1186 provider: ProviderDriverKind.make("codex"),
1176 turnId: asTurnId("turn-missing-baseline"),
1177 payload: { state: "completed" },
1178 });
1179 expect(yield* harness.nextReceipt).toMatchObject({
1180 type: "checkpoint.diff.finalized",
1181 checkpointTurnCount: 1,
1182 });
1183 yield* Effect.promise(harness.drain);
1184 const thread = (yield* Effect.promise(harness.readModel)).threads[0];
1185 expect(thread?.checkpoints[0]).toMatchObject({
1186 status: "ready",
1187 checkpointTurnCount: 1,
1188 files: [],
1189 });
1190 expect(
1191 gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1)),
1192 ).toBe(true);
1193 expect(
1194 thread?.activities.some((activity) => activity.kind === "checkpoint.capture.failed"),
1195 ).toBe(false);
1196 }),
1197 );
11871198
1188 createdAt: "2026-01-01T00:00:00.000Z",
1189 threadId: ThreadId.make("thread-1"),
1190 turnId: asTurnId("turn-missing-baseline"),
1191 payload: { state: "completed" },
1192 });
1199 effectIt.effect.each([
1200 { timing: "between turns", commit: false },
1201 { timing: "between turns", commit: true },
1202 { timing: "during a turn", commit: false },
1203 { timing: "during a turn", commit: true },
1204 ])("resumes checkpointing after git init $timing (commit: $commit)", ({ timing, commit }) =>
1205 Effect.gen(function* () {
1206 const harness = yield* Effect.promise(() =>
1207 createHarness({ initializeGit: false, seedFilesystemCheckpoints: false }),
1208 );
1209 const threadId = ThreadId.make("thread-1");
1210 const createdAt = "2026-01-01T00:00:00.000Z";
1211 const emit = (type: "turn.started" | "turn.completed", turn: number) =>
1212 harness.provider.emit({
1213 type,
1214 eventId: EventId.make(`${type}-${turn}`),
1215 provider: ProviderDriverKind.make("codex"),
1216 createdAt,
1217 threadId,
1218 turnId: asTurnId(`turn-${turn}`),
1219 ...(type === "turn.completed" ? { payload: { state: "completed" } } : {}),
1220 });
1221 emit("turn.started", 1);
1222 yield* Effect.promise(harness.drain);
1223 NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "before git\n");
1224 emit("turn.completed", 1);
1225 yield* Effect.promise(harness.drain);
1226 expect((yield* Effect.promise(harness.readModel)).threads[0]?.checkpoints).toEqual([]);
11931227
1194 await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
1195 const thread = await waitForThread(
1196 harness.readModel,
1197 (entry) =>
1198 entry.checkpoints.length === 1 &&
1199 entry.activities.some((activity) => activity.kind === "checkpoint.capture.failed"),
1200 );
1228 if (timing === "during a turn") {
1229 emit("turn.started", 2);
1230 yield* Effect.promise(harness.drain);
1231 }
1232 runGit(harness.cwd, ["init", "--initial-branch=main"]);
1233 if (commit) {
1234 runGit(harness.cwd, ["add", "."]);
1235 runGit(harness.cwd, [
1236 "-c",
1237 "user.name=Test",
1238 "-c",
1239 "user.email=test@example.com",
1240 "commit",
1241 "-m",
1242 "Initial",
1243 ]);
1244 }
1245 if (timing === "between turns") {
1246 // Exercise the domain entry point as well as the provider turn-start event.
1247 yield* harness.engine.dispatch({
1248 type: "thread.turn.start",
1249 commandId: CommandId.make("cmd-after-git-init"),
1250 threadId,
1251 message: {
1252 messageId: MessageId.make("message-after-git-init"),
1253 role: "user",
1254 text: "continue",
1255 attachments: [],
1256 },
1257 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1258 runtimeMode: "approval-required",
1259 createdAt,
1260 });
1261 expect(yield* harness.nextReceipt).toMatchObject({
1262 type: "checkpoint.baseline.captured",
1263 checkpointTurnCount: 0,
1264 });
1265 emit("turn.started", 2);
1266 yield* Effect.promise(harness.drain);
1267 }
1268 NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "after git\n");
1269 emit("turn.completed", 2);
1270 expect(yield* harness.nextReceipt).toMatchObject({
1271 type: "checkpoint.diff.finalized",
1272 checkpointTurnCount: 1,
1273 });
1274 expect(yield* harness.nextReceipt).toMatchObject({ type: "turn.processing.quiesced" });
1275 yield* Effect.promise(harness.drain);
1276 const firstCheckpoint = (yield* Effect.promise(harness.readModel)).threads[0]?.checkpoints[0];
1277 expect(firstCheckpoint?.files).toEqual(
1278 timing === "between turns"
1279 ? [{ path: "README.md", kind: "modified", additions: 1, deletions: 1 }]
1280 : [],
1281 );
1282 expect(
1283 gitShowFileAtRef(harness.cwd, checkpointRefForThreadTurn(threadId, 1), "README.md"),
1284 ).toBe("after git\n");
1285 expect(gitRefExists(harness.cwd, checkpointRefForThreadTurn(threadId, 0))).toBe(
1286 timing === "between turns",
1287 );
12011288
1202 expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
1203 expect(
1204 thread.activities.some((activity) => activity.kind === "checkpoint.capture.failed"),
1205 ).toBe(true);
1206 });
1289 emit("turn.started", 3);
1290 yield* Effect.promise(harness.drain);
1291 NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "next turn\n");
1292 emit("turn.completed", 3);
1293 expect(yield* harness.nextReceipt).toMatchObject({
1294 type: "checkpoint.diff.finalized",
1295 checkpointTurnCount: 2,
1296 });
1297 yield* Effect.promise(harness.drain);
1298 const thread = (yield* Effect.promise(harness.readModel)).threads[0];
1299 expect(thread?.checkpoints[1]?.files).toEqual([
1300 { path: "README.md", kind: "modified", additions: 1, deletions: 1 },
1301 ]);
1302 expect(
1303 thread?.activities.some((activity) => activity.kind === "checkpoint.capture.failed"),
1304 ).toBe(false);
1305 }),
1306 );
12071307
12081308 it("captures pre-turn baseline from project workspace root when thread worktree is unset", async () => {
12091309 const harness = await createHarness({
apps/server/src/orchestration/Layers/CheckpointReactor.ts+40-35
......@@ -258,41 +258,46 @@ const make = Effect.gen(function* () {
258258 // reflects files created or deleted during this turn.
259259 yield* workspaceEntries.refresh(input.cwd);
260260
261 const files = yield* checkpointStore
262 .diffCheckpoints({
263 cwd: input.cwd,
264 fromCheckpointRef,
265 toCheckpointRef: targetCheckpointRef,
266 fallbackFromToHead: false,
267 ignoreWhitespace: false,
268 format: "numstat",
269 })
270 .pipe(
271 Effect.map((diff) =>
272 parseTurnDiffFilesFromNumstat(diff).map((file) => ({
273 path: file.path,
274 kind: "modified" as const,
275 additions: file.additions,
276 deletions: file.deletions,
277 })),
278 ),
279 Effect.tapError((error) =>
280 appendCaptureFailureActivity({
281 threadId: input.threadId,
282 turnId: input.turnId,
283 detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`,
284 createdAt: input.createdAt,
285 }),
286 ),
287 Effect.catch((error) =>
288 Effect.logWarning("failed to derive checkpoint file summary", {
289 threadId: input.threadId,
290 turnId: input.turnId,
291 turnCount: input.turnCount,
292 detail: error.message,
293 }).pipe(Effect.as([])),
294 ),
295 );
261 // Git may have been initialized during this turn, leaving no pre-turn
262 // snapshot. Keep the completion checkpoint for future turns, but do not
263 // invent a baseline or attempt a diff against a ref that does not exist.
264 const files = yield* (
265 fromCheckpointExists
266 ? checkpointStore.diffCheckpoints({
267 cwd: input.cwd,
268 fromCheckpointRef,
269 toCheckpointRef: targetCheckpointRef,
270 fallbackFromToHead: false,
271 ignoreWhitespace: false,
272 format: "numstat",
273 })
274 : Effect.succeed("")
275 ).pipe(
276 Effect.map((diff) =>
277 parseTurnDiffFilesFromNumstat(diff).map((file) => ({
278 path: file.path,
279 kind: "modified" as const,
280 additions: file.additions,
281 deletions: file.deletions,
282 })),
283 ),
284 Effect.tapError((error) =>
285 appendCaptureFailureActivity({
286 threadId: input.threadId,
287 turnId: input.turnId,
288 detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`,
289 createdAt: input.createdAt,
290 }),
291 ),
292 Effect.catch((error) =>
293 Effect.logWarning("failed to derive checkpoint file summary", {
294 threadId: input.threadId,
295 turnId: input.turnId,
296 turnCount: input.turnCount,
297 detail: error.message,
298 }).pipe(Effect.as([])),
299 ),
300 );
296301
297302 const assistantMessageId =
298303 input.assistantMessageId ??