authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 18:07:22-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 18:07:22-07:00
log0488e47d9c0d0ca42595341b15c1d187ea7107c8
treeb169fc4300cff6445f944ded32c487296dc0e7ae
parent0eba548282b96ee420ad0b1a3f5d9d4487376b90
signature Signed by PGP key B5690EEEBB952194

[codex] Structure checkpoint diff failures (#3453)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 164 insertions(+), 52 deletions(-)

apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+9-1
...@@ -9,6 +9,7 @@ import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSn...@@ -9,6 +9,7 @@ import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSn
9import { checkpointRefForThreadTurn } from "./Utils.ts";9import { checkpointRefForThreadTurn } from "./Utils.ts";
10import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts";10import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts";
11import * as CheckpointStore from "./CheckpointStore.ts";11import * as CheckpointStore from "./CheckpointStore.ts";
12import { CheckpointThreadNotFoundError } from "./Errors.ts";
1213
13function makeThreadCheckpointContext(input: {14function makeThreadCheckpointContext(input: {
14 readonly projectId: ProjectId;15 readonly projectId: ProjectId;
...@@ -412,7 +413,14 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -412,7 +413,14 @@ describe("CheckpointDiffQuery.layer", () => {
412 });413 });
413 }).pipe(Effect.provide(layer), Effect.flip);414 }).pipe(Effect.provide(layer), Effect.flip);
414415
415 expect(error.message).toContain("Thread 'thread-missing' not found.");416 expect(error).toBeInstanceOf(CheckpointThreadNotFoundError);
417 expect(error).toMatchObject({
418 operation: "CheckpointDiffQuery.getTurnDiff",
419 threadId,
420 });
421 expect(error.message).toBe(
422 "Checkpoint invariant violation in CheckpointDiffQuery.getTurnDiff: Thread 'thread-missing' not found.",
423 );
416 }),424 }),
417 );425 );
418});426});
apps/server/src/checkpointing/CheckpointDiffQuery.ts+40-29
...@@ -22,7 +22,13 @@ import * as Option from "effect/Option";...@@ -22,7 +22,13 @@ import * as Option from "effect/Option";
22import * as Schema from "effect/Schema";22import * as Schema from "effect/Schema";
2323
24import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";24import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
25import { CheckpointInvariantError, CheckpointUnavailableError } from "./Errors.ts";25import {
26 CheckpointDiffResultInvalidError,
27 CheckpointRefUnavailableError,
28 CheckpointThreadNotFoundError,
29 CheckpointTurnRangeUnavailableError,
30 CheckpointWorkspacePathMissingError,
31} from "./Errors.ts";
26import type { CheckpointServiceError } from "./Errors.ts";32import type { CheckpointServiceError } from "./Errors.ts";
27import { checkpointRefForThreadTurn } from "./Utils.ts";33import { checkpointRefForThreadTurn } from "./Utils.ts";
28import * as CheckpointStore from "./CheckpointStore.ts";34import * as CheckpointStore from "./CheckpointStore.ts";
...@@ -92,9 +98,9 @@ export const make = Effect.gen(function* () {...@@ -92,9 +98,9 @@ export const make = Effect.gen(function* () {
92 diff: "",98 diff: "",
93 };99 };
94 if (!isTurnDiffResult(emptyDiff)) {100 if (!isTurnDiffResult(emptyDiff)) {
95 return yield* new CheckpointInvariantError({101 return yield* new CheckpointDiffResultInvalidError({
96 operation,102 operation,
97 detail: "Computed turn diff result does not satisfy contract schema.",103 threadId: input.threadId,
98 });104 });
99 }105 }
100 return emptyDiff;106 return emptyDiff;
...@@ -104,9 +110,9 @@ export const make = Effect.gen(function* () {...@@ -104,9 +110,9 @@ export const make = Effect.gen(function* () {
104 .getThreadCheckpointContext(input.threadId)110 .getThreadCheckpointContext(input.threadId)
105 .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext"));111 .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext"));
106 if (Option.isNone(threadContext)) {112 if (Option.isNone(threadContext)) {
107 return yield* new CheckpointInvariantError({113 return yield* new CheckpointThreadNotFoundError({
108 operation,114 operation,
109 detail: `Thread '${input.threadId}' not found.`,115 threadId: input.threadId,
110 });116 });
111 }117 }
112118
...@@ -115,18 +121,19 @@ export const make = Effect.gen(function* () {...@@ -115,18 +121,19 @@ export const make = Effect.gen(function* () {
115 0,121 0,
116 );122 );
117 if (input.toTurnCount > maxTurnCount) {123 if (input.toTurnCount > maxTurnCount) {
118 return yield* new CheckpointUnavailableError({124 return yield* new CheckpointTurnRangeUnavailableError({
125 operation,
119 threadId: input.threadId,126 threadId: input.threadId,
120 turnCount: input.toTurnCount,127 requestedTurnCount: input.toTurnCount,
121 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`,128 availableTurnCount: maxTurnCount,
122 });129 });
123 }130 }
124131
125 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;132 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
126 if (!workspaceCwd) {133 if (!workspaceCwd) {
127 return yield* new CheckpointInvariantError({134 return yield* new CheckpointWorkspacePathMissingError({
128 operation,135 operation,
129 detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`,136 threadId: input.threadId,
130 });137 });
131 }138 }
132139
...@@ -137,10 +144,11 @@ export const make = Effect.gen(function* () {...@@ -137,10 +144,11 @@ export const make = Effect.gen(function* () {
137 (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount,144 (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount,
138 )?.checkpointRef;145 )?.checkpointRef;
139 if (!fromCheckpointRef) {146 if (!fromCheckpointRef) {
140 return yield* new CheckpointUnavailableError({147 return yield* new CheckpointRefUnavailableError({
148 operation,
141 threadId: input.threadId,149 threadId: input.threadId,
142 turnCount: input.fromTurnCount,150 turnCount: input.fromTurnCount,
143 detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`,151 checkpoint: "from",
144 });152 });
145 }153 }
146154
...@@ -148,10 +156,11 @@ export const make = Effect.gen(function* () {...@@ -148,10 +156,11 @@ export const make = Effect.gen(function* () {
148 (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount,156 (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount,
149 )?.checkpointRef;157 )?.checkpointRef;
150 if (!toCheckpointRef) {158 if (!toCheckpointRef) {
151 return yield* new CheckpointUnavailableError({159 return yield* new CheckpointRefUnavailableError({
160 operation,
152 threadId: input.threadId,161 threadId: input.threadId,
153 turnCount: input.toTurnCount,162 turnCount: input.toTurnCount,
154 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,163 checkpoint: "to",
155 });164 });
156 }165 }
157166
...@@ -167,9 +176,9 @@ export const make = Effect.gen(function* () {...@@ -167,9 +176,9 @@ export const make = Effect.gen(function* () {
167176
168 const turnDiff = buildTurnDiffResult(input, diff);177 const turnDiff = buildTurnDiffResult(input, diff);
169 if (!isTurnDiffResult(turnDiff)) {178 if (!isTurnDiffResult(turnDiff)) {
170 return yield* new CheckpointInvariantError({179 return yield* new CheckpointDiffResultInvalidError({
171 operation,180 operation,
172 detail: "Computed turn diff result does not satisfy contract schema.",181 threadId: input.threadId,
173 });182 });
174 }183 }
175184
...@@ -200,9 +209,9 @@ export const make = Effect.gen(function* () {...@@ -200,9 +209,9 @@ export const make = Effect.gen(function* () {
200 "",209 "",
201 );210 );
202 if (!isTurnDiffResult(emptyDiff)) {211 if (!isTurnDiffResult(emptyDiff)) {
203 return yield* new CheckpointInvariantError({212 return yield* new CheckpointDiffResultInvalidError({
204 operation,213 operation,
205 detail: "Computed full thread diff result does not satisfy contract schema.",214 threadId: input.threadId,
206 });215 });
207 }216 }
208 return emptyDiff satisfies OrchestrationGetFullThreadDiffResult;217 return emptyDiff satisfies OrchestrationGetFullThreadDiffResult;
...@@ -213,33 +222,35 @@ export const make = Effect.gen(function* () {...@@ -213,33 +222,35 @@ export const make = Effect.gen(function* () {
213 .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext"));222 .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext"));
214223
215 if (Option.isNone(threadContext)) {224 if (Option.isNone(threadContext)) {
216 return yield* new CheckpointInvariantError({225 return yield* new CheckpointThreadNotFoundError({
217 operation,226 operation,
218 detail: `Thread '${input.threadId}' not found.`,227 threadId: input.threadId,
219 });228 });
220 }229 }
221230
222 if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) {231 if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) {
223 return yield* new CheckpointUnavailableError({232 return yield* new CheckpointTurnRangeUnavailableError({
233 operation,
224 threadId: input.threadId,234 threadId: input.threadId,
225 turnCount: input.toTurnCount,235 requestedTurnCount: input.toTurnCount,
226 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`,236 availableTurnCount: threadContext.value.latestCheckpointTurnCount,
227 });237 });
228 }238 }
229239
230 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;240 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
231 if (!workspaceCwd) {241 if (!workspaceCwd) {
232 return yield* new CheckpointInvariantError({242 return yield* new CheckpointWorkspacePathMissingError({
233 operation,243 operation,
234 detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`,244 threadId: input.threadId,
235 });245 });
236 }246 }
237247
238 if (!threadContext.value.toCheckpointRef) {248 if (!threadContext.value.toCheckpointRef) {
239 return yield* new CheckpointUnavailableError({249 return yield* new CheckpointRefUnavailableError({
250 operation,
240 threadId: input.threadId,251 threadId: input.threadId,
241 turnCount: input.toTurnCount,252 turnCount: input.toTurnCount,
242 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,253 checkpoint: "to",
243 });254 });
244 }255 }
245256
...@@ -262,9 +273,9 @@ export const make = Effect.gen(function* () {...@@ -262,9 +273,9 @@ export const make = Effect.gen(function* () {
262 diff,273 diff,
263 );274 );
264 if (!isTurnDiffResult(turnDiff)) {275 if (!isTurnDiffResult(turnDiff)) {
265 return yield* new CheckpointInvariantError({276 return yield* new CheckpointDiffResultInvalidError({
266 operation,277 operation,
267 detail: "Computed full thread diff result does not satisfy contract schema.",278 threadId: input.threadId,
268 });279 });
269 }280 }
270281
apps/server/src/checkpointing/Errors.test.ts created+39
...@@ -0,0 +1,39 @@
1import { expect, it } from "@effect/vitest";
2import { ThreadId } from "@t3tools/contracts";
3
4import {
5 CheckpointRefUnavailableError,
6 CheckpointTurnRangeUnavailableError,
7 CheckpointWorkspacePathMissingError,
8} from "./Errors.ts";
9
10const threadId = ThreadId.make("thread-1");
11
12it("derives checkpoint messages from structured context", () => {
13 const range = new CheckpointTurnRangeUnavailableError({
14 operation: "CheckpointDiffQuery.getTurnDiff",
15 threadId,
16 requestedTurnCount: 4,
17 availableTurnCount: 2,
18 });
19 const checkpoint = new CheckpointRefUnavailableError({
20 operation: "CheckpointDiffQuery.getTurnDiff",
21 threadId,
22 turnCount: 2,
23 checkpoint: "to",
24 });
25 const workspace = new CheckpointWorkspacePathMissingError({
26 operation: "CheckpointDiffQuery.getFullThreadDiff",
27 threadId,
28 });
29
30 expect(range.message).toBe(
31 "Checkpoint unavailable for thread thread-1 turn 4: Turn diff range exceeds current turn count: requested 4, current 2.",
32 );
33 expect(checkpoint.message).toBe(
34 "Checkpoint unavailable for thread thread-1 turn 2: Checkpoint ref is unavailable for turn 2.",
35 );
36 expect(workspace.message).toBe(
37 "Checkpoint invariant violation in CheckpointDiffQuery.getFullThreadDiff: Workspace path missing for thread 'thread-1' when computing full thread diff.",
38 );
39});
apps/server/src/checkpointing/Errors.ts+76-22
...@@ -1,40 +1,94 @@...@@ -1,40 +1,94 @@
1import { NonNegativeInt, ThreadId, type VcsError } from "@t3tools/contracts";
1import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
3
2import type { ProjectionRepositoryError } from "../persistence/Errors.ts";4import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
3import type { VcsError } from "@t3tools/contracts";
45
5/**6export const CheckpointDiffOperation = Schema.Literals([
6 * CheckpointUnavailableError - Expected checkpoint does not exist.7 "CheckpointDiffQuery.getTurnDiff",
7 */8 "CheckpointDiffQuery.getFullThreadDiff",
8export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()(9]);
9 "CheckpointUnavailableError",10export type CheckpointDiffOperation = typeof CheckpointDiffOperation.Type;
11
12/** The computed result does not satisfy the checkpoint RPC contract. */
13export class CheckpointDiffResultInvalidError extends Schema.TaggedErrorClass<CheckpointDiffResultInvalidError>()(
14 "CheckpointDiffResultInvalidError",
15 {
16 operation: CheckpointDiffOperation,
17 threadId: ThreadId,
18 },
19) {
20 override get message(): string {
21 const result =
22 this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
23 return `Checkpoint invariant violation in ${this.operation}: Computed ${result} result does not satisfy contract schema.`;
24 }
25}
26
27/** Projection state no longer contains the requested checkpoint thread. */
28export class CheckpointThreadNotFoundError extends Schema.TaggedErrorClass<CheckpointThreadNotFoundError>()(
29 "CheckpointThreadNotFoundError",
30 {
31 operation: CheckpointDiffOperation,
32 threadId: ThreadId,
33 },
34) {
35 override get message(): string {
36 return `Checkpoint invariant violation in ${this.operation}: Thread '${this.threadId}' not found.`;
37 }
38}
39
40/** The checkpoint thread has no workspace path from which to compute a diff. */
41export class CheckpointWorkspacePathMissingError extends Schema.TaggedErrorClass<CheckpointWorkspacePathMissingError>()(
42 "CheckpointWorkspacePathMissingError",
43 {
44 operation: CheckpointDiffOperation,
45 threadId: ThreadId,
46 },
47) {
48 override get message(): string {
49 const diff =
50 this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
51 return `Checkpoint invariant violation in ${this.operation}: Workspace path missing for thread '${this.threadId}' when computing ${diff}.`;
52 }
53}
54
55/** The requested turn lies beyond the latest available checkpoint. */
56export class CheckpointTurnRangeUnavailableError extends Schema.TaggedErrorClass<CheckpointTurnRangeUnavailableError>()(
57 "CheckpointTurnRangeUnavailableError",
10 {58 {
11 threadId: Schema.String,59 operation: CheckpointDiffOperation,
12 turnCount: Schema.Number,60 threadId: ThreadId,
13 detail: Schema.String,61 requestedTurnCount: NonNegativeInt,
14 cause: Schema.optional(Schema.Defect()),62 availableTurnCount: NonNegativeInt,
15 },63 },
16) {64) {
17 override get message(): string {65 override get message(): string {
18 return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: ${this.detail}`;66 return `Checkpoint unavailable for thread ${this.threadId} turn ${this.requestedTurnCount}: Turn diff range exceeds current turn count: requested ${this.requestedTurnCount}, current ${this.availableTurnCount}.`;
19 }67 }
20}68}
2169
22/**70/** Expected checkpoint metadata does not contain the requested Git ref. */
23 * CheckpointInvariantError - Inconsistent provider/filesystem/catalog state.71export class CheckpointRefUnavailableError extends Schema.TaggedErrorClass<CheckpointRefUnavailableError>()(
24 */72 "CheckpointRefUnavailableError",
25export class CheckpointInvariantError extends Schema.TaggedErrorClass<CheckpointInvariantError>()(
26 "CheckpointInvariantError",
27 {73 {
28 operation: Schema.String,74 operation: CheckpointDiffOperation,
29 detail: Schema.String,75 threadId: ThreadId,
30 cause: Schema.optional(Schema.Defect()),76 turnCount: NonNegativeInt,
77 checkpoint: Schema.Literals(["from", "to"]),
31 },78 },
32) {79) {
33 override get message(): string {80 override get message(): string {
34 return `Checkpoint invariant violation in ${this.operation}: ${this.detail}`;81 return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: Checkpoint ref is unavailable for turn ${this.turnCount}.`;
35 }82 }
36}83}
3784
38export type CheckpointStoreError = VcsError | CheckpointInvariantError | CheckpointUnavailableError;85export type CheckpointStoreError = VcsError;
3986
40export type CheckpointServiceError = CheckpointStoreError | ProjectionRepositoryError;87export type CheckpointServiceError =
88 | CheckpointStoreError
89 | ProjectionRepositoryError
90 | CheckpointDiffResultInvalidError
91 | CheckpointThreadNotFoundError
92 | CheckpointWorkspacePathMissingError
93 | CheckpointTurnRangeUnavailableError
94 | CheckpointRefUnavailableError;