Merge pull request #2487 from jeremymcs/fix/isolation-mode-downgrade

fix(gsd): downgrade isolation mode when worktree creation fails
This commit is contained in:
TÂCHES 2026-03-25 21:46:38 -06:00 committed by GitHub
commit a53d021864
3 changed files with 103 additions and 0 deletions

View file

@ -118,6 +118,10 @@ export class AutoSession {
// ── Sidecar queue ─────────────────────────────────────────────────────
sidecarQueue: SidecarItem[] = [];
// ── Isolation degradation ────────────────────────────────────────────
/** Set to true when worktree creation fails; prevents merge of nonexistent branch. */
isolationDegraded = false;
// ── Dispatch circuit breakers ──────────────────────────────────────
rewriteAttemptCount = 0;
@ -200,6 +204,7 @@ export class AutoSession {
this.pendingQuickTasks = [];
this.sidecarQueue = [];
this.rewriteAttemptCount = 0;
this.isolationDegraded = false;
// Signal handler
this.sigtermHandler = null;

View file

@ -846,3 +846,70 @@ test("GitService is rebuilt with originalBasePath after exitMilestone", () => {
assert.equal(gitServiceBasePath, "/project"); // project root, not worktree
});
// ─── Isolation Degradation Tests (#2483) ──────────────────────────────────
test("enterMilestone sets isolationDegraded when worktree creation throws (#2483)", () => {
const s = makeSession();
const deps = makeDeps({
getAutoWorktreePath: () => null,
createAutoWorktree: () => {
throw new Error("empty repo");
},
});
const ctx = makeNotifyCtx();
const resolver = new WorktreeResolver(s, deps);
resolver.enterMilestone("M001", ctx);
assert.equal(s.isolationDegraded, true);
assert.equal(s.basePath, "/project"); // unchanged — error recovery
});
test("enterMilestone is no-op when isolationDegraded is true (#2483)", () => {
const s = makeSession();
s.isolationDegraded = true;
const deps = makeDeps();
const ctx = makeNotifyCtx();
const resolver = new WorktreeResolver(s, deps);
resolver.enterMilestone("M001", ctx);
assert.equal(s.basePath, "/project"); // unchanged
assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
assert.equal(findCalls(deps.calls, "shouldUseWorktreeIsolation").length, 0);
});
test("mergeAndExit is no-op when isolationDegraded is true (#2483)", () => {
const s = makeSession({
basePath: "/project",
originalBasePath: "/project",
});
s.isolationDegraded = true;
const deps = makeDeps({
getIsolationMode: () => "worktree",
});
const ctx = makeNotifyCtx();
const resolver = new WorktreeResolver(s, deps);
resolver.mergeAndExit("M001", ctx);
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
assert.equal(findCalls(deps.calls, "getIsolationMode").length, 0);
assert.ok(
ctx.messages.some(
(m) => m.level === "info" && m.msg.includes("isolation was degraded"),
),
);
});
test("isolationDegraded is reset by session.reset() (#2483)", () => {
const s = new AutoSession();
s.isolationDegraded = true;
s.reset();
assert.equal(s.isolationDegraded, false);
});

View file

@ -150,6 +150,18 @@ export class WorktreeResolver {
*/
enterMilestone(milestoneId: string, ctx: NotifyCtx): void {
this.validateMilestoneId(milestoneId);
// If worktree creation failed earlier this session, skip all future attempts
if (this.s.isolationDegraded) {
debugLog("WorktreeResolver", {
action: "enterMilestone",
milestoneId,
skipped: true,
reason: "isolation-degraded",
});
return;
}
if (!this.deps.shouldUseWorktreeIsolation()) {
debugLog("WorktreeResolver", {
action: "enterMilestone",
@ -220,6 +232,9 @@ export class WorktreeResolver {
`Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`,
"warning",
);
// Degrade isolation for the rest of this session so mergeAndExit
// doesn't try to merge a nonexistent worktree branch (#2483)
this.s.isolationDegraded = true;
// Do NOT update s.basePath — stay in project root
}
}
@ -304,6 +319,22 @@ export class WorktreeResolver {
*/
mergeAndExit(milestoneId: string, ctx: NotifyCtx): void {
this.validateMilestoneId(milestoneId);
// If worktree creation failed earlier, skip merge — work is on current branch (#2483)
if (this.s.isolationDegraded) {
debugLog("WorktreeResolver", {
action: "mergeAndExit",
milestoneId,
skipped: true,
reason: "isolation-degraded",
});
ctx.notify(
`Skipping worktree merge for ${milestoneId} — isolation was degraded (worktree creation failed earlier). Work is on the current branch.`,
"info",
);
return;
}
const mode = this.deps.getIsolationMode();
debugLog("WorktreeResolver", {
action: "mergeAndExit",