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
99import { checkpointRefForThreadTurn } from "./Utils.ts";
1010import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts";
1111import * as CheckpointStore from "./CheckpointStore.ts";
12import { CheckpointThreadNotFoundError } from "./Errors.ts";
1213
1314function makeThreadCheckpointContext(input: {
1415 readonly projectId: ProjectId;
......@@ -412,7 +413,14 @@ describe("CheckpointDiffQuery.layer", () => {
412413 });
413414 }).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 );
416424 }),
417425 );
418426});
apps/server/src/checkpointing/CheckpointDiffQuery.ts+40-29
......@@ -22,7 +22,13 @@ import * as Option from "effect/Option";
2222import * as Schema from "effect/Schema";
2323
2424import * 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";
2632import type { CheckpointServiceError } from "./Errors.ts";
2733import { checkpointRefForThreadTurn } from "./Utils.ts";
2834import * as CheckpointStore from "./CheckpointStore.ts";
......@@ -92,9 +98,9 @@ export const make = Effect.gen(function* () {
9298 diff: "",
9399 };
94100 if (!isTurnDiffResult(emptyDiff)) {
95 return yield* new CheckpointInvariantError({
101 return yield* new CheckpointDiffResultInvalidError({
96102 operation,
97 detail: "Computed turn diff result does not satisfy contract schema.",
103 threadId: input.threadId,
98104 });
99105 }
100106 return emptyDiff;
......@@ -104,9 +110,9 @@ export const make = Effect.gen(function* () {
104110 .getThreadCheckpointContext(input.threadId)
105111 .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext"));
106112 if (Option.isNone(threadContext)) {
107 return yield* new CheckpointInvariantError({
113 return yield* new CheckpointThreadNotFoundError({
108114 operation,
109 detail: `Thread '${input.threadId}' not found.`,
115 threadId: input.threadId,
110116 });
111117 }
112118
......@@ -115,18 +121,19 @@ export const make = Effect.gen(function* () {
115121 0,
116122 );
117123 if (input.toTurnCount > maxTurnCount) {
118 return yield* new CheckpointUnavailableError({
124 return yield* new CheckpointTurnRangeUnavailableError({
125 operation,
119126 threadId: input.threadId,
120 turnCount: input.toTurnCount,
121 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`,
127 requestedTurnCount: input.toTurnCount,
128 availableTurnCount: maxTurnCount,
122129 });
123130 }
124131
125132 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
126133 if (!workspaceCwd) {
127 return yield* new CheckpointInvariantError({
134 return yield* new CheckpointWorkspacePathMissingError({
128135 operation,
129 detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`,
136 threadId: input.threadId,
130137 });
131138 }
132139
......@@ -137,10 +144,11 @@ export const make = Effect.gen(function* () {
137144 (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount,
138145 )?.checkpointRef;
139146 if (!fromCheckpointRef) {
140 return yield* new CheckpointUnavailableError({
147 return yield* new CheckpointRefUnavailableError({
148 operation,
141149 threadId: input.threadId,
142150 turnCount: input.fromTurnCount,
143 detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`,
151 checkpoint: "from",
144152 });
145153 }
146154
......@@ -148,10 +156,11 @@ export const make = Effect.gen(function* () {
148156 (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount,
149157 )?.checkpointRef;
150158 if (!toCheckpointRef) {
151 return yield* new CheckpointUnavailableError({
159 return yield* new CheckpointRefUnavailableError({
160 operation,
152161 threadId: input.threadId,
153162 turnCount: input.toTurnCount,
154 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
163 checkpoint: "to",
155164 });
156165 }
157166
......@@ -167,9 +176,9 @@ export const make = Effect.gen(function* () {
167176
168177 const turnDiff = buildTurnDiffResult(input, diff);
169178 if (!isTurnDiffResult(turnDiff)) {
170 return yield* new CheckpointInvariantError({
179 return yield* new CheckpointDiffResultInvalidError({
171180 operation,
172 detail: "Computed turn diff result does not satisfy contract schema.",
181 threadId: input.threadId,
173182 });
174183 }
175184
......@@ -200,9 +209,9 @@ export const make = Effect.gen(function* () {
200209 "",
201210 );
202211 if (!isTurnDiffResult(emptyDiff)) {
203 return yield* new CheckpointInvariantError({
212 return yield* new CheckpointDiffResultInvalidError({
204213 operation,
205 detail: "Computed full thread diff result does not satisfy contract schema.",
214 threadId: input.threadId,
206215 });
207216 }
208217 return emptyDiff satisfies OrchestrationGetFullThreadDiffResult;
......@@ -213,33 +222,35 @@ export const make = Effect.gen(function* () {
213222 .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext"));
214223
215224 if (Option.isNone(threadContext)) {
216 return yield* new CheckpointInvariantError({
225 return yield* new CheckpointThreadNotFoundError({
217226 operation,
218 detail: `Thread '${input.threadId}' not found.`,
227 threadId: input.threadId,
219228 });
220229 }
221230
222231 if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) {
223 return yield* new CheckpointUnavailableError({
232 return yield* new CheckpointTurnRangeUnavailableError({
233 operation,
224234 threadId: input.threadId,
225 turnCount: input.toTurnCount,
226 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`,
235 requestedTurnCount: input.toTurnCount,
236 availableTurnCount: threadContext.value.latestCheckpointTurnCount,
227237 });
228238 }
229239
230240 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
231241 if (!workspaceCwd) {
232 return yield* new CheckpointInvariantError({
242 return yield* new CheckpointWorkspacePathMissingError({
233243 operation,
234 detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`,
244 threadId: input.threadId,
235245 });
236246 }
237247
238248 if (!threadContext.value.toCheckpointRef) {
239 return yield* new CheckpointUnavailableError({
249 return yield* new CheckpointRefUnavailableError({
250 operation,
240251 threadId: input.threadId,
241252 turnCount: input.toTurnCount,
242 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
253 checkpoint: "to",
243254 });
244255 }
245256
......@@ -262,9 +273,9 @@ export const make = Effect.gen(function* () {
262273 diff,
263274 );
264275 if (!isTurnDiffResult(turnDiff)) {
265 return yield* new CheckpointInvariantError({
276 return yield* new CheckpointDiffResultInvalidError({
266277 operation,
267 detail: "Computed full thread diff result does not satisfy contract schema.",
278 threadId: input.threadId,
268279 });
269280 }
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 @@
1import { NonNegativeInt, ThreadId, type VcsError } from "@t3tools/contracts";
12import * as Schema from "effect/Schema";
3
24import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
3import type { VcsError } from "@t3tools/contracts";
45
5/**
6 * CheckpointUnavailableError - Expected checkpoint does not exist.
7 */
8export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()(
9 "CheckpointUnavailableError",
6export const CheckpointDiffOperation = Schema.Literals([
7 "CheckpointDiffQuery.getTurnDiff",
8 "CheckpointDiffQuery.getFullThreadDiff",
9]);
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",
1058 {
11 threadId: Schema.String,
12 turnCount: Schema.Number,
13 detail: Schema.String,
14 cause: Schema.optional(Schema.Defect()),
59 operation: CheckpointDiffOperation,
60 threadId: ThreadId,
61 requestedTurnCount: NonNegativeInt,
62 availableTurnCount: NonNegativeInt,
1563 },
1664) {
1765 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}.`;
1967 }
2068}
2169
22/**
23 * CheckpointInvariantError - Inconsistent provider/filesystem/catalog state.
24 */
25export class CheckpointInvariantError extends Schema.TaggedErrorClass<CheckpointInvariantError>()(
26 "CheckpointInvariantError",
70/** Expected checkpoint metadata does not contain the requested Git ref. */
71export class CheckpointRefUnavailableError extends Schema.TaggedErrorClass<CheckpointRefUnavailableError>()(
72 "CheckpointRefUnavailableError",
2773 {
28 operation: Schema.String,
29 detail: Schema.String,
30 cause: Schema.optional(Schema.Defect()),
74 operation: CheckpointDiffOperation,
75 threadId: ThreadId,
76 turnCount: NonNegativeInt,
77 checkpoint: Schema.Literals(["from", "to"]),
3178 },
3279) {
3380 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}.`;
3582 }
3683}
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;