/** * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints. * * Owns hidden checkpoint capture/restore and diff computation for a workspace * thread timeline. Git workspaces use their repository's hidden refs; other * workspaces use a T3-managed shadow Git store outside the project directory. * * The live adapter resolves the active VCS driver once per checkpoint operation * and delegates to the driver's optional checkpoint capability. * * Uses Effect `Context.Service` for dependency injection and exposes typed * domain errors for checkpoint storage operations. * * @module CheckpointStore */ // @effect-diagnostics nodeBuiltinImport:off import * as NodeCrypto from "node:crypto"; import * as NodePath from "node:path"; import { type CheckpointRef, VcsProcessExitError } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import type { CheckpointStoreError } from "./Errors.ts"; import type { VcsCheckpointOps } from "../vcs/VcsDriver.ts"; import { ServerConfig } from "../config.ts"; import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; export interface CaptureCheckpointInput { readonly cwd: string; readonly checkpointRef: CheckpointRef; } export interface RestoreCheckpointInput { readonly cwd: string; readonly checkpointRef: CheckpointRef; readonly fallbackToHead?: boolean; } export interface DiffCheckpointsInput { readonly cwd: string; readonly fromCheckpointRef: CheckpointRef; readonly toCheckpointRef: CheckpointRef; readonly fallbackFromToHead?: boolean; readonly ignoreWhitespace: boolean; } export interface DeleteCheckpointRefsInput { readonly cwd: string; readonly checkpointRefs: ReadonlyArray; } /** Service tag for checkpoint persistence and restore operations. */ export class CheckpointStore extends Context.Service< CheckpointStore, { /** Check whether cwd is inside a user-managed Git worktree. */ readonly isGitRepository: (cwd: string) => Effect.Effect; /** * Capture a checkpoint commit and store it at the provided checkpoint ref. * * Uses an isolated temporary Git index and writes a hidden ref. */ readonly captureCheckpoint: ( input: CaptureCheckpointInput, ) => Effect.Effect; /** Check whether a checkpoint ref exists. */ readonly hasCheckpointRef: ( input: Omit, ) => Effect.Effect; /** * Restore workspace and staging state to a checkpoint. * * Optionally falls back to current `HEAD` when the checkpoint ref is missing. */ readonly restoreCheckpoint: ( input: RestoreCheckpointInput, ) => Effect.Effect; /** * Compute a patch diff between two checkpoint refs. * * Can optionally treat a missing "from" ref as `HEAD`. */ readonly diffCheckpoints: ( input: DiffCheckpointsInput, ) => Effect.Effect; /** * Delete the provided checkpoint refs. * * Best-effort delete: missing refs are tolerated. */ readonly deleteCheckpointRefs: ( input: DeleteCheckpointRefsInput, ) => Effect.Effect; } >()("t3/checkpointing/CheckpointStore") {} export const make = Effect.gen(function* () { const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; const vcsProcess = yield* VcsProcess.VcsProcess; const fileSystem = yield* FileSystem.FileSystem; const config = yield* ServerConfig; const shadowRepositoryPath = (cwd: string) => NodePath.join( config.stateDir, "checkpoints", NodeCrypto.createHash("sha256").update(NodePath.resolve(cwd)).digest("hex"), ); const canonicalizeWorkspacePath = (value: string) => fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => NodePath.resolve(value))); const runShadowGit = Effect.fn("CheckpointStore.runShadowGit")(function* (input: { readonly operation: string; readonly cwd: string; readonly args: ReadonlyArray; readonly env?: NodeJS.ProcessEnv; readonly allowNonZeroExit?: boolean; readonly maxOutputBytes?: number; }) { const gitDir = shadowRepositoryPath(input.cwd); yield* vcsProcess.run({ operation: `${input.operation}.init`, command: "git", cwd: input.cwd, args: ["init", "--bare", "--quiet", gitDir], }); return yield* vcsProcess.run({ operation: input.operation, command: "git", cwd: input.cwd, args: [`--git-dir=${gitDir}`, `--work-tree=${NodePath.resolve(input.cwd)}`, ...input.args], ...(input.env ? { env: input.env } : {}), ...(input.allowNonZeroExit !== undefined ? { allowNonZeroExit: input.allowNonZeroExit } : {}), ...(input.maxOutputBytes !== undefined ? { maxOutputBytes: input.maxOutputBytes } : {}), }); }); const resolveShadowCommit = Effect.fn("CheckpointStore.resolveShadowCommit")(function* (input: { readonly cwd: string; readonly checkpointRef: CheckpointRef; }) { const refResult = yield* runShadowGit({ operation: "CheckpointStore.shadow.resolveCheckpointRef", cwd: input.cwd, args: ["for-each-ref", "--format=%(objectname)", "--", input.checkpointRef], allowNonZeroExit: true, }); const refStdout = refResult.stdout.trim(); const refStderr = refResult.stderr.trim(); if (refResult.exitCode !== 0 || (refStdout.length === 0 && refStderr.length > 0)) { return yield* new VcsProcessExitError({ operation: "CheckpointStore.shadow.resolveCheckpointRef", command: "git for-each-ref", cwd: input.cwd, exitCode: refResult.exitCode === 0 ? 1 : refResult.exitCode, detail: "Failed to resolve shadow checkpoint ref.", stderrLength: refStderr.length, stderrTruncated: refResult.stderrTruncated, }); } if (refStdout.length === 0) { return null; } const result = yield* runShadowGit({ operation: "CheckpointStore.shadow.resolveCheckpointCommit", cwd: input.cwd, args: ["rev-parse", "--verify", "--quiet", `${input.checkpointRef}^{commit}`], allowNonZeroExit: true, }); const stdout = result.stdout.trim(); const stderr = result.stderr.trim(); if (result.exitCode !== 0) { return yield* new VcsProcessExitError({ operation: "CheckpointStore.shadow.resolveCheckpointCommit", command: "git rev-parse", cwd: input.cwd, exitCode: result.exitCode, detail: `Checkpoint ref ${input.checkpointRef} does not resolve to a commit.`, stderrLength: stderr.length, stderrTruncated: result.stderrTruncated, }); } return stdout.length > 0 ? stdout : null; }); const shadowCheckpoints: VcsCheckpointOps = { captureCheckpoint: Effect.fn("CheckpointStore.shadow.captureCheckpoint")(function* (input) { const gitDir = shadowRepositoryPath(input.cwd); const tempIndexPath = NodePath.join(gitDir, `t3-checkpoint-index-${NodeCrypto.randomUUID()}`); const env: NodeJS.ProcessEnv = { ...process.env, GIT_INDEX_FILE: tempIndexPath, GIT_AUTHOR_NAME: "T3 Code", GIT_AUTHOR_EMAIL: "t3code@users.noreply.github.com", GIT_COMMITTER_NAME: "T3 Code", GIT_COMMITTER_EMAIL: "t3code@users.noreply.github.com", }; const cleanup = fileSystem.remove(tempIndexPath, { force: true }).pipe(Effect.ignore); yield* Effect.gen(function* () { yield* runShadowGit({ operation: "CheckpointStore.shadow.captureCheckpoint.readTree", cwd: input.cwd, args: ["read-tree", "--empty"], env, }); yield* runShadowGit({ operation: "CheckpointStore.shadow.captureCheckpoint.add", cwd: input.cwd, args: ["add", "-A", "--", "."], env, }); const tree = yield* runShadowGit({ operation: "CheckpointStore.shadow.captureCheckpoint.writeTree", cwd: input.cwd, args: ["write-tree"], env, }); const commit = yield* runShadowGit({ operation: "CheckpointStore.shadow.captureCheckpoint.commitTree", cwd: input.cwd, args: [ "commit-tree", tree.stdout.trim(), "-m", `t3 checkpoint ref=${input.checkpointRef}`, ], env, }); yield* runShadowGit({ operation: "CheckpointStore.shadow.captureCheckpoint.updateRef", cwd: input.cwd, args: ["update-ref", input.checkpointRef, commit.stdout.trim()], }); }).pipe(Effect.ensuring(cleanup)); }), hasCheckpointRef: (input) => resolveShadowCommit(input).pipe(Effect.map((commit) => commit !== null)), restoreCheckpoint: Effect.fn("CheckpointStore.shadow.restoreCheckpoint")(function* (input) { const commit = yield* resolveShadowCommit(input); if (!commit) { // Shadow repositories intentionally have no HEAD: every restorable // state must be addressed by its explicit checkpoint ref. Fail closed // so callers never roll conversation history back without matching files. return false; } const tempIndexPath = NodePath.join( shadowRepositoryPath(input.cwd), `t3-checkpoint-index-${NodeCrypto.randomUUID()}`, ); const env: NodeJS.ProcessEnv = { ...process.env, GIT_INDEX_FILE: tempIndexPath }; const cleanup = fileSystem.remove(tempIndexPath, { force: true }).pipe(Effect.ignore); yield* Effect.gen(function* () { yield* runShadowGit({ operation: "CheckpointStore.shadow.restoreCheckpoint.readTree", cwd: input.cwd, args: ["read-tree", commit], env, }); yield* runShadowGit({ operation: "CheckpointStore.shadow.restoreCheckpoint.checkout", cwd: input.cwd, args: ["checkout-index", "--all", "--force"], env, }); yield* runShadowGit({ operation: "CheckpointStore.shadow.restoreCheckpoint.clean", cwd: input.cwd, args: ["clean", "-fd", "--", "."], env, }); }).pipe(Effect.ensuring(cleanup)); return true; }), diffCheckpoints: Effect.fn("CheckpointStore.shadow.diffCheckpoints")(function* (input) { const result = yield* runShadowGit({ operation: "CheckpointStore.shadow.diffCheckpoints", cwd: input.cwd, args: [ "diff", "--patch", "--no-color", "--no-ext-diff", "--no-textconv", ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []), `${input.fromCheckpointRef}^{commit}`, `${input.toCheckpointRef}^{commit}`, ], allowNonZeroExit: true, maxOutputBytes: 50 * 1024 * 1024, }); if (result.exitCode !== 0) { return yield* new VcsProcessExitError({ operation: "CheckpointStore.shadow.diffCheckpoints", command: "git diff", cwd: input.cwd, exitCode: result.exitCode, detail: "Checkpoint ref is unavailable for diff operation.", stderrLength: result.stderr.trim().length, stderrTruncated: result.stderrTruncated, }); } return result.stdout; }), deleteCheckpointRefs: Effect.fn("CheckpointStore.shadow.deleteCheckpointRefs")( function* (input) { yield* Effect.forEach( input.checkpointRefs, (checkpointRef) => runShadowGit({ operation: "CheckpointStore.shadow.deleteCheckpointRefs", cwd: input.cwd, args: ["update-ref", "-d", checkpointRef], allowNonZeroExit: true, }), { concurrency: 1, discard: true }, ); }, ), }; const resolveCheckpoints = Effect.fn("CheckpointStore.resolveCheckpoints")(function* ( _operation: string, cwd: string, ) { const handle = yield* vcsRegistry.detect({ cwd }); const workspaceOwnsRepository = handle !== null && (yield* canonicalizeWorkspacePath(handle.repository.rootPath)) === (yield* canonicalizeWorkspacePath(cwd)); return workspaceOwnsRepository ? (handle.driver.checkpoints ?? shadowCheckpoints) : shadowCheckpoints; }); const isGitRepository: CheckpointStore["Service"]["isGitRepository"] = Effect.fn( "CheckpointStore.isGitRepository", )(function* (cwd) { const repository = yield* vcsRegistry.detect({ cwd, requestedKind: "git" }); return ( repository !== null && (yield* canonicalizeWorkspacePath(repository.repository.rootPath)) === (yield* canonicalizeWorkspacePath(cwd)) ); }); const captureCheckpoint: CheckpointStore["Service"]["captureCheckpoint"] = Effect.fn( "captureCheckpoint", )(function* (input) { const checkpoints = yield* resolveCheckpoints("CheckpointStore.captureCheckpoint", input.cwd); return yield* checkpoints.captureCheckpoint(input); }); const hasCheckpointRef: CheckpointStore["Service"]["hasCheckpointRef"] = Effect.fn( "hasCheckpointRef", )(function* (input) { const checkpoints = yield* resolveCheckpoints("CheckpointStore.hasCheckpointRef", input.cwd); return yield* checkpoints.hasCheckpointRef(input); }); const restoreCheckpoint: CheckpointStore["Service"]["restoreCheckpoint"] = Effect.fn( "restoreCheckpoint", )(function* (input) { const checkpoints = yield* resolveCheckpoints("CheckpointStore.restoreCheckpoint", input.cwd); return yield* checkpoints.restoreCheckpoint(input); }); const diffCheckpoints: CheckpointStore["Service"]["diffCheckpoints"] = Effect.fn( "diffCheckpoints", )(function* (input) { const checkpoints = yield* resolveCheckpoints("CheckpointStore.diffCheckpoints", input.cwd); return yield* checkpoints.diffCheckpoints(input); }); const deleteCheckpointRefs: CheckpointStore["Service"]["deleteCheckpointRefs"] = Effect.fn( "deleteCheckpointRefs", )(function* (input) { const checkpoints = yield* resolveCheckpoints( "CheckpointStore.deleteCheckpointRefs", input.cwd, ); return yield* checkpoints.deleteCheckpointRefs(input); }); return CheckpointStore.of({ isGitRepository, captureCheckpoint, hasCheckpointRef, restoreCheckpoint, diffCheckpoints, deleteCheckpointRefs, }); }); export const layer = Layer.effect(CheckpointStore, make);