Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Use a checkout of the commit under Version or commit with its lockfile dependencies installed. Save the script below outside the checkout as repro.sh and run env -u NODE_ENV sh /path/to/repro.sh from the checkout root, on macOS or Linux as a non-root user. It creates a private HOME, TMPDIR and T3CODE_HOME under a mktemp directory (set VERIFY_TMP_PARENT to choose its parent), installs one temporary test at apps/server/src/orchestration-v2/TmpRollbackNoGitRepro.test.ts (refusing to overwrite an existing file), runs it with the server t3 project, and removes the test and fixture on exit.
The test calls the exported production CheckpointRollbackServiceV2.execute with the same input fields the effect worker passes. It uses the real CheckpointService, CheckpointStore, GitWorkflowService, GitManager, GitVcsDriver, VcsDriverRegistry and Git subprocess layers against real temporary Git repositories. Projection and project records, the event sink, runtime policy and provider sessions are fixtures. No server, browser or live provider runs.
- Create a main repository and a managed linked worktree on a preserved branch under the server's worktrees directory. Write
checkpoint-stateto a tracked filea.txtand capture a ready checkpoint with the realCheckpointService. - Change
a.txttoafter-checkpoint. - Make only the worktree's parent directory read-only and run
git worktree remove --forcefrom the main repository. Git exits 255 withPermission deniedafter deleting the worktree's contents and unregistering it, leaving an empty folder without.git. Restore the parent's permissions. - Call
CheckpointRollbackServiceV2.executefor that thread and checkpoint with file restoration enabled (the default). It fails as described under Actual behavior, anda.txtstays missing. - Controls in the same run: removing the whole worktree folder before rollback lets the same production path recreate the checkout and restore
checkpoint-state; deleting only.git(leaving the tracked file) fails the same way and leavesafter-checkpoint;git worktree prune --expire nowon a healthy worktree leaves its.gitin place, so prune alone does not create this state. Finally the script moves the empty folder aside, retries the rollback, and checks thatcheckpoint-stateis restored.
The script exits 0 only when both failing folder states, the missing-folder control and the move-aside workaround match these assertions.
Reproduction script: repro.sh
#!/bin/sh
# Run from an installed-dependency t3code checkout at the reported commit.
set -eu
CHECKOUT=$(pwd)
VERIFY_TMP_PARENT=${VERIFY_TMP_PARENT:-${TMPDIR:-/tmp}}
RUN_ROOT=$(mktemp -d "$VERIFY_TMP_PARENT/rollback-verify.XXXXXX")
mkdir -p "$RUN_ROOT/fake-home" "$RUN_ROOT/tmp" "$RUN_ROOT/t3home"
export HOME="$RUN_ROOT/fake-home" TMPDIR="$RUN_ROOT/tmp" T3CODE_HOME="$RUN_ROOT/t3home"
export PATH="$CHECKOUT/node_modules/.bin:$PATH"
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0
TEST_REL="apps/server/src/orchestration-v2/TmpRollbackNoGitRepro.test.ts"
TEST_ABS="$CHECKOUT/$TEST_REL"
if [ -e "$TEST_ABS" ]; then echo "Refusing to replace existing test"; exit 1; fi
trap 'rm -f "$TEST_ABS"; rm -rf "$RUN_ROOT"' EXIT INT TERM
cat > "$TEST_ABS" <<'T3REPRO_EOF'
// @effect-diagnostics nodeBuiltinImport:off
import { execFileSync, spawnSync } from "node:child_process";
import * as NodePath from "node:path";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import {
CheckpointScopeId,
NodeId,
type OrchestrationV2CheckpointScope,
ProjectId,
ProviderInstanceId,
ProviderSessionId,
ProviderThreadId,
ThreadId,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
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 ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as GitManager from "../git/GitManager.ts";
import * as GitWorkflow from "../git/GitWorkflowService.ts";
import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
import * as TextGeneration from "../textGeneration/TextGeneration.ts";
import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
import * as VcsProjectConfig from "../vcs/VcsProjectConfig.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as CheckpointService from "./CheckpointService.ts";
import * as CheckpointRollbackService from "./CheckpointRollbackService.ts";
import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";
import * as EventSink from "./EventSink.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProjectionStore from "./ProjectionStore.ts";
import * as ProjectStore from "./ProjectStore.ts";
import * as ProviderSessionManager from "./ProviderSessionManager.ts";
import * as RuntimePolicy from "./RuntimePolicy.ts";
const scratchRoot = process.env["TMPDIR"] ?? "/tmp";
const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const baseDir = NodePath.join(scratchRoot, "t3-rollback-nogit", runId, "t3home");
const worktreesDir = NodePath.join(baseDir, "worktrees");
const state: {
projection: ProjectionStore.ProjectionCheckpointRollbackContext | undefined;
workspaces: Map<string, string>;
} = { projection: undefined, workspaces: new Map() };
const platform = Layer.mergeAll(NodeServices.layer, NodeCrypto.layer);
const serverConfigLayer = ServerConfig.layerTest(process.cwd(), baseDir);
const baseLayers = Layer.mergeAll(
platform,
serverConfigLayer.pipe(Layer.provide(platform)),
ServerSettings.layerTest().pipe(Layer.provide(platform)),
);
const vcsProcessLayer = VcsProcess.layer.pipe(Layer.provide(baseLayers));
const gitDriverLayer = GitVcsDriver.layer.pipe(
Layer.provide(vcsProcessLayer),
Layer.provide(serverConfigLayer),
Layer.provide(baseLayers),
);
const vcsRegistryLayer = VcsDriverRegistry.layer.pipe(
Layer.provide(VcsProjectConfig.layer),
Layer.provide(vcsProcessLayer),
Layer.provide(serverConfigLayer),
Layer.provide(baseLayers),
);
const checkpointStoreLayer = CheckpointStore.layer.pipe(
Layer.provide(vcsRegistryLayer),
Layer.provide(baseLayers),
);
const checkpointServiceLayer = CheckpointService.layer.pipe(
Layer.provide(checkpointStoreLayer),
Layer.provide(IdAllocator.layer),
Layer.provide(baseLayers),
);
const projectStoreLayer = Layer.mock(ProjectStore.ProjectStoreV2)({
get: (id: ProjectId) => Effect.sync(() => {
const workspaceRoot = state.workspaces.get(id);
return workspaceRoot === undefined ? Option.none() : Option.some({ workspaceRoot } as never);
}),
findActiveByWorkspaceRoot: () => Effect.succeedNone,
});
const projectionLayer = Layer.mock(ProjectionStore.ProjectionStoreV2)({
getThread: () => Effect.sync(() => state.projection!.thread),
getCheckpointRollbackContext: () => Effect.sync(() => state.projection!),
getShellSnapshot: () => Effect.succeed({ schemaVersion: 1, snapshotSequence: 0, threads: [], archivedThreads: [] }),
});
const managerLayer = GitManager.layer.pipe(Layer.provide(Layer.mergeAll(
gitDriverLayer,
baseLayers,
projectStoreLayer,
projectionLayer,
Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({}),
Layer.mock(TextGeneration.TextGeneration)({}),
Layer.mock(ProviderRegistry.ProviderRegistry)({}),
Layer.mock(ProjectSetupScriptRunner.ProjectSetupScriptRunner)({}),
)));
const gitWorkflowLayer = GitWorkflow.layer.pipe(Layer.provide(Layer.mergeAll(
managerLayer, gitDriverLayer, vcsRegistryLayer, baseLayers,
)));
const rollbackLayer = CheckpointRollbackService.layer.pipe(
Layer.provide(
Layer.mergeAll(
ThreadCommandExecutor.layer,
serverConfigLayer,
ServerSettings.layerTest(),
checkpointServiceLayer,
gitWorkflowLayer,
IdAllocator.layer,
projectStoreLayer,
projectionLayer,
Layer.mock(EventSink.EventSinkV2)({ write: () => Effect.succeed([]) }),
Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
open: () => Effect.succeed({ rollbackThread: () => Effect.die("no turns") } as never),
}),
Layer.mock(RuntimePolicy.RuntimePolicyV2)({
resolve: ({ thread }: { thread: { worktreePath: string | null } }) =>
Effect.succeed({ cwd: thread.worktreePath } as never),
}),
),
),
Layer.provide(baseLayers),
);
const layerTest = Layer.mergeAll(rollbackLayer, checkpointServiceLayer, baseLayers);
const git = (cwd: string, args: ReadonlyArray<string>) =>
execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
const runScenario = (scenario: "emptied" | "removed" | "leftover") =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const checkpoints = yield* CheckpointService.CheckpointServiceV2;
const rollbackService = yield* CheckpointRollbackService.CheckpointRollbackServiceV2;
const workspaceRoot = path.join(baseDir, scenario, "project");
const projectId = ProjectId.make(`rollback-nogit-project-${scenario}`);
const worktreePath = path.join(worktreesDir, "project", scenario);
const branch = `feature/${scenario}`;
const threadId = ThreadId.make(`rollback-nogit-thread-${scenario}`);
const providerThreadId = ProviderThreadId.make(`rollback-nogit-provider-thread-${scenario}`);
const providerSessionId = ProviderSessionId.make(`rollback-nogit-session-${scenario}`);
const providerInstanceId = ProviderInstanceId.make("codex");
yield* fileSystem.makeDirectory(workspaceRoot, { recursive: true });
yield* fileSystem.makeDirectory(path.dirname(worktreePath), { recursive: true });
yield* fileSystem.writeFileString(path.join(workspaceRoot, "a.txt"), "base\n");
git(workspaceRoot, ["init", "-b", "main"]);
git(workspaceRoot, ["config", "user.name", "Test"]);
git(workspaceRoot, ["config", "user.email", "test@example.com"]);
git(workspaceRoot, ["add", "-A"]);
git(workspaceRoot, ["commit", "-m", "base"]);
git(workspaceRoot, ["worktree", "add", "-b", branch, worktreePath]);
yield* fileSystem.writeFileString(path.join(worktreePath, "a.txt"), "checkpoint-state\n");
const now = yield* DateTime.now;
const scope: OrchestrationV2CheckpointScope = {
id: CheckpointScopeId.make(`rollback-nogit-scope-${scenario}`),
threadId,
runId: null as never,
nodeId: NodeId.make(`rollback-nogit-node-${scenario}`),
parentScopeId: null,
providerThreadId,
kind: "root_run",
ordinalWithinParent: 0,
advancesAppRunCount: true,
cwd: worktreePath,
createdAt: now,
};
const checkpoint = yield* checkpoints.capture({
scope,
runId: null,
nodeId: scope.nodeId,
ordinalWithinScope: 0,
appRunOrdinal: 0,
capturedAt: now,
});
assert.equal(checkpoint.status, "ready");
// A change the rollback must revert once it restores files.
yield* fileSystem.writeFileString(path.join(worktreePath, "a.txt"), "after-checkpoint\n");
state.workspaces.set(projectId, workspaceRoot);
state.projection = {
thread: {
id: threadId,
projectId,
worktreePath,
branch,
activeProviderThreadId: providerThreadId,
modelSelection: { instanceId: providerInstanceId, model: "test-model" },
},
providerThread: { id: providerThreadId, providerSessionId, providerInstanceId },
providerSession: undefined,
checkpoint,
scope,
runsToRollback: [],
targetAttempt: undefined,
rolledBackAttemptIds: [],
staleCheckpoints: [],
rootNodes: [],
providerTurns: [],
} as unknown as ProjectionStore.ProjectionCheckpointRollbackContext;
// Prune alone must leave a healthy existing worktree and its .git intact.
git(workspaceRoot, ["worktree", "prune", "--expire", "now"]);
assert.isTrue(yield* fileSystem.exists(path.join(worktreePath, ".git")));
if (scenario === "emptied") {
yield* fileSystem.remove(path.join(worktreePath, ".git"));
git(workspaceRoot, ["worktree", "prune", "--expire", "now"]);
} else if (scenario === "leftover") {
// Real Git removes contents but cannot unlink the final folder from a read-only parent.
const parent = path.dirname(worktreePath);
yield* fileSystem.chmod(parent, 0o555);
let removal;
try {
removal = spawnSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: workspaceRoot, encoding: "utf8" });
} finally {
yield* fileSystem.chmod(parent, 0o755);
}
assert.notEqual(removal.status, 0);
assert.include(removal.stderr, "Permission denied");
assert.isTrue(yield* fileSystem.exists(worktreePath));
assert.isFalse(yield* fileSystem.exists(path.join(worktreePath, ".git")));
assert.deepEqual(yield* fileSystem.readDirectory(worktreePath), []);
assert.notInclude(git(workspaceRoot, ["worktree", "list", "--porcelain"]), branch);
console.log("NATURAL_GIT_REMOVE exit", removal.status, "permission failure left an empty unregistered folder without .git; permissions restored before rollback");
} else {
yield* fileSystem.remove(worktreePath, { recursive: true });
}
const result = yield* rollbackService
.execute({ threadId, providerThreadId, checkpointId: checkpoint.id, scopeId: scope.id })
.pipe(Effect.result);
let gitToplevel = "<git failed>";
try {
gitToplevel = git(worktreePath, ["rev-parse", "--show-toplevel"]);
} catch {
gitToplevel = "<git failed>";
}
const aText = yield* fileSystem
.readFileString(path.join(worktreePath, "a.txt"))
.pipe(Effect.orElseSucceed(() => "<missing>"));
if (result._tag === "Failure") {
const error = result.failure;
assert.equal(error._tag, "CheckpointRollbackExecutionError");
assert.equal(error.reason, "unexpected-failure");
assert.equal(error.cause._tag, "CheckpointRestoreError");
assert.equal(error.cause.cause._tag, "VcsUnsupportedOperationError");
assert.equal(error.cause.cause.operation, "VcsDriverRegistry.resolve");
assert.equal(CheckpointRollbackService.rollbackFailureClientMessage(error), CheckpointRollbackService.ROLLBACK_FAILED_MESSAGE);
console.log(scenario.toUpperCase(), "CAUSE_CHAIN", error._tag, error.reason, error.cause._tag, error.cause.cause._tag, error.cause.cause.operation);
console.log(scenario.toUpperCase(), "CLIENT_MESSAGE", CheckpointRollbackService.rollbackFailureClientMessage(error));
}
return { result, gitToplevel, aText, worktreePath, workspaceRoot };
});
it.effect("TMP rollback with an emptied worktree folder fails and never recreates it", () =>
Effect.gen(function* () {
const emptied = yield* runScenario("emptied");
const removed = yield* runScenario("removed");
const leftover = yield* runScenario("leftover");
console.log("LEFTOVER result", String(leftover.result), "a.txt", JSON.stringify(leftover.aText));
assert.equal(leftover.result._tag, "Failure");
assert.equal(leftover.aText, "<missing>");
console.log("EMPTIED result", String(emptied.result), "git", emptied.gitToplevel);
console.log("REMOVED result", String(removed.result), "git", removed.gitToplevel);
console.log("EMPTIED a.txt", JSON.stringify(emptied.aText));
console.log("REMOVED a.txt", JSON.stringify(removed.aText));
// Control: a removed worktree folder is recreated and the restore succeeds.
assert.equal(removed.result._tag, "Success");
assert.isTrue(removed.gitToplevel.endsWith(removed.worktreePath));
assert.equal(removed.aText, "checkpoint-state\n");
// Defect: an emptied worktree folder is not recreated, git is unusable there, restore fails.
assert.equal(emptied.result._tag, "Failure");
console.log("EMPTIED error", String((emptied.result as never as { failure: unknown }).failure));
assert.equal(emptied.gitToplevel, "<git failed>");
assert.equal(emptied.aText, "after-checkpoint\n");
// Safe workaround: preserve the folder by moving it aside, then let production recovery run.
const fileSystem = yield* FileSystem.FileSystem;
yield* fileSystem.rename(leftover.worktreePath, `${leftover.worktreePath}.saved`);
const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2;
const projection = state.projection!;
yield* service.execute({ threadId: projection.thread.id, providerThreadId: projection.providerThread.id,
checkpointId: projection.checkpoint.id, scopeId: projection.scope.id });
assert.equal(yield* fileSystem.readFileString(NodePath.join(leftover.worktreePath, "a.txt")), "checkpoint-state\n");
console.log("WORKAROUND production recovery restored checkpoint-state; saved folder retained");
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);
T3REPRO_EOF
OUT="$RUN_ROOT/output.txt"
if ! env -u NODE_ENV vp test run "$TEST_REL" --silent=false --project t3 >"$OUT" 2>&1; then cat "$OUT"; exit 1; fi
cat "$OUT"
grep -q 'EMPTIED CAUSE_CHAIN CheckpointRollbackExecutionError unexpected-failure CheckpointRestoreError VcsUnsupportedOperationError VcsDriverRegistry.resolve' "$OUT"
grep -q 'LEFTOVER CAUSE_CHAIN CheckpointRollbackExecutionError unexpected-failure CheckpointRestoreError VcsUnsupportedOperationError VcsDriverRegistry.resolve' "$OUT"
grep -q 'REMOVED result success' "$OUT"
grep -q 'WORKAROUND production recovery restored checkpoint-state' "$OUT"
echo 'RESULT: defect reproduced with production GitWorkflowService, GitManager, CheckpointService, CheckpointStore and VCS layers; missing-folder control and backup-then-recover workaround passed'Expected behavior
When the thread's managed worktree path has no checkout, rollback recreates the checkout from the thread's branch and then restores the ready checkpoint, whether the path is absent or an empty leftover folder. The rollback recovery block exists to recreate managed worktrees "only where the server would have made it, as turn start does", and restore safety states that rollback recreates worktrees that storage cleanup removed. In the reproduction, the empty-folder case should end like the missing-folder control: rollback succeeds and a.txt contains checkpoint-state. This report does not ask rollback to delete folders that still contain user files.
Actual behavior
Summary. When a thread's managed worktree folder still exists but its checkout is gone (the folder is empty and has no .git), checkpoint rollback with file restoration ("Edit from here" with files reverted) fails instead of recreating the checkout from the thread's preserved branch, as it already does when the whole folder is missing. The user sees "The provider could not roll back this conversation. Try again; if it keeps failing, check the provider and server logs." and every retry fails the same way. One concrete way to reach this state is a git worktree remove --force that deletes the worktree's contents and unregisters it but cannot remove the final directory; #24390 reports the same leftover state from failed removals on Windows. This is a sibling of #24390: its fix PR #24398 (and the later #34894) change only ProviderTurnStartService.ts and storageCleanup.ts, so the checkpoint rollback path keeps the same directory-existence check. It is not a duplicate of either and not a regression from them.
The leftover folder passes fileSystem.exists(worktreePath), so the recovery block never prunes or recreates the checkout. The restore then resolves a repository at that path through VcsDriverRegistry.resolve, which fails with VcsUnsupportedOperationError ("No supported VCS repository was detected at ..."). The structured cause chain is CheckpointRollbackExecutionError(reason="unexpected-failure") → CheckpointRestoreError → VcsUnsupportedOperationError(operation="VcsDriverRegistry.resolve"). In the empty-folder case a.txt stays missing; in the .git-only case it keeps after-checkpoint. The missing-folder control succeeds and restores checkpoint-state. A rerun of the script above on the tested commit passed all its assertions with these results.
The client message mapper turns this error into the generic message quoted in the Summary, and the test asserts that mapping. From source, the effect worker records checkpoint.rollback.fail after its last attempt, and the web Edit from here flow rejects the waiting action with that projected message and shows it as the thread error banner; existing focused tests for that server failure propagation (2 cases) and the client rollback-failure waiter (1 case) pass. The banner itself was not observed in a running browser or desktop app, and no mobile client was run.
Provider history: the reproduction rolls back to the thread-start checkpoint, which has no provider turns to rewind, so it exercises only file restoration. When turns do need rewinding, the service calls session.rollbackThread before restoring files, so the native provider conversation may already have been rewound when the file restore fails. That combination was not exercised, and this report makes no claim about it.
Evidence
- Expected source: the managed-worktree recreation block in
apps/server/src/orchestration-v2/CheckpointRollbackService.tslines 220-237, the recreation statement inapps/server/src/orchestration-v2/CheckpointRestoreSafety.tslines 33-45, and the passing missing-folder control in the reproduction. - Failure source: the observed reproduction output under Logs or stack traces, and the
!(yield* fileSystem.exists(worktreePath))condition atapps/server/src/orchestration-v2/CheckpointRollbackService.tsline 225, which an existing folder without.gitpasses over. - Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
The run covers the production rollback service and Git layers; the client-facing banner is established from source and focused tests only, and the failed-removal trigger was produced with POSIX directory permissions on macOS. Windows removal behavior was not tested locally; #24390 reports it.
Restoration check
With the script's input, rollback for the empty leftover folder succeeds and a.txt contains checkpoint-state, matching the missing-folder control. As controls, the missing-folder case still recreates the checkout and restores, and a managed folder that still holds user files is not deleted to make room for the checkout. The script asserts the current failing results, so its LEFTOVER expectations need updating to the restored values for this check.
Impact
Minor bug or occasional failure
Version or commit
0.1.9 at d9805b2, rerun from a clean checkout; the same script also passed on main at 5bb4452 using the tested commit's installed dependency tree (not a fresh install or full build); the recovery guard is unchanged on main at 80cccdc by source inspection only
Environment
Darwin arm64, Node v26.12.1, Git 2.56.0, repository-pinned Vite+ 1.4.0 and Vitest 5.0.5; production server services against temporary Git repositories, with no server, browser, desktop app or live provider
Logs or stack traces
The script's output, keeping only its result lines and test summary; the full error strings contain the private fixture path, shown here as [fixture]:
EMPTIED CAUSE_CHAIN CheckpointRollbackExecutionError unexpected-failure CheckpointRestoreError VcsUnsupportedOperationError VcsDriverRegistry.resolve
EMPTIED CLIENT_MESSAGE The provider could not roll back this conversation. Try again; if it keeps failing, check the provider and server logs.
NATURAL_GIT_REMOVE exit 255 permission failure left an empty unregistered folder without .git; permissions restored before rollback
LEFTOVER CAUSE_CHAIN CheckpointRollbackExecutionError unexpected-failure CheckpointRestoreError VcsUnsupportedOperationError VcsDriverRegistry.resolve
LEFTOVER CLIENT_MESSAGE The provider could not roll back this conversation. Try again; if it keeps failing, check the provider and server logs.
LEFTOVER result failure(CheckpointRollbackExecutionError: The provider could not roll back this conversation. Try again; if it keeps failing, check the provider and server logs. (cause: CheckpointRestoreError: Failed to restore checkpoint checkpoint:scope:rollback-nogit-scope-leftover:name:0 for scope rollback-nogit-scope-leftover. (cause: VcsUnsupportedOperationError: VCS operation is unsupported for unknown in VcsDriverRegistry.resolve: No supported VCS repository was detected at [fixture]/t3home/worktrees/project/leftover.))) a.txt "<missing>"
EMPTIED a.txt "after-checkpoint\n"
REMOVED result success(undefined)
REMOVED a.txt "checkpoint-state\n"
WORKAROUND production recovery restored checkpoint-state; saved folder retained
Test Files 1 passed (1)
Tests 1 passed (1)
RESULT: defect reproduced with production GitWorkflowService, GitManager, CheckpointService, CheckpointStore and VCS layers; missing-folder control and backup-then-recover workaround passed
Screenshots, recordings, or supporting files
No response
Workaround
With no active work in the thread, move the leftover worktree folder to a backup location so the thread's worktree path is absent, then retry Edit from here. The existing recovery recreates the checkout from the thread's branch and restores the checkpoint; the reproduction verifies this with the backup retained. Check the backup for residual files before deleting it, and do not create a fake .git file.
Activity