authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 22:05:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 22:05:35-07:00
log7118e43d16b157dd3079f354bd66ca5d772a9a93
tree7c8a186196469b91ddbcbbdf8871bff4fda077ec
parent4e8ee13d8f54e9aa84f1b15f9d802c30650217ea
signature Signed by PGP key B5690EEEBB952194

[codex] Refactor checkpointing Effect services (#3181)

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

19 files changed, 1102 insertions(+), 1141 deletions(-)

apps/server/integration/OrchestrationEngineHarness.integration.ts+4-5
......@@ -22,8 +22,7 @@ import * as Schema from "effect/Schema";
2222import * as Scope from "effect/Scope";
2323import * as Stream from "effect/Stream";
2424
25import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";
26import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";
25import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts";
2726import { TextGeneration, type TextGenerationShape } from "../src/textGeneration/TextGeneration.ts";
2827import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts";
2928import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";
......@@ -180,7 +179,7 @@ export interface OrchestrationIntegrationHarness {
180179 readonly engine: OrchestrationEngineShape;
181180 readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
182181 readonly providerService: ProviderService["Service"];
183 readonly checkpointStore: CheckpointStore["Service"];
182 readonly checkpointStore: CheckpointStore.CheckpointStore["Service"];
184183 readonly checkpointRepository: ProjectionCheckpointRepository["Service"];
185184 readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"];
186185 readonly waitForThread: (
......@@ -296,7 +295,7 @@ export const makeOrchestrationIntegrationHarness = (
296295 );
297296 const providerRegistryLayer = makeProviderRegistryLayer();
298297
299 const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistry.layer));
298 const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer));
300299 const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive;
301300 const runtimeServicesLayer = Layer.mergeAll(
302301 projectionSnapshotQueryLayer,
......@@ -399,7 +398,7 @@ export const makeOrchestrationIntegrationHarness = (
399398 runtime.runPromise(Effect.service(ProviderService)),
400399 ).pipe(Effect.orDie);
401400 const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () =>
402 runtime.runPromise(Effect.service(CheckpointStore)),
401 runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)),
403402 ).pipe(Effect.orDie);
404403 const checkpointRepository = yield* tryRuntimePromise(
405404 "load ProjectionCheckpointRepository service",
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts created+418
......@@ -0,0 +1,418 @@
1import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";
2import { it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import { describe, expect } from "vite-plus/test";
7
8import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
9import { checkpointRefForThreadTurn } from "./Utils.ts";
10import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts";
11import * as CheckpointStore from "./CheckpointStore.ts";
12
13function makeThreadCheckpointContext(input: {
14 readonly projectId: ProjectId;
15 readonly threadId: ThreadId;
16 readonly workspaceRoot: string;
17 readonly worktreePath: string | null;
18 readonly checkpointTurnCount: number;
19 readonly checkpointRef: CheckpointRef;
20}): ProjectionSnapshotQuery.ProjectionThreadCheckpointContext {
21 return {
22 threadId: input.threadId,
23 projectId: input.projectId,
24 workspaceRoot: input.workspaceRoot,
25 worktreePath: input.worktreePath,
26 checkpoints: [
27 {
28 turnId: TurnId.make("turn-1"),
29 checkpointTurnCount: input.checkpointTurnCount,
30 checkpointRef: input.checkpointRef,
31 status: "ready",
32 files: [],
33 assistantMessageId: null,
34 completedAt: "2026-01-01T00:00:00.000Z",
35 },
36 ],
37 };
38}
39
40describe("CheckpointDiffQuery.layer", () => {
41 it.effect("uses the narrow full-thread context lookup for all-turns diffs", () =>
42 Effect.gen(function* () {
43 const projectId = ProjectId.make("project-full-thread");
44 const threadId = ThreadId.make("thread-full-thread");
45 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 4);
46 let getThreadCheckpointContextCalls = 0;
47 let getFullThreadDiffContextCalls = 0;
48 const diffCheckpointsCalls: Array<{
49 readonly fromCheckpointRef: CheckpointRef;
50 readonly toCheckpointRef: CheckpointRef;
51 readonly cwd: string;
52 readonly ignoreWhitespace: boolean;
53 }> = [];
54
55 const checkpointStore: CheckpointStore.CheckpointStore["Service"] = {
56 isGitRepository: () => Effect.succeed(true),
57 captureCheckpoint: () => Effect.void,
58 hasCheckpointRef: () => Effect.succeed(true),
59 restoreCheckpoint: () => Effect.succeed(true),
60 diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) =>
61 Effect.sync(() => {
62 diffCheckpointsCalls.push({
63 fromCheckpointRef,
64 toCheckpointRef,
65 cwd,
66 ignoreWhitespace,
67 });
68 return "full thread diff patch";
69 }),
70 deleteCheckpointRefs: () => Effect.void,
71 };
72
73 const layer = CheckpointDiffQuery.layer.pipe(
74 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
75 Layer.provideMerge(
76 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
77 getCommandReadModel: () =>
78 Effect.die("CheckpointDiffQuery should not request the command read model"),
79 getSnapshot: () =>
80 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
81 getShellSnapshot: () =>
82 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
83 getArchivedShellSnapshot: () =>
84 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
85 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
86 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
87 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
88 getProjectShellById: () => Effect.succeed(Option.none()),
89 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
90 getThreadCheckpointContext: () =>
91 Effect.sync(() => {
92 getThreadCheckpointContextCalls += 1;
93 return Option.none();
94 }),
95 getFullThreadDiffContext: () =>
96 Effect.sync(() => {
97 getFullThreadDiffContextCalls += 1;
98 return Option.some({
99 threadId,
100 projectId,
101 workspaceRoot: "/tmp/workspace",
102 worktreePath: "/tmp/worktree",
103 latestCheckpointTurnCount: 4,
104 toCheckpointRef,
105 });
106 }),
107 getThreadShellById: () => Effect.succeed(Option.none()),
108 getThreadDetailById: () => Effect.succeed(Option.none()),
109 }),
110 ),
111 );
112
113 const result = yield* Effect.gen(function* () {
114 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
115 return yield* query.getFullThreadDiff({
116 threadId,
117 toTurnCount: 4,
118 ignoreWhitespace: true,
119 });
120 }).pipe(Effect.provide(layer));
121
122 expect(getThreadCheckpointContextCalls).toBe(0);
123 expect(getFullThreadDiffContextCalls).toBe(1);
124 expect(diffCheckpointsCalls).toEqual([
125 {
126 cwd: "/tmp/worktree",
127 fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0),
128 toCheckpointRef,
129 ignoreWhitespace: true,
130 },
131 ]);
132 expect(result).toEqual({
133 threadId,
134 fromTurnCount: 0,
135 toTurnCount: 4,
136 diff: "full thread diff patch",
137 });
138 }),
139 );
140
141 it.effect("computes diffs using canonical turn-0 checkpoint refs", () =>
142 Effect.gen(function* () {
143 const projectId = ProjectId.make("project-1");
144 const threadId = ThreadId.make("thread-1");
145 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
146 const diffCheckpointsCalls: Array<{
147 readonly fromCheckpointRef: CheckpointRef;
148 readonly toCheckpointRef: CheckpointRef;
149 readonly cwd: string;
150 readonly ignoreWhitespace: boolean;
151 }> = [];
152
153 const threadCheckpointContext = makeThreadCheckpointContext({
154 projectId,
155 threadId,
156 workspaceRoot: "/tmp/workspace",
157 worktreePath: null,
158 checkpointTurnCount: 1,
159 checkpointRef: toCheckpointRef,
160 });
161
162 const checkpointStore: CheckpointStore.CheckpointStore["Service"] = {
163 isGitRepository: () => Effect.succeed(true),
164 captureCheckpoint: () => Effect.void,
165 hasCheckpointRef: () => Effect.succeed(true),
166 restoreCheckpoint: () => Effect.succeed(true),
167 diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) =>
168 Effect.sync(() => {
169 diffCheckpointsCalls.push({
170 fromCheckpointRef,
171 toCheckpointRef,
172 cwd,
173 ignoreWhitespace,
174 });
175 return "diff patch";
176 }),
177 deleteCheckpointRefs: () => Effect.void,
178 };
179
180 const layer = CheckpointDiffQuery.layer.pipe(
181 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
182 Layer.provideMerge(
183 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
184 getCommandReadModel: () =>
185 Effect.die("CheckpointDiffQuery should not request the command read model"),
186 getSnapshot: () =>
187 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
188 getShellSnapshot: () =>
189 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
190 getArchivedShellSnapshot: () =>
191 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
192 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
193 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
194 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
195 getProjectShellById: () => Effect.succeed(Option.none()),
196 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
197 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
198 getFullThreadDiffContext: () => Effect.die("unused"),
199 getThreadShellById: () => Effect.succeed(Option.none()),
200 getThreadDetailById: () => Effect.succeed(Option.none()),
201 }),
202 ),
203 );
204
205 const result = yield* Effect.gen(function* () {
206 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
207 return yield* query.getTurnDiff({
208 threadId,
209 fromTurnCount: 0,
210 toTurnCount: 1,
211 ignoreWhitespace: true,
212 });
213 }).pipe(Effect.provide(layer));
214
215 const expectedFromRef = checkpointRefForThreadTurn(threadId, 0);
216 expect(diffCheckpointsCalls).toEqual([
217 {
218 cwd: "/tmp/workspace",
219 fromCheckpointRef: expectedFromRef,
220 toCheckpointRef,
221 ignoreWhitespace: true,
222 },
223 ]);
224 expect(result).toEqual({
225 threadId,
226 fromTurnCount: 0,
227 toTurnCount: 1,
228 diff: "diff patch",
229 });
230 }),
231 );
232
233 it.effect("defaults to hide whitespace changes", () =>
234 Effect.gen(function* () {
235 const projectId = ProjectId.make("project-default-whitespace");
236 const threadId = ThreadId.make("thread-default-whitespace");
237 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
238 const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = [];
239
240 const threadCheckpointContext = makeThreadCheckpointContext({
241 projectId,
242 threadId,
243 workspaceRoot: "/tmp/workspace",
244 worktreePath: null,
245 checkpointTurnCount: 1,
246 checkpointRef: toCheckpointRef,
247 });
248
249 const checkpointStore: CheckpointStore.CheckpointStore["Service"] = {
250 isGitRepository: () => Effect.succeed(true),
251 captureCheckpoint: () => Effect.void,
252 hasCheckpointRef: () => Effect.succeed(true),
253 restoreCheckpoint: () => Effect.succeed(true),
254 diffCheckpoints: ({ ignoreWhitespace }) =>
255 Effect.sync(() => {
256 diffCheckpointsCalls.push({ ignoreWhitespace });
257 return "diff patch";
258 }),
259 deleteCheckpointRefs: () => Effect.void,
260 };
261
262 const layer = CheckpointDiffQuery.layer.pipe(
263 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
264 Layer.provideMerge(
265 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
266 getCommandReadModel: () =>
267 Effect.die("CheckpointDiffQuery should not request the command read model"),
268 getSnapshot: () =>
269 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
270 getShellSnapshot: () =>
271 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
272 getArchivedShellSnapshot: () =>
273 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
274 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
275 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
276 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
277 getProjectShellById: () => Effect.succeed(Option.none()),
278 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
279 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
280 getFullThreadDiffContext: () => Effect.die("unused"),
281 getThreadShellById: () => Effect.succeed(Option.none()),
282 getThreadDetailById: () => Effect.succeed(Option.none()),
283 }),
284 ),
285 );
286
287 yield* Effect.gen(function* () {
288 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
289 return yield* query.getTurnDiff({
290 threadId,
291 fromTurnCount: 0,
292 toTurnCount: 1,
293 });
294 }).pipe(Effect.provide(layer));
295
296 expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]);
297 }),
298 );
299
300 it.effect("does not preflight checkpoint refs before diffing", () =>
301 Effect.gen(function* () {
302 const projectId = ProjectId.make("project-no-preflight");
303 const threadId = ThreadId.make("thread-no-preflight");
304 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
305 let hasCheckpointRefCallCount = 0;
306
307 const threadCheckpointContext = makeThreadCheckpointContext({
308 projectId,
309 threadId,
310 workspaceRoot: "/tmp/workspace",
311 worktreePath: null,
312 checkpointTurnCount: 1,
313 checkpointRef: toCheckpointRef,
314 });
315
316 const checkpointStore: CheckpointStore.CheckpointStore["Service"] = {
317 isGitRepository: () => Effect.succeed(true),
318 captureCheckpoint: () => Effect.void,
319 hasCheckpointRef: () =>
320 Effect.sync(() => {
321 hasCheckpointRefCallCount += 1;
322 return true;
323 }),
324 restoreCheckpoint: () => Effect.succeed(true),
325 diffCheckpoints: () => Effect.succeed("diff patch"),
326 deleteCheckpointRefs: () => Effect.void,
327 };
328
329 const layer = CheckpointDiffQuery.layer.pipe(
330 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
331 Layer.provideMerge(
332 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
333 getCommandReadModel: () =>
334 Effect.die("CheckpointDiffQuery should not request the command read model"),
335 getSnapshot: () =>
336 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
337 getShellSnapshot: () =>
338 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
339 getArchivedShellSnapshot: () =>
340 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
341 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
342 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
343 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
344 getProjectShellById: () => Effect.succeed(Option.none()),
345 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
346 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
347 getFullThreadDiffContext: () => Effect.die("unused"),
348 getThreadShellById: () => Effect.succeed(Option.none()),
349 getThreadDetailById: () => Effect.succeed(Option.none()),
350 }),
351 ),
352 );
353
354 yield* Effect.gen(function* () {
355 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
356 return yield* query.getTurnDiff({
357 threadId,
358 fromTurnCount: 0,
359 toTurnCount: 1,
360 ignoreWhitespace: true,
361 });
362 }).pipe(Effect.provide(layer));
363
364 expect(hasCheckpointRefCallCount).toBe(0);
365 }),
366 );
367
368 it.effect("fails when the thread is missing from the snapshot", () =>
369 Effect.gen(function* () {
370 const threadId = ThreadId.make("thread-missing");
371
372 const checkpointStore: CheckpointStore.CheckpointStore["Service"] = {
373 isGitRepository: () => Effect.succeed(true),
374 captureCheckpoint: () => Effect.void,
375 hasCheckpointRef: () => Effect.succeed(true),
376 restoreCheckpoint: () => Effect.succeed(true),
377 diffCheckpoints: () => Effect.succeed(""),
378 deleteCheckpointRefs: () => Effect.void,
379 };
380
381 const layer = CheckpointDiffQuery.layer.pipe(
382 Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)),
383 Layer.provideMerge(
384 Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
385 getCommandReadModel: () =>
386 Effect.die("CheckpointDiffQuery should not request the command read model"),
387 getSnapshot: () =>
388 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
389 getShellSnapshot: () =>
390 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
391 getArchivedShellSnapshot: () =>
392 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
393 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
394 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
395 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
396 getProjectShellById: () => Effect.succeed(Option.none()),
397 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
398 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
399 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
400 getThreadShellById: () => Effect.succeed(Option.none()),
401 getThreadDetailById: () => Effect.succeed(Option.none()),
402 }),
403 ),
404 );
405
406 const error = yield* Effect.gen(function* () {
407 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
408 return yield* query.getTurnDiff({
409 threadId,
410 fromTurnCount: 0,
411 toTurnCount: 1,
412 });
413 }).pipe(Effect.provide(layer), Effect.flip);
414
415 expect(error.message).toContain("Thread 'thread-missing' not found.");
416 }),
417 );
418});
apps/server/src/checkpointing/CheckpointDiffQuery.ts created+280
......@@ -0,0 +1,280 @@
1/**
2 * CheckpointDiffQuery - Query interface for computed checkpoint diffs.
3 *
4 * Provides read-only diff operations across checkpoint snapshots used by
5 * orchestration APIs.
6 *
7 * @module CheckpointDiffQuery
8 */
9import {
10 type CheckpointRef,
11 OrchestrationGetTurnDiffResult,
12 type OrchestrationGetFullThreadDiffInput,
13 type OrchestrationGetFullThreadDiffResult,
14 type OrchestrationGetTurnDiffInput,
15 type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType,
16 type ThreadId,
17} from "@t3tools/contracts";
18import * as Context from "effect/Context";
19import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";
21import * as Option from "effect/Option";
22import * as Schema from "effect/Schema";
23
24import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
25import { CheckpointInvariantError, CheckpointUnavailableError } from "./Errors.ts";
26import type { CheckpointServiceError } from "./Errors.ts";
27import { checkpointRefForThreadTurn } from "./Utils.ts";
28import * as CheckpointStore from "./CheckpointStore.ts";
29
30/** Service tag for checkpoint diff queries. */
31export class CheckpointDiffQuery extends Context.Service<
32 CheckpointDiffQuery,
33 {
34 /**
35 * Read the patch diff for a single turn checkpoint transition.
36 *
37 * Verifies checkpoint availability in both projection state and filesystem.
38 */
39 readonly getTurnDiff: (
40 input: OrchestrationGetTurnDiffInput,
41 ) => Effect.Effect<OrchestrationGetTurnDiffResultType, CheckpointServiceError>;
42
43 /**
44 * Read the full patch diff across a thread range of checkpoints.
45 *
46 * Uses turn-diff semantics with `fromTurnCount = 0`.
47 */
48 readonly getFullThreadDiff: (
49 input: OrchestrationGetFullThreadDiffInput,
50 ) => Effect.Effect<OrchestrationGetFullThreadDiffResult, CheckpointServiceError>;
51 }
52>()("t3/checkpointing/CheckpointDiffQuery") {}
53
54const isTurnDiffResult = Schema.is(OrchestrationGetTurnDiffResult);
55
56function buildTurnDiffResult(
57 input: {
58 readonly threadId: ThreadId;
59 readonly fromTurnCount: number;
60 readonly toTurnCount: number;
61 },
62 diff: string,
63): OrchestrationGetTurnDiffResultType {
64 return {
65 threadId: input.threadId,
66 fromTurnCount: input.fromTurnCount,
67 toTurnCount: input.toTurnCount,
68 diff,
69 };
70}
71
72export const make = Effect.gen(function* () {
73 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
74 const checkpointStore = yield* CheckpointStore.CheckpointStore;
75
76 const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")(
77 function* (input) {
78 const operation = "CheckpointDiffQuery.getTurnDiff";
79 const ignoreWhitespace = input.ignoreWhitespace ?? true;
80 yield* Effect.annotateCurrentSpan({
81 "checkpoint.thread_id": input.threadId,
82 "checkpoint.from_turn_count": input.fromTurnCount,
83 "checkpoint.to_turn_count": input.toTurnCount,
84 "checkpoint.ignore_whitespace": ignoreWhitespace,
85 });
86
87 if (input.fromTurnCount === input.toTurnCount) {
88 const emptyDiff: OrchestrationGetTurnDiffResultType = {
89 threadId: input.threadId,
90 fromTurnCount: input.fromTurnCount,
91 toTurnCount: input.toTurnCount,
92 diff: "",
93 };
94 if (!isTurnDiffResult(emptyDiff)) {
95 return yield* new CheckpointInvariantError({
96 operation,
97 detail: "Computed turn diff result does not satisfy contract schema.",
98 });
99 }
100 return emptyDiff;
101 }
102
103 const threadContext = yield* projectionSnapshotQuery
104 .getThreadCheckpointContext(input.threadId)
105 .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext"));
106 if (Option.isNone(threadContext)) {
107 return yield* new CheckpointInvariantError({
108 operation,
109 detail: `Thread '${input.threadId}' not found.`,
110 });
111 }
112
113 const maxTurnCount = threadContext.value.checkpoints.reduce(
114 (max, checkpoint) => Math.max(max, checkpoint.checkpointTurnCount),
115 0,
116 );
117 if (input.toTurnCount > maxTurnCount) {
118 return yield* new CheckpointUnavailableError({
119 threadId: input.threadId,
120 turnCount: input.toTurnCount,
121 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`,
122 });
123 }
124
125 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
126 if (!workspaceCwd) {
127 return yield* new CheckpointInvariantError({
128 operation,
129 detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`,
130 });
131 }
132
133 const fromCheckpointRef =
134 input.fromTurnCount === 0
135 ? checkpointRefForThreadTurn(input.threadId, 0)
136 : threadContext.value.checkpoints.find(
137 (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount,
138 )?.checkpointRef;
139 if (!fromCheckpointRef) {
140 return yield* new CheckpointUnavailableError({
141 threadId: input.threadId,
142 turnCount: input.fromTurnCount,
143 detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`,
144 });
145 }
146
147 const toCheckpointRef = threadContext.value.checkpoints.find(
148 (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount,
149 )?.checkpointRef;
150 if (!toCheckpointRef) {
151 return yield* new CheckpointUnavailableError({
152 threadId: input.threadId,
153 turnCount: input.toTurnCount,
154 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
155 });
156 }
157
158 const diff = yield* checkpointStore
159 .diffCheckpoints({
160 cwd: workspaceCwd,
161 fromCheckpointRef,
162 toCheckpointRef,
163 fallbackFromToHead: false,
164 ignoreWhitespace,
165 })
166 .pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints"));
167
168 const turnDiff = buildTurnDiffResult(input, diff);
169 if (!isTurnDiffResult(turnDiff)) {
170 return yield* new CheckpointInvariantError({
171 operation,
172 detail: "Computed turn diff result does not satisfy contract schema.",
173 });
174 }
175
176 return turnDiff;
177 },
178 );
179
180 const getFullThreadDiff: CheckpointDiffQuery["Service"]["getFullThreadDiff"] = Effect.fn(
181 "CheckpointDiffQuery.getFullThreadDiff",
182 )(function* (input) {
183 const operation = "CheckpointDiffQuery.getFullThreadDiff";
184 const ignoreWhitespace = input.ignoreWhitespace ?? true;
185 yield* Effect.annotateCurrentSpan({
186 "checkpoint.thread_id": input.threadId,
187 "checkpoint.from_turn_count": 0,
188 "checkpoint.to_turn_count": input.toTurnCount,
189 "checkpoint.ignore_whitespace": ignoreWhitespace,
190 "checkpoint.diff_kind": "full-thread",
191 });
192
193 if (input.toTurnCount === 0) {
194 const emptyDiff = buildTurnDiffResult(
195 {
196 threadId: input.threadId,
197 fromTurnCount: 0,
198 toTurnCount: 0,
199 },
200 "",
201 );
202 if (!isTurnDiffResult(emptyDiff)) {
203 return yield* new CheckpointInvariantError({
204 operation,
205 detail: "Computed full thread diff result does not satisfy contract schema.",
206 });
207 }
208 return emptyDiff satisfies OrchestrationGetFullThreadDiffResult;
209 }
210
211 const threadContext = yield* projectionSnapshotQuery
212 .getFullThreadDiffContext(input.threadId, input.toTurnCount)
213 .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext"));
214
215 if (Option.isNone(threadContext)) {
216 return yield* new CheckpointInvariantError({
217 operation,
218 detail: `Thread '${input.threadId}' not found.`,
219 });
220 }
221
222 if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) {
223 return yield* new CheckpointUnavailableError({
224 threadId: input.threadId,
225 turnCount: input.toTurnCount,
226 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`,
227 });
228 }
229
230 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
231 if (!workspaceCwd) {
232 return yield* new CheckpointInvariantError({
233 operation,
234 detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`,
235 });
236 }
237
238 if (!threadContext.value.toCheckpointRef) {
239 return yield* new CheckpointUnavailableError({
240 threadId: input.threadId,
241 turnCount: input.toTurnCount,
242 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
243 });
244 }
245
246 const diff = yield* checkpointStore
247 .diffCheckpoints({
248 cwd: workspaceCwd,
249 fromCheckpointRef: checkpointRefForThreadTurn(input.threadId, 0),
250 toCheckpointRef: threadContext.value.toCheckpointRef as CheckpointRef,
251 fallbackFromToHead: false,
252 ignoreWhitespace,
253 })
254 .pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints"));
255
256 const turnDiff = buildTurnDiffResult(
257 {
258 threadId: input.threadId,
259 fromTurnCount: 0,
260 toTurnCount: input.toTurnCount,
261 },
262 diff,
263 );
264 if (!isTurnDiffResult(turnDiff)) {
265 return yield* new CheckpointInvariantError({
266 operation,
267 detail: "Computed full thread diff result does not satisfy contract schema.",
268 });
269 }
270
271 return turnDiff satisfies OrchestrationGetFullThreadDiffResult;
272 });
273
274 return CheckpointDiffQuery.of({
275 getTurnDiff,
276 getFullThreadDiff,
277 });
278});
279
280export const layer = Layer.effect(CheckpointDiffQuery, make);
apps/server/src/checkpointing/CheckpointStore.test.ts created+204
......@@ -0,0 +1,204 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import path from "node:path";
3
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { it } from "@effect/vitest";
6import { ThreadId, type VcsError } from "@t3tools/contracts";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as PlatformError from "effect/PlatformError";
11import * as Scope from "effect/Scope";
12import { describe, expect } from "vite-plus/test";
13
14import { checkpointRefForThreadTurn } from "./Utils.ts";
15import * as CheckpointStore from "./CheckpointStore.ts";
16import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
17import * as VcsProcess from "../vcs/VcsProcess.ts";
18import * as ServerConfig from "../config.ts";
19
20const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
21 prefix: "t3-checkpoint-store-test-",
22});
23const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
24const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));
25const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(
26 Layer.provideMerge(VcsDriverTestLayer),
27 Layer.provideMerge(NodeServices.layer),
28);
29const TestLayer = CheckpointStoreTestLayer.pipe(
30 Layer.provideMerge(VcsProcessTestLayer),
31 Layer.provideMerge(VcsDriverTestLayer),
32 Layer.provideMerge(ServerConfigLayer),
33 Layer.provideMerge(NodeServices.layer),
34);
35
36function makeTmpDir(
37 prefix = "checkpoint-store-test-",
38): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> {
39 return Effect.gen(function* () {
40 const fileSystem = yield* FileSystem.FileSystem;
41 return yield* fileSystem.makeTempDirectoryScoped({ prefix });
42 });
43}
44
45function writeTextFile(
46 filePath: string,
47 contents: string,
48): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
49 return Effect.gen(function* () {
50 const fileSystem = yield* FileSystem.FileSystem;
51 yield* fileSystem.writeFileString(filePath, contents);
52 });
53}
54
55function git(
56 cwd: string,
57 args: ReadonlyArray<string>,
58): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> {
59 return Effect.gen(function* () {
60 const process = yield* VcsProcess.VcsProcess;
61 const result = yield* process.run({
62 operation: "CheckpointStore.test.git",
63 command: "git",
64 cwd,
65 args,
66 timeoutMs: 10_000,
67 });
68 return result.stdout.trim();
69 });
70}
71
72function initRepoWithCommit(
73 cwd: string,
74): Effect.Effect<
75 void,
76 VcsError | PlatformError.PlatformError,
77 VcsProcess.VcsProcess | FileSystem.FileSystem
78> {
79 return Effect.gen(function* () {
80 yield* git(cwd, ["init"]);
81 yield* git(cwd, ["config", "user.email", "test@test.com"]);
82 yield* git(cwd, ["config", "user.name", "Test"]);
83 yield* writeTextFile(path.join(cwd, "README.md"), "# test\n");
84 yield* git(cwd, ["add", "."]);
85 yield* git(cwd, ["commit", "-m", "initial commit"]);
86 });
87}
88
89function buildLargeText(lineCount = 5_000): string {
90 return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
91 .join("\n")
92 .concat("\n");
93}
94
95it.layer(TestLayer)("CheckpointStore.layer", (it) => {
96 describe("diffCheckpoints", () => {
97 it.effect("returns full oversized checkpoint diffs without truncation", () =>
98 Effect.gen(function* () {
99 const tmp = yield* makeTmpDir();
100 yield* initRepoWithCommit(tmp);
101 const checkpointStore = yield* CheckpointStore.CheckpointStore;
102 const threadId = ThreadId.make("thread-checkpoint-store");
103 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
104 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
105
106 yield* checkpointStore.captureCheckpoint({
107 cwd: tmp,
108 checkpointRef: fromCheckpointRef,
109 });
110 yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText());
111 yield* checkpointStore.captureCheckpoint({
112 cwd: tmp,
113 checkpointRef: toCheckpointRef,
114 });
115
116 const diff = yield* checkpointStore.diffCheckpoints({
117 cwd: tmp,
118 fromCheckpointRef,
119 toCheckpointRef,
120 ignoreWhitespace: true,
121 });
122
123 expect(diff).toContain("diff --git");
124 expect(diff).not.toContain("[truncated]");
125 expect(diff).toContain("+line 04999");
126 }),
127 );
128
129 it.effect("can hide indentation churn when changes wrap existing lines", () =>
130 Effect.gen(function* () {
131 const tmp = yield* makeTmpDir();
132 yield* initRepoWithCommit(tmp);
133 const checkpointStore = yield* CheckpointStore.CheckpointStore;
134 const threadId = ThreadId.make("thread-checkpoint-store-whitespace");
135 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
136 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
137
138 const componentPath = path.join(tmp, "Component.tsx");
139 yield* writeTextFile(
140 componentPath,
141 [
142 "export function View() {",
143 " return (",
144 " <section>",
145 " <h1>Title</h1>",
146 " <p>Body</p>",
147 " </section>",
148 " );",
149 "}",
150 "",
151 ].join("\n"),
152 );
153 yield* checkpointStore.captureCheckpoint({
154 cwd: tmp,
155 checkpointRef: fromCheckpointRef,
156 });
157 yield* writeTextFile(
158 componentPath,
159 [
160 "export function View() {",
161 " return (",
162 " <section>",
163 " {isReady ? (",
164 " <div>",
165 " <h1>Title</h1>",
166 " <p>Body</p>",
167 " </div>",
168 " ) : null}",
169 " </section>",
170 " );",
171 "}",
172 "",
173 ].join("\n"),
174 );
175 yield* checkpointStore.captureCheckpoint({
176 cwd: tmp,
177 checkpointRef: toCheckpointRef,
178 });
179
180 const normalDiff = yield* checkpointStore.diffCheckpoints({
181 cwd: tmp,
182 fromCheckpointRef,
183 toCheckpointRef,
184 ignoreWhitespace: false,
185 });
186 const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({
187 cwd: tmp,
188 fromCheckpointRef,
189 toCheckpointRef,
190 ignoreWhitespace: true,
191 });
192
193 expect(normalDiff).toContain("diff --git");
194 expect(normalDiff).toContain("- <h1>Title</h1>");
195 expect(normalDiff).toContain("+ <h1>Title</h1>");
196 expect(whitespaceIgnoredDiff).toContain("diff --git");
197 expect(whitespaceIgnoredDiff).toContain("+ {isReady ? (");
198 expect(whitespaceIgnoredDiff).toContain("+ <div>");
199 expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>");
200 expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>");
201 }),
202 );
203 });
204});
apps/server/src/checkpointing/CheckpointStore.ts created+171
......@@ -0,0 +1,171 @@
1/**
2 * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints.
3 *
4 * Owns hidden Git-ref checkpoint capture/restore and diff computation for a
5 * workspace thread timeline. It does not store user-facing checkpoint metadata
6 * and does not coordinate provider conversation rollback.
7 *
8 * The live adapter resolves the active VCS driver once per checkpoint operation
9 * and delegates to the driver's optional checkpoint capability.
10 *
11 * Uses Effect `Context.Service` for dependency injection and exposes typed
12 * domain errors for checkpoint storage operations.
13 *
14 * @module CheckpointStore
15 */
16import { VcsUnsupportedOperationError, type CheckpointRef } from "@t3tools/contracts";
17import * as Context from "effect/Context";
18import * as Effect from "effect/Effect";
19import * as Layer from "effect/Layer";
20
21import type { CheckpointStoreError } from "./Errors.ts";
22import type { VcsCheckpointOps } from "../vcs/VcsDriver.ts";
23import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
24
25export interface CaptureCheckpointInput {
26 readonly cwd: string;
27 readonly checkpointRef: CheckpointRef;
28}
29
30export interface RestoreCheckpointInput {
31 readonly cwd: string;
32 readonly checkpointRef: CheckpointRef;
33 readonly fallbackToHead?: boolean;
34}
35
36export interface DiffCheckpointsInput {
37 readonly cwd: string;
38 readonly fromCheckpointRef: CheckpointRef;
39 readonly toCheckpointRef: CheckpointRef;
40 readonly fallbackFromToHead?: boolean;
41 readonly ignoreWhitespace: boolean;
42}
43
44export interface DeleteCheckpointRefsInput {
45 readonly cwd: string;
46 readonly checkpointRefs: ReadonlyArray<CheckpointRef>;
47}
48
49/** Service tag for checkpoint persistence and restore operations. */
50export class CheckpointStore extends Context.Service<
51 CheckpointStore,
52 {
53 /** Check whether cwd is inside a Git worktree. */
54 readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>;
55
56 /**
57 * Capture a checkpoint commit and store it at the provided checkpoint ref.
58 *
59 * Uses an isolated temporary Git index and writes a hidden ref.
60 */
61 readonly captureCheckpoint: (
62 input: CaptureCheckpointInput,
63 ) => Effect.Effect<void, CheckpointStoreError>;
64
65 /** Check whether a checkpoint ref exists. */
66 readonly hasCheckpointRef: (
67 input: Omit<RestoreCheckpointInput, "fallbackToHead">,
68 ) => Effect.Effect<boolean, CheckpointStoreError>;
69
70 /**
71 * Restore workspace and staging state to a checkpoint.
72 *
73 * Optionally falls back to current `HEAD` when the checkpoint ref is missing.
74 */
75 readonly restoreCheckpoint: (
76 input: RestoreCheckpointInput,
77 ) => Effect.Effect<boolean, CheckpointStoreError>;
78
79 /**
80 * Compute a patch diff between two checkpoint refs.
81 *
82 * Can optionally treat a missing "from" ref as `HEAD`.
83 */
84 readonly diffCheckpoints: (
85 input: DiffCheckpointsInput,
86 ) => Effect.Effect<string, CheckpointStoreError>;
87
88 /**
89 * Delete the provided checkpoint refs.
90 *
91 * Best-effort delete: missing refs are tolerated.
92 */
93 readonly deleteCheckpointRefs: (
94 input: DeleteCheckpointRefsInput,
95 ) => Effect.Effect<void, CheckpointStoreError>;
96 }
97>()("t3/checkpointing/CheckpointStore") {}
98
99export const make = Effect.gen(function* () {
100 const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry;
101
102 const resolveCheckpoints = Effect.fn("CheckpointStore.resolveCheckpoints")(function* (
103 operation: string,
104 cwd: string,
105 ) {
106 const handle = yield* vcsRegistry.resolve({ cwd });
107 if (!handle.driver.checkpoints) {
108 return yield* new VcsUnsupportedOperationError({
109 operation,
110 kind: handle.kind,
111 detail: `${handle.kind} driver does not implement checkpoint operations.`,
112 });
113 }
114 return handle.driver.checkpoints satisfies VcsCheckpointOps;
115 });
116
117 const isGitRepository: CheckpointStore["Service"]["isGitRepository"] = (cwd) =>
118 vcsRegistry.resolve({ cwd, requestedKind: "git" }).pipe(
119 Effect.map(() => true),
120 Effect.orElseSucceed(() => false),
121 );
122
123 const captureCheckpoint: CheckpointStore["Service"]["captureCheckpoint"] = Effect.fn(
124 "captureCheckpoint",
125 )(function* (input) {
126 const checkpoints = yield* resolveCheckpoints("CheckpointStore.captureCheckpoint", input.cwd);
127 return yield* checkpoints.captureCheckpoint(input);
128 });
129
130 const hasCheckpointRef: CheckpointStore["Service"]["hasCheckpointRef"] = Effect.fn(
131 "hasCheckpointRef",
132 )(function* (input) {
133 const checkpoints = yield* resolveCheckpoints("CheckpointStore.hasCheckpointRef", input.cwd);
134 return yield* checkpoints.hasCheckpointRef(input);
135 });
136
137 const restoreCheckpoint: CheckpointStore["Service"]["restoreCheckpoint"] = Effect.fn(
138 "restoreCheckpoint",
139 )(function* (input) {
140 const checkpoints = yield* resolveCheckpoints("CheckpointStore.restoreCheckpoint", input.cwd);
141 return yield* checkpoints.restoreCheckpoint(input);
142 });
143
144 const diffCheckpoints: CheckpointStore["Service"]["diffCheckpoints"] = Effect.fn(
145 "diffCheckpoints",
146 )(function* (input) {
147 const checkpoints = yield* resolveCheckpoints("CheckpointStore.diffCheckpoints", input.cwd);
148 return yield* checkpoints.diffCheckpoints(input);
149 });
150
151 const deleteCheckpointRefs: CheckpointStore["Service"]["deleteCheckpointRefs"] = Effect.fn(
152 "deleteCheckpointRefs",
153 )(function* (input) {
154 const checkpoints = yield* resolveCheckpoints(
155 "CheckpointStore.deleteCheckpointRefs",
156 input.cwd,
157 );
158 return yield* checkpoints.deleteCheckpointRefs(input);
159 });
160
161 return CheckpointStore.of({
162 isGitRepository,
163 captureCheckpoint,
164 hasCheckpointRef,
165 restoreCheckpoint,
166 diffCheckpoints,
167 deleteCheckpointRefs,
168 });
169});
170
171export const layer = Layer.effect(CheckpointStore, make);
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts deleted-421
......@@ -1,421 +0,0 @@
1import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import { describe, expect, it } from "vite-plus/test";
6
7import {
8 ProjectionSnapshotQuery,
9 type ProjectionThreadCheckpointContext,
10} from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
11import { checkpointRefForThreadTurn } from "../Utils.ts";
12import { CheckpointDiffQueryLive } from "./CheckpointDiffQuery.ts";
13import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts";
14import { CheckpointDiffQuery } from "../Services/CheckpointDiffQuery.ts";
15
16function makeThreadCheckpointContext(input: {
17 readonly projectId: ProjectId;
18 readonly threadId: ThreadId;
19 readonly workspaceRoot: string;
20 readonly worktreePath: string | null;
21 readonly checkpointTurnCount: number;
22 readonly checkpointRef: CheckpointRef;
23}): ProjectionThreadCheckpointContext {
24 return {
25 threadId: input.threadId,
26 projectId: input.projectId,
27 workspaceRoot: input.workspaceRoot,
28 worktreePath: input.worktreePath,
29 checkpoints: [
30 {
31 turnId: TurnId.make("turn-1"),
32 checkpointTurnCount: input.checkpointTurnCount,
33 checkpointRef: input.checkpointRef,
34 status: "ready",
35 files: [],
36 assistantMessageId: null,
37 completedAt: "2026-01-01T00:00:00.000Z",
38 },
39 ],
40 };
41}
42
43describe("CheckpointDiffQueryLive", () => {
44 it("uses the narrow full-thread context lookup for all-turns diffs", async () => {
45 const projectId = ProjectId.make("project-full-thread");
46 const threadId = ThreadId.make("thread-full-thread");
47 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 4);
48 let getThreadCheckpointContextCalls = 0;
49 let getFullThreadDiffContextCalls = 0;
50 const diffCheckpointsCalls: Array<{
51 readonly fromCheckpointRef: CheckpointRef;
52 readonly toCheckpointRef: CheckpointRef;
53 readonly cwd: string;
54 readonly ignoreWhitespace: boolean;
55 }> = [];
56
57 const checkpointStore: CheckpointStoreShape = {
58 isGitRepository: () => Effect.succeed(true),
59 captureCheckpoint: () => Effect.void,
60 hasCheckpointRef: () => Effect.succeed(true),
61 restoreCheckpoint: () => Effect.succeed(true),
62 diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) =>
63 Effect.sync(() => {
64 diffCheckpointsCalls.push({
65 fromCheckpointRef,
66 toCheckpointRef,
67 cwd,
68 ignoreWhitespace,
69 });
70 return "full thread diff patch";
71 }),
72 deleteCheckpointRefs: () => Effect.void,
73 };
74
75 const layer = CheckpointDiffQueryLive.pipe(
76 Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
77 Layer.provideMerge(
78 Layer.succeed(ProjectionSnapshotQuery, {
79 getCommandReadModel: () =>
80 Effect.die("CheckpointDiffQuery should not request the command read model"),
81 getSnapshot: () =>
82 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
83 getShellSnapshot: () =>
84 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
85 getArchivedShellSnapshot: () =>
86 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
87 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
88 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
89 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
90 getProjectShellById: () => Effect.succeed(Option.none()),
91 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
92 getThreadCheckpointContext: () =>
93 Effect.sync(() => {
94 getThreadCheckpointContextCalls += 1;
95 return Option.none();
96 }),
97 getFullThreadDiffContext: () =>
98 Effect.sync(() => {
99 getFullThreadDiffContextCalls += 1;
100 return Option.some({
101 threadId,
102 projectId,
103 workspaceRoot: "/tmp/workspace",
104 worktreePath: "/tmp/worktree",
105 latestCheckpointTurnCount: 4,
106 toCheckpointRef,
107 });
108 }),
109 getThreadShellById: () => Effect.succeed(Option.none()),
110 getThreadDetailById: () => Effect.succeed(Option.none()),
111 }),
112 ),
113 );
114
115 const result = await Effect.runPromise(
116 Effect.gen(function* () {
117 const query = yield* CheckpointDiffQuery;
118 return yield* query.getFullThreadDiff({
119 threadId,
120 toTurnCount: 4,
121 ignoreWhitespace: true,
122 });
123 }).pipe(Effect.provide(layer)),
124 );
125
126 expect(getThreadCheckpointContextCalls).toBe(0);
127 expect(getFullThreadDiffContextCalls).toBe(1);
128 expect(diffCheckpointsCalls).toEqual([
129 {
130 cwd: "/tmp/worktree",
131 fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0),
132 toCheckpointRef,
133 ignoreWhitespace: true,
134 },
135 ]);
136 expect(result).toEqual({
137 threadId,
138 fromTurnCount: 0,
139 toTurnCount: 4,
140 diff: "full thread diff patch",
141 });
142 });
143
144 it("computes diffs using canonical turn-0 checkpoint refs", async () => {
145 const projectId = ProjectId.make("project-1");
146 const threadId = ThreadId.make("thread-1");
147 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
148 const diffCheckpointsCalls: Array<{
149 readonly fromCheckpointRef: CheckpointRef;
150 readonly toCheckpointRef: CheckpointRef;
151 readonly cwd: string;
152 readonly ignoreWhitespace: boolean;
153 }> = [];
154
155 const threadCheckpointContext = makeThreadCheckpointContext({
156 projectId,
157 threadId,
158 workspaceRoot: "/tmp/workspace",
159 worktreePath: null,
160 checkpointTurnCount: 1,
161 checkpointRef: toCheckpointRef,
162 });
163
164 const checkpointStore: CheckpointStoreShape = {
165 isGitRepository: () => Effect.succeed(true),
166 captureCheckpoint: () => Effect.void,
167 hasCheckpointRef: () => Effect.succeed(true),
168 restoreCheckpoint: () => Effect.succeed(true),
169 diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) =>
170 Effect.sync(() => {
171 diffCheckpointsCalls.push({
172 fromCheckpointRef,
173 toCheckpointRef,
174 cwd,
175 ignoreWhitespace,
176 });
177 return "diff patch";
178 }),
179 deleteCheckpointRefs: () => Effect.void,
180 };
181
182 const layer = CheckpointDiffQueryLive.pipe(
183 Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
184 Layer.provideMerge(
185 Layer.succeed(ProjectionSnapshotQuery, {
186 getCommandReadModel: () =>
187 Effect.die("CheckpointDiffQuery should not request the command read model"),
188 getSnapshot: () =>
189 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
190 getShellSnapshot: () =>
191 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
192 getArchivedShellSnapshot: () =>
193 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
194 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
195 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
196 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
197 getProjectShellById: () => Effect.succeed(Option.none()),
198 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
199 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
200 getFullThreadDiffContext: () => Effect.die("unused"),
201 getThreadShellById: () => Effect.succeed(Option.none()),
202 getThreadDetailById: () => Effect.succeed(Option.none()),
203 }),
204 ),
205 );
206
207 const result = await Effect.runPromise(
208 Effect.gen(function* () {
209 const query = yield* CheckpointDiffQuery;
210 return yield* query.getTurnDiff({
211 threadId,
212 fromTurnCount: 0,
213 toTurnCount: 1,
214 ignoreWhitespace: true,
215 });
216 }).pipe(Effect.provide(layer)),
217 );
218
219 const expectedFromRef = checkpointRefForThreadTurn(threadId, 0);
220 expect(diffCheckpointsCalls).toEqual([
221 {
222 cwd: "/tmp/workspace",
223 fromCheckpointRef: expectedFromRef,
224 toCheckpointRef,
225 ignoreWhitespace: true,
226 },
227 ]);
228 expect(result).toEqual({
229 threadId,
230 fromTurnCount: 0,
231 toTurnCount: 1,
232 diff: "diff patch",
233 });
234 });
235
236 it("defaults to hide whitespace changes", async () => {
237 const projectId = ProjectId.make("project-default-whitespace");
238 const threadId = ThreadId.make("thread-default-whitespace");
239 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
240 const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = [];
241
242 const threadCheckpointContext = makeThreadCheckpointContext({
243 projectId,
244 threadId,
245 workspaceRoot: "/tmp/workspace",
246 worktreePath: null,
247 checkpointTurnCount: 1,
248 checkpointRef: toCheckpointRef,
249 });
250
251 const checkpointStore: CheckpointStoreShape = {
252 isGitRepository: () => Effect.succeed(true),
253 captureCheckpoint: () => Effect.void,
254 hasCheckpointRef: () => Effect.succeed(true),
255 restoreCheckpoint: () => Effect.succeed(true),
256 diffCheckpoints: ({ ignoreWhitespace }) =>
257 Effect.sync(() => {
258 diffCheckpointsCalls.push({ ignoreWhitespace });
259 return "diff patch";
260 }),
261 deleteCheckpointRefs: () => Effect.void,
262 };
263
264 const layer = CheckpointDiffQueryLive.pipe(
265 Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
266 Layer.provideMerge(
267 Layer.succeed(ProjectionSnapshotQuery, {
268 getCommandReadModel: () =>
269 Effect.die("CheckpointDiffQuery should not request the command read model"),
270 getSnapshot: () =>
271 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
272 getShellSnapshot: () =>
273 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
274 getArchivedShellSnapshot: () =>
275 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
276 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
277 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
278 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
279 getProjectShellById: () => Effect.succeed(Option.none()),
280 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
281 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
282 getFullThreadDiffContext: () => Effect.die("unused"),
283 getThreadShellById: () => Effect.succeed(Option.none()),
284 getThreadDetailById: () => Effect.succeed(Option.none()),
285 }),
286 ),
287 );
288
289 await Effect.runPromise(
290 Effect.gen(function* () {
291 const query = yield* CheckpointDiffQuery;
292 return yield* query.getTurnDiff({
293 threadId,
294 fromTurnCount: 0,
295 toTurnCount: 1,
296 });
297 }).pipe(Effect.provide(layer)),
298 );
299
300 expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]);
301 });
302
303 it("does not preflight checkpoint refs before diffing", async () => {
304 const projectId = ProjectId.make("project-no-preflight");
305 const threadId = ThreadId.make("thread-no-preflight");
306 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
307 let hasCheckpointRefCallCount = 0;
308
309 const threadCheckpointContext = makeThreadCheckpointContext({
310 projectId,
311 threadId,
312 workspaceRoot: "/tmp/workspace",
313 worktreePath: null,
314 checkpointTurnCount: 1,
315 checkpointRef: toCheckpointRef,
316 });
317
318 const checkpointStore: CheckpointStoreShape = {
319 isGitRepository: () => Effect.succeed(true),
320 captureCheckpoint: () => Effect.void,
321 hasCheckpointRef: () =>
322 Effect.sync(() => {
323 hasCheckpointRefCallCount += 1;
324 return true;
325 }),
326 restoreCheckpoint: () => Effect.succeed(true),
327 diffCheckpoints: () => Effect.succeed("diff patch"),
328 deleteCheckpointRefs: () => Effect.void,
329 };
330
331 const layer = CheckpointDiffQueryLive.pipe(
332 Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
333 Layer.provideMerge(
334 Layer.succeed(ProjectionSnapshotQuery, {
335 getCommandReadModel: () =>
336 Effect.die("CheckpointDiffQuery should not request the command read model"),
337 getSnapshot: () =>
338 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
339 getShellSnapshot: () =>
340 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
341 getArchivedShellSnapshot: () =>
342 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
343 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
344 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
345 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
346 getProjectShellById: () => Effect.succeed(Option.none()),
347 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
348 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
349 getFullThreadDiffContext: () => Effect.die("unused"),
350 getThreadShellById: () => Effect.succeed(Option.none()),
351 getThreadDetailById: () => Effect.succeed(Option.none()),
352 }),
353 ),
354 );
355
356 await Effect.runPromise(
357 Effect.gen(function* () {
358 const query = yield* CheckpointDiffQuery;
359 return yield* query.getTurnDiff({
360 threadId,
361 fromTurnCount: 0,
362 toTurnCount: 1,
363 ignoreWhitespace: true,
364 });
365 }).pipe(Effect.provide(layer)),
366 );
367
368 expect(hasCheckpointRefCallCount).toBe(0);
369 });
370
371 it("fails when the thread is missing from the snapshot", async () => {
372 const threadId = ThreadId.make("thread-missing");
373
374 const checkpointStore: CheckpointStoreShape = {
375 isGitRepository: () => Effect.succeed(true),
376 captureCheckpoint: () => Effect.void,
377 hasCheckpointRef: () => Effect.succeed(true),
378 restoreCheckpoint: () => Effect.succeed(true),
379 diffCheckpoints: () => Effect.succeed(""),
380 deleteCheckpointRefs: () => Effect.void,
381 };
382
383 const layer = CheckpointDiffQueryLive.pipe(
384 Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
385 Layer.provideMerge(
386 Layer.succeed(ProjectionSnapshotQuery, {
387 getCommandReadModel: () =>
388 Effect.die("CheckpointDiffQuery should not request the command read model"),
389 getSnapshot: () =>
390 Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
391 getShellSnapshot: () =>
392 Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
393 getArchivedShellSnapshot: () =>
394 Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
395 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
396 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
397 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
398 getProjectShellById: () => Effect.succeed(Option.none()),
399 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
400 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
401 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
402 getThreadShellById: () => Effect.succeed(Option.none()),
403 getThreadDetailById: () => Effect.succeed(Option.none()),
404 }),
405 ),
406 );
407
408 await expect(
409 Effect.runPromise(
410 Effect.gen(function* () {
411 const query = yield* CheckpointDiffQuery;
412 return yield* query.getTurnDiff({
413 threadId,
414 fromTurnCount: 0,
415 toTurnCount: 1,
416 });
417 }).pipe(Effect.provide(layer)),
418 ),
419 ).rejects.toThrow("Thread 'thread-missing' not found.");
420 });
421});
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts deleted-248
......@@ -1,248 +0,0 @@
1import {
2 type CheckpointRef,
3 OrchestrationGetTurnDiffResult,
4 type ThreadId,
5 type OrchestrationGetFullThreadDiffResult,
6 type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType,
7} from "@t3tools/contracts";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
12
13import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
14import { CheckpointInvariantError, CheckpointUnavailableError } from "../Errors.ts";
15import { checkpointRefForThreadTurn } from "../Utils.ts";
16import { CheckpointStore } from "../Services/CheckpointStore.ts";
17import {
18 CheckpointDiffQuery,
19 type CheckpointDiffQueryShape,
20} from "../Services/CheckpointDiffQuery.ts";
21
22const isTurnDiffResult = Schema.is(OrchestrationGetTurnDiffResult);
23
24function buildTurnDiffResult(
25 input: {
26 readonly threadId: ThreadId;
27 readonly fromTurnCount: number;
28 readonly toTurnCount: number;
29 },
30 diff: string,
31): OrchestrationGetTurnDiffResultType {
32 return {
33 threadId: input.threadId,
34 fromTurnCount: input.fromTurnCount,
35 toTurnCount: input.toTurnCount,
36 diff,
37 };
38}
39
40const make = Effect.gen(function* () {
41 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
42 const checkpointStore = yield* CheckpointStore;
43
44 const getTurnDiff: CheckpointDiffQueryShape["getTurnDiff"] = Effect.fn("getTurnDiff")(
45 function* (input) {
46 const operation = "CheckpointDiffQuery.getTurnDiff";
47 const ignoreWhitespace = input.ignoreWhitespace ?? true;
48 yield* Effect.annotateCurrentSpan({
49 "checkpoint.thread_id": input.threadId,
50 "checkpoint.from_turn_count": input.fromTurnCount,
51 "checkpoint.to_turn_count": input.toTurnCount,
52 "checkpoint.ignore_whitespace": ignoreWhitespace,
53 });
54
55 if (input.fromTurnCount === input.toTurnCount) {
56 const emptyDiff: OrchestrationGetTurnDiffResultType = {
57 threadId: input.threadId,
58 fromTurnCount: input.fromTurnCount,
59 toTurnCount: input.toTurnCount,
60 diff: "",
61 };
62 if (!isTurnDiffResult(emptyDiff)) {
63 return yield* new CheckpointInvariantError({
64 operation,
65 detail: "Computed turn diff result does not satisfy contract schema.",
66 });
67 }
68 return emptyDiff;
69 }
70
71 const threadContext = yield* projectionSnapshotQuery
72 .getThreadCheckpointContext(input.threadId)
73 .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext"));
74 if (Option.isNone(threadContext)) {
75 return yield* new CheckpointInvariantError({
76 operation,
77 detail: `Thread '${input.threadId}' not found.`,
78 });
79 }
80
81 const maxTurnCount = threadContext.value.checkpoints.reduce(
82 (max, checkpoint) => Math.max(max, checkpoint.checkpointTurnCount),
83 0,
84 );
85 if (input.toTurnCount > maxTurnCount) {
86 return yield* new CheckpointUnavailableError({
87 threadId: input.threadId,
88 turnCount: input.toTurnCount,
89 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`,
90 });
91 }
92
93 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
94 if (!workspaceCwd) {
95 return yield* new CheckpointInvariantError({
96 operation,
97 detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`,
98 });
99 }
100
101 const fromCheckpointRef =
102 input.fromTurnCount === 0
103 ? checkpointRefForThreadTurn(input.threadId, 0)
104 : threadContext.value.checkpoints.find(
105 (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount,
106 )?.checkpointRef;
107 if (!fromCheckpointRef) {
108 return yield* new CheckpointUnavailableError({
109 threadId: input.threadId,
110 turnCount: input.fromTurnCount,
111 detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`,
112 });
113 }
114
115 const toCheckpointRef = threadContext.value.checkpoints.find(
116 (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount,
117 )?.checkpointRef;
118 if (!toCheckpointRef) {
119 return yield* new CheckpointUnavailableError({
120 threadId: input.threadId,
121 turnCount: input.toTurnCount,
122 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
123 });
124 }
125
126 const diff = yield* checkpointStore
127 .diffCheckpoints({
128 cwd: workspaceCwd,
129 fromCheckpointRef,
130 toCheckpointRef,
131 fallbackFromToHead: false,
132 ignoreWhitespace,
133 })
134 .pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints"));
135
136 const turnDiff = buildTurnDiffResult(input, diff);
137 if (!isTurnDiffResult(turnDiff)) {
138 return yield* new CheckpointInvariantError({
139 operation,
140 detail: "Computed turn diff result does not satisfy contract schema.",
141 });
142 }
143
144 return turnDiff;
145 },
146 );
147
148 const getFullThreadDiff: CheckpointDiffQueryShape["getFullThreadDiff"] = Effect.fn(
149 "CheckpointDiffQuery.getFullThreadDiff",
150 )(function* (input) {
151 const operation = "CheckpointDiffQuery.getFullThreadDiff";
152 const ignoreWhitespace = input.ignoreWhitespace ?? true;
153 yield* Effect.annotateCurrentSpan({
154 "checkpoint.thread_id": input.threadId,
155 "checkpoint.from_turn_count": 0,
156 "checkpoint.to_turn_count": input.toTurnCount,
157 "checkpoint.ignore_whitespace": ignoreWhitespace,
158 "checkpoint.diff_kind": "full-thread",
159 });
160
161 if (input.toTurnCount === 0) {
162 const emptyDiff = buildTurnDiffResult(
163 {
164 threadId: input.threadId,
165 fromTurnCount: 0,
166 toTurnCount: 0,
167 },
168 "",
169 );
170 if (!isTurnDiffResult(emptyDiff)) {
171 return yield* new CheckpointInvariantError({
172 operation,
173 detail: "Computed full thread diff result does not satisfy contract schema.",
174 });
175 }
176 return emptyDiff satisfies OrchestrationGetFullThreadDiffResult;
177 }
178
179 const threadContext = yield* projectionSnapshotQuery
180 .getFullThreadDiffContext(input.threadId, input.toTurnCount)
181 .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext"));
182
183 if (Option.isNone(threadContext)) {
184 return yield* new CheckpointInvariantError({
185 operation,
186 detail: `Thread '${input.threadId}' not found.`,
187 });
188 }
189
190 if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) {
191 return yield* new CheckpointUnavailableError({
192 threadId: input.threadId,
193 turnCount: input.toTurnCount,
194 detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`,
195 });
196 }
197
198 const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot;
199 if (!workspaceCwd) {
200 return yield* new CheckpointInvariantError({
201 operation,
202 detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`,
203 });
204 }
205
206 if (!threadContext.value.toCheckpointRef) {
207 return yield* new CheckpointUnavailableError({
208 threadId: input.threadId,
209 turnCount: input.toTurnCount,
210 detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`,
211 });
212 }
213
214 const diff = yield* checkpointStore
215 .diffCheckpoints({
216 cwd: workspaceCwd,
217 fromCheckpointRef: checkpointRefForThreadTurn(input.threadId, 0),
218 toCheckpointRef: threadContext.value.toCheckpointRef as CheckpointRef,
219 fallbackFromToHead: false,
220 ignoreWhitespace,
221 })
222 .pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints"));
223
224 const turnDiff = buildTurnDiffResult(
225 {
226 threadId: input.threadId,
227 fromTurnCount: 0,
228 toTurnCount: input.toTurnCount,
229 },
230 diff,
231 );
232 if (!isTurnDiffResult(turnDiff)) {
233 return yield* new CheckpointInvariantError({
234 operation,
235 detail: "Computed full thread diff result does not satisfy contract schema.",
236 });
237 }
238
239 return turnDiff satisfies OrchestrationGetFullThreadDiffResult;
240 });
241
242 return {
243 getTurnDiff,
244 getFullThreadDiff,
245 } satisfies CheckpointDiffQueryShape;
246});
247
248export const CheckpointDiffQueryLive = Layer.effect(CheckpointDiffQuery, make);
apps/server/src/checkpointing/Layers/CheckpointStore.test.ts deleted-206
......@@ -1,206 +0,0 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import path from "node:path";
3
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { it } from "@effect/vitest";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";
10import * as Scope from "effect/Scope";
11import { describe, expect } from "vite-plus/test";
12
13import { checkpointRefForThreadTurn } from "../Utils.ts";
14import { CheckpointStoreLive } from "./CheckpointStore.ts";
15import { CheckpointStore } from "../Services/CheckpointStore.ts";
16import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
17import * as VcsProcess from "../../vcs/VcsProcess.ts";
18import type { VcsError } from "@t3tools/contracts";
19import { ServerConfig } from "../../config.ts";
20import { ThreadId } from "@t3tools/contracts";
21
22const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
23 prefix: "t3-checkpoint-store-test-",
24});
25const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
26const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));
27const CheckpointStoreTestLayer = CheckpointStoreLive.pipe(
28 Layer.provideMerge(VcsDriverTestLayer),
29 Layer.provideMerge(NodeServices.layer),
30);
31const TestLayer = CheckpointStoreTestLayer.pipe(
32 Layer.provideMerge(VcsProcessTestLayer),
33 Layer.provideMerge(VcsDriverTestLayer),
34 Layer.provideMerge(ServerConfigLayer),
35 Layer.provideMerge(NodeServices.layer),
36);
37
38function makeTmpDir(
39 prefix = "checkpoint-store-test-",
40): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> {
41 return Effect.gen(function* () {
42 const fileSystem = yield* FileSystem.FileSystem;
43 return yield* fileSystem.makeTempDirectoryScoped({ prefix });
44 });
45}
46
47function writeTextFile(
48 filePath: string,
49 contents: string,
50): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
51 return Effect.gen(function* () {
52 const fileSystem = yield* FileSystem.FileSystem;
53 yield* fileSystem.writeFileString(filePath, contents);
54 });
55}
56
57function git(
58 cwd: string,
59 args: ReadonlyArray<string>,
60): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> {
61 return Effect.gen(function* () {
62 const process = yield* VcsProcess.VcsProcess;
63 const result = yield* process.run({
64 operation: "CheckpointStore.test.git",
65 command: "git",
66 cwd,
67 args,
68 timeoutMs: 10_000,
69 });
70 return result.stdout.trim();
71 });
72}
73
74function initRepoWithCommit(
75 cwd: string,
76): Effect.Effect<
77 void,
78 VcsError | PlatformError.PlatformError,
79 VcsProcess.VcsProcess | FileSystem.FileSystem
80> {
81 return Effect.gen(function* () {
82 yield* git(cwd, ["init"]);
83 yield* git(cwd, ["config", "user.email", "test@test.com"]);
84 yield* git(cwd, ["config", "user.name", "Test"]);
85 yield* writeTextFile(path.join(cwd, "README.md"), "# test\n");
86 yield* git(cwd, ["add", "."]);
87 yield* git(cwd, ["commit", "-m", "initial commit"]);
88 });
89}
90
91function buildLargeText(lineCount = 5_000): string {
92 return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
93 .join("\n")
94 .concat("\n");
95}
96
97it.layer(TestLayer)("CheckpointStoreLive", (it) => {
98 describe("diffCheckpoints", () => {
99 it.effect("returns full oversized checkpoint diffs without truncation", () =>
100 Effect.gen(function* () {
101 const tmp = yield* makeTmpDir();
102 yield* initRepoWithCommit(tmp);
103 const checkpointStore = yield* CheckpointStore;
104 const threadId = ThreadId.make("thread-checkpoint-store");
105 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
106 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
107
108 yield* checkpointStore.captureCheckpoint({
109 cwd: tmp,
110 checkpointRef: fromCheckpointRef,
111 });
112 yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText());
113 yield* checkpointStore.captureCheckpoint({
114 cwd: tmp,
115 checkpointRef: toCheckpointRef,
116 });
117
118 const diff = yield* checkpointStore.diffCheckpoints({
119 cwd: tmp,
120 fromCheckpointRef,
121 toCheckpointRef,
122 ignoreWhitespace: true,
123 });
124
125 expect(diff).toContain("diff --git");
126 expect(diff).not.toContain("[truncated]");
127 expect(diff).toContain("+line 04999");
128 }),
129 );
130
131 it.effect("can hide indentation churn when changes wrap existing lines", () =>
132 Effect.gen(function* () {
133 const tmp = yield* makeTmpDir();
134 yield* initRepoWithCommit(tmp);
135 const checkpointStore = yield* CheckpointStore;
136 const threadId = ThreadId.make("thread-checkpoint-store-whitespace");
137 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
138 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
139
140 const componentPath = path.join(tmp, "Component.tsx");
141 yield* writeTextFile(
142 componentPath,
143 [
144 "export function View() {",
145 " return (",
146 " <section>",
147 " <h1>Title</h1>",
148 " <p>Body</p>",
149 " </section>",
150 " );",
151 "}",
152 "",
153 ].join("\n"),
154 );
155 yield* checkpointStore.captureCheckpoint({
156 cwd: tmp,
157 checkpointRef: fromCheckpointRef,
158 });
159 yield* writeTextFile(
160 componentPath,
161 [
162 "export function View() {",
163 " return (",
164 " <section>",
165 " {isReady ? (",
166 " <div>",
167 " <h1>Title</h1>",
168 " <p>Body</p>",
169 " </div>",
170 " ) : null}",
171 " </section>",
172 " );",
173 "}",
174 "",
175 ].join("\n"),
176 );
177 yield* checkpointStore.captureCheckpoint({
178 cwd: tmp,
179 checkpointRef: toCheckpointRef,
180 });
181
182 const normalDiff = yield* checkpointStore.diffCheckpoints({
183 cwd: tmp,
184 fromCheckpointRef,
185 toCheckpointRef,
186 ignoreWhitespace: false,
187 });
188 const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({
189 cwd: tmp,
190 fromCheckpointRef,
191 toCheckpointRef,
192 ignoreWhitespace: true,
193 });
194
195 expect(normalDiff).toContain("diff --git");
196 expect(normalDiff).toContain("- <h1>Title</h1>");
197 expect(normalDiff).toContain("+ <h1>Title</h1>");
198 expect(whitespaceIgnoredDiff).toContain("diff --git");
199 expect(whitespaceIgnoredDiff).toContain("+ {isReady ? (");
200 expect(whitespaceIgnoredDiff).toContain("+ <div>");
201 expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>");
202 expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>");
203 }),
204 );
205 });
206});
apps/server/src/checkpointing/Layers/CheckpointStore.ts deleted-89
......@@ -1,89 +0,0 @@
1/**
2 * CheckpointStoreLive - Filesystem checkpoint store adapter layer.
3 *
4 * Resolves the active VCS driver once per checkpoint operation and delegates
5 * checkpoint-specific behavior to the driver's optional checkpoint capability.
6 *
7 * @module CheckpointStoreLive
8 */
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11
12import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts";
13import { VcsUnsupportedOperationError } from "@t3tools/contracts";
14import { VcsDriverRegistry } from "../../vcs/VcsDriverRegistry.ts";
15import type { VcsCheckpointOps } from "../../vcs/VcsDriver.ts";
16
17const makeCheckpointStore = Effect.gen(function* () {
18 const vcsRegistry = yield* VcsDriverRegistry;
19
20 const resolveCheckpoints = Effect.fn("CheckpointStore.resolveCheckpoints")(function* (
21 operation: string,
22 cwd: string,
23 ) {
24 const handle = yield* vcsRegistry.resolve({ cwd });
25 if (!handle.driver.checkpoints) {
26 return yield* new VcsUnsupportedOperationError({
27 operation,
28 kind: handle.kind,
29 detail: `${handle.kind} driver does not implement checkpoint operations.`,
30 });
31 }
32 return handle.driver.checkpoints satisfies VcsCheckpointOps;
33 });
34
35 const isGitRepository: CheckpointStoreShape["isGitRepository"] = (cwd) =>
36 vcsRegistry.resolve({ cwd, requestedKind: "git" }).pipe(
37 Effect.map(() => true),
38 Effect.orElseSucceed(() => false),
39 );
40
41 const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = Effect.fn(
42 "captureCheckpoint",
43 )(function* (input) {
44 const checkpoints = yield* resolveCheckpoints("CheckpointStore.captureCheckpoint", input.cwd);
45 return yield* checkpoints.captureCheckpoint(input);
46 });
47
48 const hasCheckpointRef: CheckpointStoreShape["hasCheckpointRef"] = Effect.fn("hasCheckpointRef")(
49 function* (input) {
50 const checkpoints = yield* resolveCheckpoints("CheckpointStore.hasCheckpointRef", input.cwd);
51 return yield* checkpoints.hasCheckpointRef(input);
52 },
53 );
54
55 const restoreCheckpoint: CheckpointStoreShape["restoreCheckpoint"] = Effect.fn(
56 "restoreCheckpoint",
57 )(function* (input) {
58 const checkpoints = yield* resolveCheckpoints("CheckpointStore.restoreCheckpoint", input.cwd);
59 return yield* checkpoints.restoreCheckpoint(input);
60 });
61
62 const diffCheckpoints: CheckpointStoreShape["diffCheckpoints"] = Effect.fn("diffCheckpoints")(
63 function* (input) {
64 const checkpoints = yield* resolveCheckpoints("CheckpointStore.diffCheckpoints", input.cwd);
65 return yield* checkpoints.diffCheckpoints(input);
66 },
67 );
68
69 const deleteCheckpointRefs: CheckpointStoreShape["deleteCheckpointRefs"] = Effect.fn(
70 "deleteCheckpointRefs",
71 )(function* (input) {
72 const checkpoints = yield* resolveCheckpoints(
73 "CheckpointStore.deleteCheckpointRefs",
74 input.cwd,
75 );
76 return yield* checkpoints.deleteCheckpointRefs(input);
77 });
78
79 return {
80 isGitRepository,
81 captureCheckpoint,
82 hasCheckpointRef,
83 restoreCheckpoint,
84 diffCheckpoints,
85 deleteCheckpointRefs,
86 } satisfies CheckpointStoreShape;
87});
88
89export const CheckpointStoreLive = Layer.effect(CheckpointStore, makeCheckpointStore);
apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts deleted-49
......@@ -1,49 +0,0 @@
1/**
2 * CheckpointDiffQuery - Query interface for computed checkpoint diffs.
3 *
4 * Provides read-only diff operations across checkpoint snapshots used by
5 * orchestration APIs.
6 *
7 * @module CheckpointDiffQuery
8 */
9import type {
10 OrchestrationGetFullThreadDiffInput,
11 OrchestrationGetFullThreadDiffResult,
12 OrchestrationGetTurnDiffInput,
13 OrchestrationGetTurnDiffResult,
14} from "@t3tools/contracts";
15import * as Context from "effect/Context";
16import type * as Effect from "effect/Effect";
17
18import type { CheckpointServiceError } from "../Errors.ts";
19
20/**
21 * CheckpointDiffQueryShape - Service API for checkpoint diff queries.
22 */
23export interface CheckpointDiffQueryShape {
24 /**
25 * Read the patch diff for a single turn checkpoint transition.
26 *
27 * Verifies checkpoint availability in both projection state and filesystem.
28 */
29 readonly getTurnDiff: (
30 input: OrchestrationGetTurnDiffInput,
31 ) => Effect.Effect<OrchestrationGetTurnDiffResult, CheckpointServiceError>;
32
33 /**
34 * Read the full patch diff across a thread range of checkpoints.
35 *
36 * Delegates to turn diff with `fromTurnCount = 0`.
37 */
38 readonly getFullThreadDiff: (
39 input: OrchestrationGetFullThreadDiffInput,
40 ) => Effect.Effect<OrchestrationGetFullThreadDiffResult, CheckpointServiceError>;
41}
42
43/**
44 * CheckpointDiffQuery - Service tag for checkpoint diff queries.
45 */
46export class CheckpointDiffQuery extends Context.Service<
47 CheckpointDiffQuery,
48 CheckpointDiffQueryShape
49>()("t3/checkpointing/Services/CheckpointDiffQuery") {}
apps/server/src/checkpointing/Services/CheckpointStore.ts deleted-101
......@@ -1,101 +0,0 @@
1/**
2 * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints.
3 *
4 * Owns hidden Git-ref checkpoint capture/restore and diff computation for a
5 * workspace thread timeline. It does not store user-facing checkpoint metadata
6 * and does not coordinate provider conversation rollback.
7 *
8 * Uses Effect `Context.Service` for dependency injection and exposes typed
9 * domain errors for checkpoint storage operations.
10 *
11 * @module CheckpointStore
12 */
13import * as Context from "effect/Context";
14import type * as Effect from "effect/Effect";
15
16import type { CheckpointStoreError } from "../Errors.ts";
17import { CheckpointRef } from "@t3tools/contracts";
18
19export interface CaptureCheckpointInput {
20 readonly cwd: string;
21 readonly checkpointRef: CheckpointRef;
22}
23
24export interface RestoreCheckpointInput {
25 readonly cwd: string;
26 readonly checkpointRef: CheckpointRef;
27 readonly fallbackToHead?: boolean;
28}
29
30export interface DiffCheckpointsInput {
31 readonly cwd: string;
32 readonly fromCheckpointRef: CheckpointRef;
33 readonly toCheckpointRef: CheckpointRef;
34 readonly fallbackFromToHead?: boolean;
35 readonly ignoreWhitespace: boolean;
36}
37
38export interface DeleteCheckpointRefsInput {
39 readonly cwd: string;
40 readonly checkpointRefs: ReadonlyArray<CheckpointRef>;
41}
42
43/**
44 * CheckpointStoreShape - Service API for checkpoint capture/restore and diff access.
45 */
46export interface CheckpointStoreShape {
47 /**
48 * Check whether cwd is inside a Git worktree.
49 */
50 readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>;
51
52 /**
53 * Capture a checkpoint commit and store it at the provided checkpoint ref.
54 *
55 * Uses an isolated temporary Git index and writes a hidden ref.
56 */
57 readonly captureCheckpoint: (
58 input: CaptureCheckpointInput,
59 ) => Effect.Effect<void, CheckpointStoreError>;
60
61 /**
62 * Check whether a checkpoint ref exists.
63 */
64 readonly hasCheckpointRef: (
65 input: Omit<RestoreCheckpointInput, "fallbackToHead">,
66 ) => Effect.Effect<boolean, CheckpointStoreError>;
67
68 /**
69 * Restore workspace/staging state to a checkpoint.
70 *
71 * Optionally falls back to current `HEAD` when the checkpoint ref is missing.
72 */
73 readonly restoreCheckpoint: (
74 input: RestoreCheckpointInput,
75 ) => Effect.Effect<boolean, CheckpointStoreError>;
76
77 /**
78 * Compute patch diff between two checkpoint refs.
79 *
80 * Can optionally treat missing "from" ref as `HEAD`.
81 */
82 readonly diffCheckpoints: (
83 input: DiffCheckpointsInput,
84 ) => Effect.Effect<string, CheckpointStoreError>;
85
86 /**
87 * Delete the provided checkpoint refs.
88 *
89 * Best-effort delete: missing refs are tolerated.
90 */
91 readonly deleteCheckpointRefs: (
92 input: DeleteCheckpointRefsInput,
93 ) => Effect.Effect<void, CheckpointStoreError>;
94}
95
96/**
97 * CheckpointStore - Service tag for checkpoint persistence and restore operations.
98 */
99export class CheckpointStore extends Context.Service<CheckpointStore, CheckpointStoreShape>()(
100 "t3/checkpointing/Services/CheckpointStore",
101) {}
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+9-5
......@@ -30,8 +30,7 @@ import * as Scope from "effect/Scope";
3030import * as Stream from "effect/Stream";
3131import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3232
33import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";
34import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts";
33import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
3534import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
3635import * as VcsProcess from "../../vcs/VcsProcess.ts";
3736import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
......@@ -247,7 +246,10 @@ async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000)
247246
248247describe("CheckpointReactor", () => {
249248 let runtime: ManagedRuntime.ManagedRuntime<
250 OrchestrationEngineService | CheckpointReactor | CheckpointStore | ProjectionSnapshotQuery,
249 | OrchestrationEngineService
250 | CheckpointReactor
251 | CheckpointStore.CheckpointStore
252 | ProjectionSnapshotQuery,
251253 unknown
252254 > | null = null;
253255 let scope: Scope.Closeable | null = null;
......@@ -328,7 +330,7 @@ describe("CheckpointReactor", () => {
328330 Layer.provideMerge(RuntimeReceiptBusLive),
329331 Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),
330332 Layer.provideMerge(vcsStatusBroadcasterLayer),
331 Layer.provideMerge(CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistry.layer))),
333 Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))),
332334 Layer.provideMerge(
333335 WorkspaceEntries.layer.pipe(
334336 Layer.provide(WorkspacePathsLive),
......@@ -345,7 +347,9 @@ describe("CheckpointReactor", () => {
345347 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
346348 const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery));
347349 const reactor = await runtime.runPromise(Effect.service(CheckpointReactor));
348 const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore));
350 const checkpointStore = await runtime.runPromise(
351 Effect.service(CheckpointStore.CheckpointStore),
352 );
349353 scope = await Effect.runPromise(Scope.make("sequential"));
350354 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
351355 const drain = () => Effect.runPromise(reactor.drain);
apps/server/src/orchestration/Layers/CheckpointReactor.ts+2-2
......@@ -24,7 +24,7 @@ import {
2424 checkpointRefForThreadTurn,
2525 resolveThreadWorkspaceCwd,
2626} from "../../checkpointing/Utils.ts";
27import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts";
27import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
2828import { ProviderService } from "../../provider/Services/ProviderService.ts";
2929import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";
3030import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
......@@ -81,7 +81,7 @@ const make = Effect.gen(function* () {
8181 const orchestrationEngine = yield* OrchestrationEngineService;
8282 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
8383 const providerService = yield* ProviderService;
84 const checkpointStore = yield* CheckpointStore;
84 const checkpointStore = yield* CheckpointStore.CheckpointStore;
8585 const receiptBus = yield* RuntimeReceiptBus;
8686 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
8787 const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
apps/server/src/server.test.ts+1-1
......@@ -71,7 +71,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
7171
7272import * as ServerConfig from "./config.ts";
7373import { makeRoutesLayer } from "./server.ts";
74import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts";
74import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
7575import * as GitManager from "./git/GitManager.ts";
7676import * as Keybindings from "./keybindings.ts";
7777import * as ExternalLauncher from "./process/externalLauncher.ts";
apps/server/src/server.ts+4-4
......@@ -25,8 +25,8 @@ import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts
2525import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";
2626import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
2727import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts";
28import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
29import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
28import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
29import * as CheckpointStore from "./checkpointing/CheckpointStore.ts";
3030import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
3131import * as BitbucketApi from "./sourceControl/BitbucketApi.ts";
3232import * as GitHubCli from "./sourceControl/GitHubCli.ts";
......@@ -232,8 +232,8 @@ const VcsLayerLive = Layer.empty.pipe(
232232);
233233
234234const CheckpointingLayerLive = Layer.empty.pipe(
235 Layer.provideMerge(CheckpointDiffQueryLive),
236 Layer.provideMerge(CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistryLayerLive))),
235 Layer.provideMerge(CheckpointDiffQuery.layer),
236 Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),
237237);
238238
239239const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer));
apps/server/src/ws.ts+1-1
......@@ -56,7 +56,7 @@ import { clamp } from "effect/Number";
5656import { HttpRouter, HttpServerRequest, HttpServerRespondable } from "effect/unstable/http";
5757import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
5858
59import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts";
59import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
6060import * as ServerConfig from "./config.ts";
6161import * as Keybindings from "./keybindings.ts";
6262import * as ExternalLauncher from "./process/externalLauncher.ts";
docs/operations/effect-fn-checklist.md+6-6
......@@ -130,12 +130,12 @@ Effect.fn("name")(
130130- [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555)
131131- [x] Remaining nested callback wrappers in this file
132132
133### `apps/server/src/checkpointing/Layers/CheckpointStore.ts` (`10`)
133### `apps/server/src/checkpointing/CheckpointStore.ts` (`10`)
134134
135- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L89)
136- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L183)
137- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L220)
138- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L252)
135- [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L123)
136- [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L137)
137- [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L144)
138- [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L151)
139139- [ ] Nested callback wrappers in this file
140140
141141### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`)
......@@ -190,7 +190,7 @@ Effect.fn("name")(
190190- [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`)
191191- [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`)
192192- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`)
193- [ ] [apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts) (`2`)
193- [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`)
194194- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`)
195195
196196```
docs/reference/encyclopedia.md+2-2
......@@ -172,8 +172,8 @@ The file patch and changed-file summary for one turn. It is usually computed in
172172[16]: ./provider-architecture.md
173173[17]: ../apps/server/src/provider/Layers/CodexAdapter.ts
174174[18]: ./runtime-modes.md
175[19]: ../apps/server/src/checkpointing/Services/CheckpointStore.ts
176[20]: ../apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts
175[19]: ../apps/server/src/checkpointing/CheckpointStore.ts
176[20]: ../apps/server/src/checkpointing/CheckpointDiffQuery.ts
177177[21]: ../apps/server/src/persistence/Services/ProjectionCheckpoints.ts
178178[22]: ../apps/server/src/checkpointing/Utils.ts
179179[23]: ../apps/server/src/checkpointing/Diffs.ts
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts-1
......@@ -25,7 +25,6 @@ const LEGACY_BASELINE = new Map<string, number>([
2525 ["apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts", 1],
2626 ["apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts", 2],
2727 ["apps/mobile/src/state/use-remote-environment-registry.test.ts", 2],
28 ["apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts", 5],
2928 ["apps/server/src/orchestration/commandInvariants.test.ts", 6],
3029 ["apps/server/src/orchestration/Layers/CheckpointReactor.test.ts", 42],
3130 ["apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts", 5],