From e92cff7bfeac81b785122a15a0961dd8e79569f9 Mon Sep 17 00:00:00 2001 From: Waseem Ilyas <1478353+Waseemilyas@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:48:30 +0000 Subject: [PATCH] fix(server): skip execution-workspace inheritance from a terminal parent issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Creating a child issue inherits the parent's execution workspace and forces executionWorkspacePreference to reuse_existing > - The inheritance source was never checked for status, so a child of a cancelled or done issue bound itself to a worktree that was already torn down or carried abandoned state > - This pull request reads the source issue status and skips only the execution-workspace hand-down for terminal sources; project and project-workspace grouping still inherit > - The benefit is that a child of a finished issue gets a fresh workspace instead of inheriting a dead one ## Linked Issues or Issue Description Refs #11464 — covers the terminal-status shape; the implicit live-parent sharing noted in the issue comments is left as designed (children share the live parent's worktree deliberately). ## What Changed - server/src/services/issues.ts: getWorkspaceInheritanceIssue now reads the source issue status, and the create path skips execution-workspace inheritance when that status is done or cancelled. The project and projectWorkspaceId hand-downs are unchanged. - server/src/__tests__/issues-service.test.ts: a cancelled parent no longer passes its executionWorkspaceId, preference, or settings to a child created through parentId, while projectWorkspaceId still inherits. ## Verification - pnpm --filter @paperclipai/server vitest run src/__tests__/issues-service.test.ts — 128 tests pass (embedded Postgres). ## Risks - Low risk. Only the terminal-source case changes; live parents behave exactly as before, and an explicit executionWorkspaceId or inheritExecutionWorkspaceFromIssueId caller override is untouched. ## Model Used - Anthropic Claude — SWE-2 Max agent via Devin CLI, tool use and code execution. Co-authored-by: Paperclip --- server/src/__tests__/issues-service.test.ts | 69 +++++++++++++++++++++ server/src/services/issues.ts | 10 ++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 3b91c8856c..3d59936287 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3683,6 +3683,75 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { }); }); + it("does not inherit the execution workspace from a cancelled parent issue", async () => { + const companyId = randomUUID(); + const projectId = randomUUID(); + const parentIssueId = randomUUID(); + const projectWorkspaceId = randomUUID(); + const executionWorkspaceId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true }); + + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Workspace project", + status: "in_progress", + }); + + await db.insert(projectWorkspaces).values({ + id: projectWorkspaceId, + companyId, + projectId, + name: "Primary workspace", + }); + + await db.insert(executionWorkspaces).values({ + id: executionWorkspaceId, + companyId, + projectId, + projectWorkspaceId, + mode: "isolated_workspace", + strategyType: "git_worktree", + name: "Cancelled parent worktree", + status: "archived", + providerType: "git_worktree", + closedAt: new Date(), + }); + + await db.insert(issues).values({ + id: parentIssueId, + companyId, + projectId, + projectWorkspaceId, + title: "Cancelled parent issue", + status: "cancelled", + priority: "medium", + executionWorkspaceId, + executionWorkspacePreference: "reuse_existing", + executionWorkspaceSettings: { + mode: "isolated_workspace", + }, + }); + + const child = await svc.create(companyId, { + parentId: parentIssueId, + projectId, + title: "Child issue", + }); + + // Grouping still inherits; the dead worktree does not. + expect(child.projectWorkspaceId).toBe(projectWorkspaceId); + expect(child.executionWorkspaceId).toBeNull(); + expect(child.executionWorkspacePreference).toBeNull(); + }); + it("createChild applies parent defaults, acceptance criteria, workspace inheritance, and optional parent blocker chaining", async () => { const companyId = randomUUID(); const projectId = randomUUID(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index e51af10035..3ac15c74e0 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -2678,6 +2678,7 @@ async function getWorkspaceInheritanceIssue( projectWorkspaceId: issues.projectWorkspaceId, executionWorkspaceId: issues.executionWorkspaceId, executionWorkspaceSettings: issues.executionWorkspaceSettings, + status: issues.status, }) .from(issues) .where(and(eq(issues.id, issueId), eq(issues.companyId, companyId))) @@ -9954,11 +9955,18 @@ export function issueService(db: Db) { ) { projectWorkspaceId = workspaceSource.projectWorkspaceId; } + // A terminal source issue (done or cancelled) must not pass its + // execution workspace down: the worktree is already torn down or + // carries abandoned state, and `reuse_existing` would bind the new + // issue to it. Grouping fields still inherit — the child remains + // topically part of the same project — only the execution + // worktree falls back to a fresh one. if ( inheritsSourceProject && isolatedWorkspacesEnabled && !hasExplicitExecutionWorkspaceOverride && - workspaceSource.executionWorkspaceId + workspaceSource.executionWorkspaceId && + !["done", "cancelled"].includes(workspaceSource.status) ) { const sourceWorkspace = await tx .select({