From 6388832c882d0cbe127c68285fc99a25ca31449f Mon Sep 17 00:00:00 2001 From: Mnigos Date: Wed, 9 Sep 2026 16:09:39 +0200 Subject: [PATCH 1/2] fix(server): flush checkpoint objects and refs before publishing them --- apps/server/src/vcs/GitVcsDriver.test.ts | 64 +++++++++++++++++++++++- apps/server/src/vcs/GitVcsDriver.ts | 13 +++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 031055a3b6cb..6edadbd25f70 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -7,7 +7,7 @@ import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; import { assert, it } from "@effect/vitest"; -import { GitCommandError } from "@t3tools/contracts"; +import { CheckpointRef, GitCommandError } from "@t3tools/contracts"; import * as ServerConfig from "../config.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; import * as VcsProcess from "./VcsProcess.ts"; @@ -112,3 +112,65 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { ), ); }); + +it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publishing them", () => { + const observedArgs: ReadonlyArray[] = []; + + return Effect.gen(function* () { + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + + yield* driver.checkpoints.captureCheckpoint({ + cwd: "/repo", + checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread/turn/1"), + }); + + const writeCommands = ["add", "write-tree", "commit-tree", "update-ref"]; + const writes = observedArgs.filter((args) => + writeCommands.some((command) => args.includes(command)), + ); + assert.strictEqual(writes.length, 4); + for (const args of writes) { + const fsync = args.indexOf("core.fsync=objects,reference"); + assert.strictEqual(args[fsync - 1], "-c", args.join(" ")); + assert.isBelow( + fsync, + args.findIndex((arg) => writeCommands.includes(arg)), + ); + } + assert.deepStrictEqual(observedArgs.at(-1), [ + "-C", + "/repo", + "-c", + "core.fsync=objects,reference", + "update-ref", + "refs/t3/checkpoints/thread/turn/1", + "commit0000", + ]); + }).pipe( + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + Layer.mock(VcsProcess.VcsProcess)({ + run: (input) => + Effect.sync(() => { + observedArgs.push(input.args); + const stdout = input.args.includes("write-tree") + ? "tree0000\n" + : input.args.includes("commit-tree") + ? "commit0000\n" + : input.args.includes("--git-common-dir") + ? ".git\n" + : ""; + return { + exitCode: ChildProcessSpawner.ExitCode(0), + stdout, + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }; + }), + }), + ), + ), + ); +}); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index f1e48a24d6fe..bbd25b9c0c66 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -711,6 +711,11 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); }); + // Git renames loose objects and refs into place without fsync by default, so + // an unclean restart can leave 0-byte files under refs/t3/** that break every + // later fetch and push. Checkpoint writes flush before they are published. + const durableWrite = ["-c", "core.fsync=objects,reference"] as const; + const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { const operation = "GitVcsDriver.checkpoints.captureCheckpoint"; @@ -746,14 +751,14 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* execute({ operation, cwd: input.cwd, - args: ["add", "-A", "--", "."], + args: [...durableWrite, "add", "-A", "--", "."], env: commitEnv, }); const writeTreeResult = yield* execute({ operation, cwd: input.cwd, - args: ["write-tree"], + args: [...durableWrite, "write-tree"], env: commitEnv, }); const treeOid = writeTreeResult.stdout.trim(); @@ -771,7 +776,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const commitTreeResult = yield* execute({ operation, cwd: input.cwd, - args: ["commit-tree", treeOid, "-m", message], + args: [...durableWrite, "commit-tree", treeOid, "-m", message], env: commitEnv, }); const commitOid = commitTreeResult.stdout.trim(); @@ -788,7 +793,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* execute({ operation, cwd: input.cwd, - args: ["update-ref", input.checkpointRef, commitOid], + args: [...durableWrite, "update-ref", input.checkpointRef, commitOid], }); }).pipe(Effect.ensuring(cleanupTempIndex)); }), From ee91e37b139a64610ea5f96de896619bc60e15fa Mon Sep 17 00:00:00 2001 From: Mnigos Date: Wed, 9 Sep 2026 18:22:54 +0200 Subject: [PATCH 2/2] fix(server): fsync checkpoint writes on macOS too --- apps/server/src/vcs/GitVcsDriver.test.ts | 14 ++++++++------ apps/server/src/vcs/GitVcsDriver.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 6edadbd25f70..7bf14bfb3beb 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -130,18 +130,20 @@ it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publi ); assert.strictEqual(writes.length, 4); for (const args of writes) { - const fsync = args.indexOf("core.fsync=objects,reference"); - assert.strictEqual(args[fsync - 1], "-c", args.join(" ")); - assert.isBelow( - fsync, - args.findIndex((arg) => writeCommands.includes(arg)), - ); + const command = args.findIndex((arg) => writeCommands.includes(arg)); + for (const setting of ["core.fsync=objects,reference", "core.fsyncMethod=fsync"]) { + const index = args.indexOf(setting); + assert.strictEqual(args[index - 1], "-c", args.join(" ")); + assert.isBelow(index, command); + } } assert.deepStrictEqual(observedArgs.at(-1), [ "-C", "/repo", "-c", "core.fsync=objects,reference", + "-c", + "core.fsyncMethod=fsync", "update-ref", "refs/t3/checkpoints/thread/turn/1", "commit0000", diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index bbd25b9c0c66..9356b614bc8a 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -713,8 +713,14 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( // Git renames loose objects and refs into place without fsync by default, so // an unclean restart can leave 0-byte files under refs/t3/** that break every - // later fetch and push. Checkpoint writes flush before they are published. - const durableWrite = ["-c", "core.fsync=objects,reference"] as const; + // later fetch and push. Checkpoint writes flush before they are published; + // macOS defaults to writeout-only, which does not reach the disk either. + const durableWrite = [ + "-c", + "core.fsync=objects,reference", + "-c", + "core.fsyncMethod=fsync", + ] as const; const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) {