From 285f597105bce5215445d7c81faf87ab13f7cdd3 Mon Sep 17 00:00:00 2001 From: lucianholt Date: Mon, 2 Nov 2026 11:55:10 +0100 Subject: [PATCH 1/7] fix(server): failed checkpoints no longer leak Git's batch object directory On macOS and Windows, checkpoint staging runs `git add` with core.fsyncMethod=batch, which writes new objects into objects/tmp_objdir-bulk-fsync-*. When the add is killed (the 30s timeout, or interruption) Git leaves that directory behind. A workspace whose capture always times out leaked a full copy of its untracked files on every turn: 357 directories, ~145 GB, in one day. Capture now removes the batch directories that appeared during a failed staging run. Staging is serialized per repository so the cleanup never touches another capture's in-flight directory. Implemented with Claude Opus 5.5 in Claude Code (T3 Code). Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriver.test.ts | 54 +++++++++++++++++++++++- apps/server/src/vcs/GitVcsDriver.ts | 42 +++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index ba2b4abe50af7..13cfdb56dec06 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -16,7 +16,12 @@ import { ChildProcessSpawner } from "effect/process"; import * as Metric from "effect/Metric"; import { assert, it } from "@effect/vitest"; -import { CheckpointRef, GitCommandError, VcsProcessExitError } from "@t3tools/contracts"; +import { + CheckpointRef, + GitCommandError, + VcsProcessExitError, + VcsProcessTimeoutError, +} from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as ServerConfig from "../config.ts"; @@ -352,6 +357,53 @@ it.effect("checkpoint capture refuses a truncated nested repository listing", () }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); +it.effect("checkpoint capture removes the batch object directory a killed add leaves", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const liveProcess = yield* VcsProcess.VcsProcess; + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-killed-add-" }); + const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + const objects = path.join(cwd, ".git", "objects"); + yield* fs.makeDirectory(path.join(objects, "tmp_objdir-bulk-fsync-other")); + const captureDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( + Effect.provideService(VcsProcess.VcsProcess, { + run: (input) => + input.args.includes("add") + ? fs + .makeDirectory(path.join(objects, "tmp_objdir-bulk-fsync-killed", "ab"), { + recursive: true, + }) + .pipe( + Effect.orDie, + Effect.andThen( + Effect.fail( + new VcsProcessTimeoutError({ + operation: input.operation, + command: input.command, + cwd: input.cwd, + timeoutMs: 30_000, + }), + ), + ), + ) + : liveProcess.run(input), + }), + ); + + const result = yield* Effect.result( + captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef }), + ); + + assert.strictEqual(result._tag, "Failure"); + assert.deepEqual( + (yield* fs.readDirectory(objects)).filter((entry) => entry.startsWith("tmp_objdir-")), + ["tmp_objdir-bulk-fsync-other"], + ); + }).pipe(Effect.scoped, Effect.provide(layerGitContract)), +); + it.effect("checkpoint capture skips an ignored project while keeping its allowed sibling", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index b6f5fbbf496a9..beed37f640243 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -8,6 +8,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as Semaphore from "effect/Semaphore"; import { ChildProcessSpawner } from "effect/process"; import { @@ -446,6 +447,7 @@ export class GitVcsDriver extends Context.Service< const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; const CHECKPOINT_RECOVERY_MAX_CANDIDATES = 64; const CHECKPOINT_RECOVERY_TIMEOUT = "5 seconds"; +const BATCH_OBJECT_DIR = "tmp_objdir-bulk-fsync-"; const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "-c", @@ -891,6 +893,17 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( : []), ] as const; + // One staging run per repository, so failure cleanup only removes its own batch directory. + const stagingPermits = new Map(); + const withStagingPermit = (gitCommonDir: string) => { + let semaphore = stagingPermits.get(gitCommonDir); + if (semaphore === undefined) { + semaphore = Semaphore.makeUnsafe(1); + stagingPermits.set(gitCommonDir, semaphore); + } + return semaphore.withPermits(1); + }; + // Checkpoints must handle existing paths beyond Windows MAX_PATH, even when // core.longpaths is disabled in the user's Git configuration. const longPaths = hostPlatform === "win32" ? ["-c", "core.longpaths=true"] : []; @@ -1134,7 +1147,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( ], env: commitEnv, }); - yield* stageFiles([]).pipe( + const stage = stageFiles([]).pipe( Effect.catchTags({ VcsProcessExitError: (error) => Effect.gen(function* () { @@ -1190,6 +1203,33 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( ), }), ); + // Batch fsync stages objects in a tmp_objdir-bulk-fsync-* that a killed Git leaves behind. + const objectsDir = path.join(gitCommonDir, "objects"); + const listBatchObjectDirs = fileSystem.readDirectory(objectsDir).pipe( + Effect.map((entries) => entries.filter((entry) => entry.startsWith(BATCH_OBJECT_DIR))), + Effect.orElseSucceed((): ReadonlyArray => []), + ); + yield* listBatchObjectDirs.pipe( + Effect.flatMap((existing) => + stage.pipe( + Effect.onError(() => + listBatchObjectDirs.pipe( + Effect.flatMap((entries) => + Effect.forEach( + entries.filter((entry) => !existing.includes(entry)), + (entry) => + fileSystem + .remove(path.join(objectsDir, entry), { recursive: true }) + .pipe(Effect.ignore), + { discard: true }, + ), + ), + ), + ), + ), + ), + withStagingPermit(gitCommonDir), + ); const writeTreeResult = yield* execute({ operation, From a049688fb4d54a3079a78bc3bd4e44cfc9f8caa7 Mon Sep 17 00:00:00 2001 From: lucianholt Date: Mon, 2 Nov 2026 12:42:57 +0100 Subject: [PATCH 2/7] fix(server): stage checkpoints in a private object directory Deleting batch directories that appeared during a failed add could not prove ownership: another Git process in the same repository could own one, and removing it mid-add leaves that process's index pointing at missing objects while it still exits 0. Staging now runs with GIT_OBJECT_DIRECTORY set to a per-capture directory (the repository's objects stay reachable as an alternate), so Git's tmp_objdir-bulk-fsync-* lands inside a directory the capture owns and always removes. After a successful add, the new loose objects and any large-file pack are renamed into the repository, as Git's own batch migration does; the durable update-ref flushes them. Implemented with Claude Opus 5.5 in Claude Code (T3 Code). Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriver.test.ts | 34 ++++++- apps/server/src/vcs/GitVcsDriver.ts | 115 ++++++++++++++--------- 2 files changed, 98 insertions(+), 51 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 13cfdb56dec06..9afbf9594cd81 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -365,16 +365,22 @@ it.effect("checkpoint capture removes the batch object directory a killed add le const driver = yield* GitVcsDriver.makeVcsDriverShape(); const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-killed-add-" }); const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd); - const objects = path.join(cwd, ".git", "objects"); + const gitDir = path.join(cwd, ".git"); + const objects = path.join(gitDir, "objects"); yield* fs.makeDirectory(path.join(objects, "tmp_objdir-bulk-fsync-other")); const captureDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( Effect.provideService(VcsProcess.VcsProcess, { run: (input) => input.args.includes("add") ? fs - .makeDirectory(path.join(objects, "tmp_objdir-bulk-fsync-killed", "ab"), { - recursive: true, - }) + .makeDirectory( + path.join( + input.env?.GIT_OBJECT_DIRECTORY ?? objects, + "tmp_objdir-bulk-fsync-killed", + "ab", + ), + { recursive: true }, + ) .pipe( Effect.orDie, Effect.andThen( @@ -401,6 +407,26 @@ it.effect("checkpoint capture removes the batch object directory a killed add le (yield* fs.readDirectory(objects)).filter((entry) => entry.startsWith("tmp_objdir-")), ["tmp_objdir-bulk-fsync-other"], ); + assert.deepEqual( + (yield* fs.readDirectory(gitDir)).filter((entry) => entry.startsWith("t3-checkpoint-")), + [], + ); + }).pipe(Effect.scoped, Effect.provide(layerGitContract)), +); + +it.effect("checkpoint capture publishes the pack Git writes for a large file", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-large-file-" }); + const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + yield* git(["config", "core.bigFileThreshold", "1"]); + + yield* driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); + + assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n"); + assert.isNotEmpty(yield* fs.readDirectory(path.join(cwd, ".git", "objects", "pack"))); }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index beed37f640243..d40ee0572de16 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -8,7 +8,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import * as Semaphore from "effect/Semaphore"; +import type * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/process"; import { @@ -447,7 +447,6 @@ export class GitVcsDriver extends Context.Service< const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; const CHECKPOINT_RECOVERY_MAX_CANDIDATES = 64; const CHECKPOINT_RECOVERY_TIMEOUT = "5 seconds"; -const BATCH_OBJECT_DIR = "tmp_objdir-bulk-fsync-"; const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "-c", @@ -893,21 +892,55 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( : []), ] as const; - // One staging run per repository, so failure cleanup only removes its own batch directory. - const stagingPermits = new Map(); - const withStagingPermit = (gitCommonDir: string) => { - let semaphore = stagingPermits.get(gitCommonDir); - if (semaphore === undefined) { - semaphore = Semaphore.makeUnsafe(1); - stagingPermits.set(gitCommonDir, semaphore); - } - return semaphore.withPermits(1); - }; - // Checkpoints must handle existing paths beyond Windows MAX_PATH, even when // core.longpaths is disabled in the user's Git configuration. const longPaths = hostPlatform === "win32" ? ["-c", "core.longpaths=true"] : []; + const checkpointObjectsError = (cwd: string) => + Effect.mapError( + (cause: PlatformError.PlatformError) => + new VcsProcessExitError({ + operation: VcsProcess.CHECKPOINT_CAPTURE_OPERATION, + command: "git add", + cwd, + exitCode: 0, + detail: `Could not stage checkpoint objects: ${cause.message}`, + }), + ); + + // Git's own batch migration is also a rename; the durable update-ref flushes these renames. + const publishStagedObjects = Effect.fn("GitVcsDriver.publishStagedObjects")( + function* (cwd: string, from: string, to: string) { + const move = Effect.fnUntraced(function* (directory: string, names: ReadonlyArray) { + yield* fileSystem.makeDirectory(path.join(to, directory), { recursive: true }); + for (const name of names) { + const target = path.join(to, directory, name); + // Another writer may have published the same read-only object, which Windows won't replace. + yield* fileSystem + .rename(path.join(from, directory, name), target) + .pipe( + Effect.catch((error) => + fileSystem + .exists(target) + .pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error)))), + ), + ); + } + }); + for (const entry of yield* fileSystem.readDirectory(from)) { + if (/^[0-9a-f]{2}$/.test(entry)) { + yield* move(entry, yield* fileSystem.readDirectory(path.join(from, entry))); + } else if (entry === "pack") { + // Large blobs arrive as a pack, which Git reads only once its .idx exists. + const names = yield* fileSystem.readDirectory(path.join(from, entry)); + const isIndex = (name: string) => name.endsWith(".idx"); + yield* move(entry, [...names.filter((n) => !isIndex(n)), ...names.filter(isIndex)]); + } + } + }, + (effect, cwd) => effect.pipe(checkpointObjectsError(cwd)), + ); + const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { const operation = VcsProcess.CHECKPOINT_CAPTURE_OPERATION; @@ -934,9 +967,22 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( GIT_COMMITTER_EMAIL: "t3code@users.noreply.github.com", }; - const cleanupTempIndex = Effect.forEach( - [tempIndexPath, `${tempIndexPath}.lock`], - (indexFile) => fileSystem.remove(indexFile, { force: true }).pipe(Effect.ignore), + // Batch fsync stages objects in a tmp_objdir-bulk-fsync-* that a killed Git leaves behind. + // Staging into a private object directory makes that leftover ours to delete. + const objectsDir = path.join(gitCommonDir, "objects"); + const stagingObjectsDir = path.join(gitCommonDir, `t3-checkpoint-objects-${indexId}`); + const stageEnv: NodeJS.ProcessEnv = { + ...commitEnv, + GIT_OBJECT_DIRECTORY: stagingObjectsDir, + GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectsDir, process.env.GIT_ALTERNATE_OBJECT_DIRECTORIES] + .filter(Boolean) + .join(hostPlatform === "win32" ? ";" : ":"), + }; + + const cleanupScratch = Effect.forEach( + [tempIndexPath, `${tempIndexPath}.lock`, stagingObjectsDir], + (scratch) => + fileSystem.remove(scratch, { force: true, recursive: true }).pipe(Effect.ignore), { discard: true }, ); @@ -1059,7 +1105,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( }); } } - yield* cleanupTempIndex; + yield* cleanupScratch; yield* execute({ operation, cwd: input.cwd, @@ -1145,9 +1191,10 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( ".", ...exclusions, ], - env: commitEnv, + env: stageEnv, }); - const stage = stageFiles([]).pipe( + yield* fileSystem.makeDirectory(stagingObjectsDir).pipe(checkpointObjectsError(input.cwd)); + yield* stageFiles([]).pipe( Effect.catchTags({ VcsProcessExitError: (error) => Effect.gen(function* () { @@ -1203,33 +1250,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( ), }), ); - // Batch fsync stages objects in a tmp_objdir-bulk-fsync-* that a killed Git leaves behind. - const objectsDir = path.join(gitCommonDir, "objects"); - const listBatchObjectDirs = fileSystem.readDirectory(objectsDir).pipe( - Effect.map((entries) => entries.filter((entry) => entry.startsWith(BATCH_OBJECT_DIR))), - Effect.orElseSucceed((): ReadonlyArray => []), - ); - yield* listBatchObjectDirs.pipe( - Effect.flatMap((existing) => - stage.pipe( - Effect.onError(() => - listBatchObjectDirs.pipe( - Effect.flatMap((entries) => - Effect.forEach( - entries.filter((entry) => !existing.includes(entry)), - (entry) => - fileSystem - .remove(path.join(objectsDir, entry), { recursive: true }) - .pipe(Effect.ignore), - { discard: true }, - ), - ), - ), - ), - ), - ), - withStagingPermit(gitCommonDir), - ); + yield* publishStagedObjects(input.cwd, stagingObjectsDir, objectsDir); const writeTreeResult = yield* execute({ operation, @@ -1279,7 +1300,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( cwd: input.cwd, args: [...durableWrite, "update-ref", input.checkpointRef, commitOid], }); - }).pipe(Effect.ensuring(cleanupTempIndex)); + }).pipe(Effect.ensuring(cleanupScratch)); }), hasCheckpointRef: (input) => From d6d85b9c6f8a8c50a1353c752114c889d9837c2f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 2 Nov 2026 23:08:36 +0000 Subject: [PATCH 3/7] fix(server): stage checkpoint objects on their destination filesystem --- apps/server/src/vcs/GitVcsDriver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index d40ee0572de16..0ac12cf2527ff 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -970,7 +970,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( // Batch fsync stages objects in a tmp_objdir-bulk-fsync-* that a killed Git leaves behind. // Staging into a private object directory makes that leftover ours to delete. const objectsDir = path.join(gitCommonDir, "objects"); - const stagingObjectsDir = path.join(gitCommonDir, `t3-checkpoint-objects-${indexId}`); + const stagingObjectsDir = path.join(objectsDir, `t3-checkpoint-objects-${indexId}`); const stageEnv: NodeJS.ProcessEnv = { ...commitEnv, GIT_OBJECT_DIRECTORY: stagingObjectsDir, From e947ea2ebb63fd6111512b7c9561b4ab5f4176fb Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 2 Nov 2026 23:16:08 +0000 Subject: [PATCH 4/7] fix(server): quote checkpoint object alternates --- apps/server/src/vcs/GitVcsDriver.test.ts | 8 ++++++-- apps/server/src/vcs/GitVcsDriver.ts | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 9afbf9594cd81..c7da47f6be204 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -404,7 +404,9 @@ it.effect("checkpoint capture removes the batch object directory a killed add le assert.strictEqual(result._tag, "Failure"); assert.deepEqual( - (yield* fs.readDirectory(objects)).filter((entry) => entry.startsWith("tmp_objdir-")), + (yield* fs.readDirectory(objects)).filter( + (entry) => entry.startsWith("tmp_objdir-") || entry.startsWith("t3-checkpoint-"), + ), ["tmp_objdir-bulk-fsync-other"], ); assert.deepEqual( @@ -675,7 +677,9 @@ it.effect.each( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const driver = yield* GitVcsDriver.makeVcsDriverShape(); - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-sparse-" }); + const cwd = yield* fs.makeTempDirectoryScoped({ + prefix: `t3-checkpoint-sparse-${process.platform === "win32" ? ";" : ":"}`, + }); const { git } = yield* makeCheckpointFixture(driver, cwd); const write = Effect.fn(function* (name: string, contents: string) { yield* fs.makeDirectory(path.dirname(path.join(cwd, name)), { recursive: true }); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 0ac12cf2527ff..1c6165c09d5ee 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -38,6 +38,7 @@ import { } from "@t3tools/contracts"; import { isMissingExecutable } from "@t3tools/source-control-core/server/discovery"; import * as HostProcess from "@t3tools/shared/HostProcess"; +import { quoteGitPatchPath } from "@t3tools/shared/gitPatchPath"; import { makeGitVcsDriverCore, PATCH_RENDER_PREFIX_ARGS, @@ -971,10 +972,14 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( // Staging into a private object directory makes that leftover ours to delete. const objectsDir = path.join(gitCommonDir, "objects"); const stagingObjectsDir = path.join(objectsDir, `t3-checkpoint-objects-${indexId}`); + const quotedObjectsDir = quoteGitPatchPath(objectsDir); const stageEnv: NodeJS.ProcessEnv = { ...commitEnv, GIT_OBJECT_DIRECTORY: stagingObjectsDir, - GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectsDir, process.env.GIT_ALTERNATE_OBJECT_DIRECTORIES] + GIT_ALTERNATE_OBJECT_DIRECTORIES: [ + quotedObjectsDir.startsWith('"') ? quotedObjectsDir : `"${quotedObjectsDir}"`, + process.env.GIT_ALTERNATE_OBJECT_DIRECTORIES, + ] .filter(Boolean) .join(hostPlatform === "win32" ? ";" : ":"), }; From a1c6a175bff571937a1c01822aadb8690c1ffec7 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 2 Nov 2026 23:17:50 +0000 Subject: [PATCH 5/7] test(server): use injected platform for checkpoint paths --- apps/server/src/vcs/GitVcsDriver.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index c7da47f6be204..386f71a978297 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -677,8 +677,9 @@ it.effect.each( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const hostPlatform = yield* HostProcess.Platform; const cwd = yield* fs.makeTempDirectoryScoped({ - prefix: `t3-checkpoint-sparse-${process.platform === "win32" ? ";" : ":"}`, + prefix: `t3-checkpoint-sparse-${hostPlatform === "win32" ? ";" : ":"}`, }); const { git } = yield* makeCheckpointFixture(driver, cwd); const write = Effect.fn(function* (name: string, contents: string) { From f8765ce9111bbb126124156000441b5127fd1ccb Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 05:02:08 +0000 Subject: [PATCH 6/7] fix(server): keep checkpoint commits in their published object store --- apps/server/src/vcs/GitVcsDriver.ts | 8 ++++++-- packages/contracts/src/vcs.ts | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 20b1b23d6148f..99bfb48dcee49 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -906,13 +906,15 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( command: "git add", cwd, exitCode: 0, - detail: `Could not stage checkpoint objects: ${cause.message}`, + detail: "Could not stage checkpoint objects.", + cause, }), ); // Git's own batch migration is also a rename; the durable update-ref flushes these renames. const publishStagedObjects = Effect.fn("GitVcsDriver.publishStagedObjects")( function* (cwd: string, from: string, to: string) { + // Per-directory moves share the enclosing publication span. const move = Effect.fnUntraced(function* (directory: string, names: ReadonlyArray) { yield* fileSystem.makeDirectory(path.join(to, directory), { recursive: true }); for (const name of names) { @@ -960,9 +962,11 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const { gitCommonDir, indexPath, headExists } = yield* resolveCaptureRepository(input.cwd); const indexId = yield* random.uuidV4(); const tempIndexPath = path.join(gitCommonDir, `t3-checkpoint-index-${indexId}`); + const objectsDir = path.join(gitCommonDir, "objects"); const commitEnv: NodeJS.ProcessEnv = { ...process.env, GIT_INDEX_FILE: tempIndexPath, + GIT_OBJECT_DIRECTORY: objectsDir, GIT_AUTHOR_NAME: "T3 Code", GIT_AUTHOR_EMAIL: "t3code@users.noreply.github.com", GIT_COMMITTER_NAME: "T3 Code", @@ -971,7 +975,6 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( // Batch fsync stages objects in a tmp_objdir-bulk-fsync-* that a killed Git leaves behind. // Staging into a private object directory makes that leftover ours to delete. - const objectsDir = path.join(gitCommonDir, "objects"); const stagingObjectsDir = path.join(objectsDir, `t3-checkpoint-objects-${indexId}`); const quotedObjectsDir = quoteGitPatchPath(objectsDir); const stageEnv: NodeJS.ProcessEnv = { @@ -1348,6 +1351,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( operation, cwd: input.cwd, args: [...durableWrite, "update-ref", input.checkpointRef, commitOid], + env: commitEnv, }); }).pipe(Effect.ensuring(cleanupScratch)); }), diff --git a/packages/contracts/src/vcs.ts b/packages/contracts/src/vcs.ts index 91cb8fa029022..5411d5f3591f3 100644 --- a/packages/contracts/src/vcs.ts +++ b/packages/contracts/src/vcs.ts @@ -122,6 +122,7 @@ export class VcsProcessExitError extends Schema.TaggedError retryable: Schema.optional(Schema.Boolean), stderrLength: Schema.optional(NonNegativeInt), stderrTruncated: Schema.optional(Schema.Boolean), + cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { From a190263f50126b7aae4b47d9a8efc9cdcbb2c3af Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 05:08:16 +0000 Subject: [PATCH 7/7] fix(server): preserve shared checkpoint object directory permissions --- apps/server/src/vcs/GitVcsDriver.test.ts | 13 ++++++++++++- apps/server/src/vcs/GitVcsDriver.ts | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 06d03476a4698..82b757d4ee27f 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -281,11 +281,16 @@ it.effect("checkpoint recovery discovers nested HEAD independently of inherited const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-checkpoint-git-dir-" }); const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); yield* git(["init", "empty"]); + yield* git(["config", "core.sharedRepository", "group"]); const originalIndex = yield* fs.readFile(path.join(cwd, ".git", "index")); yield* fs.makeDirectory(path.join(cwd, "inherited-objects")); yield* Effect.acquireUseRelease( Effect.sync(() => { - const previous = { gitDir: process.env.GIT_DIR, objects: process.env.GIT_OBJECT_DIRECTORY }; + const previous = { + gitDir: process.env.GIT_DIR, + objects: process.env.GIT_OBJECT_DIRECTORY, + umask: process.umask(0o022), + }; process.env.GIT_DIR = path.join(cwd, ".git"); process.env.GIT_OBJECT_DIRECTORY = path.join(cwd, "inherited-objects"); return previous; @@ -293,6 +298,7 @@ it.effect("checkpoint recovery discovers nested HEAD independently of inherited () => driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }), (previous) => Effect.sync(() => { + process.umask(previous.umask); if (previous.gitDir === undefined) delete process.env.GIT_DIR; else process.env.GIT_DIR = previous.gitDir; if (previous.objects === undefined) delete process.env.GIT_OBJECT_DIRECTORY; @@ -302,6 +308,11 @@ it.effect("checkpoint recovery discovers nested HEAD independently of inherited assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n"); assert.strictEqual((yield* git(["ls-tree", "-r", checkpointRef, "--", "empty"])).stdout, ""); assert.deepEqual(yield* fs.readFile(path.join(cwd, ".git", "index")), originalIndex); + if ((yield* HostProcess.Platform) !== "win32") { + const blob = (yield* git(["rev-parse", `${checkpointRef}:file.txt`])).stdout.trim(); + const fanout = yield* fs.stat(path.join(cwd, ".git", "objects", blob.slice(0, 2))); + assert.strictEqual(fanout.mode & 0o020, 0o020); + } }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 99bfb48dcee49..add48b0715151 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -916,7 +916,20 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( function* (cwd: string, from: string, to: string) { // Per-directory moves share the enclosing publication span. const move = Effect.fnUntraced(function* (directory: string, names: ReadonlyArray) { - yield* fileSystem.makeDirectory(path.join(to, directory), { recursive: true }); + // Keep Git's shared-repository permissions on newly published directories. + const moved = yield* fileSystem + .rename(path.join(from, directory), path.join(to, directory)) + .pipe( + Effect.as(true), + Effect.catch((error) => + fileSystem + .exists(path.join(to, directory)) + .pipe( + Effect.flatMap((exists) => (exists ? Effect.succeed(false) : Effect.fail(error))), + ), + ), + ); + if (moved) return; for (const name of names) { const target = path.join(to, directory, name); // Another writer may have published the same read-only object, which Windows won't replace.