Fix shared workspace close semantics

Allow shared execution workspace sessions to be archived with warnings instead of hard-blocking on open linked issues, clear issue workspace links when those shared sessions are archived, and update the close dialog copy and coverage.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 19:19:49 -05:00
parent 84d4c328f5
commit caa7550e9f
4 changed files with 32 additions and 12 deletions

View file

@ -148,7 +148,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
await tempDb?.cleanup();
});
it("blocks close for shared workspaces that still have open linked issues", async () => {
it("allows archiving shared workspace sessions with warnings even when issues are still open", async () => {
const companyId = randomUUID();
const projectId = randomUUID();
const projectWorkspaceId = randomUUID();
@ -209,14 +209,15 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
expect(readiness).toMatchObject({
workspaceId: executionWorkspaceId,
state: "blocked",
state: "ready_with_warnings",
isSharedWorkspace: true,
isProjectPrimaryWorkspace: true,
isDestructiveCloseAllowed: false,
isDestructiveCloseAllowed: true,
});
expect(readiness?.blockingReasons).toEqual(expect.arrayContaining([
"This workspace is still linked to an open issue.",
"Shared execution workspaces are project infrastructure and cannot be destructively closed.",
expect(readiness?.blockingReasons).toEqual([]);
expect(readiness?.warnings).toEqual(expect.arrayContaining([
"This workspace is still linked to an open issue. Archiving it will detach this shared workspace session from those issues, but keep the underlying project workspace available.",
"This shared workspace session points at project workspace infrastructure. Archiving it only removes the session record.",
]));
});

View file

@ -1,7 +1,7 @@
import { and, eq } from "drizzle-orm";
import { Router } from "express";
import type { Db } from "@paperclipai/db";
import { projects, projectWorkspaces } from "@paperclipai/db";
import { issues, projects, projectWorkspaces } from "@paperclipai/db";
import { updateExecutionWorkspaceSchema } from "@paperclipai/shared";
import { validate } from "../middleware/validate.js";
import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js";
@ -303,6 +303,21 @@ export function executionWorkspaceRoutes(db: Db) {
}
workspace = archivedWorkspace;
if (existing.mode === "shared_workspace") {
await db
.update(issues)
.set({
executionWorkspaceId: null,
updatedAt: new Date(),
})
.where(
and(
eq(issues.companyId, existing.companyId),
eq(issues.executionWorkspaceId, existing.id),
),
);
}
try {
await stopRuntimeServicesForExecutionWorkspace({
db,

View file

@ -456,15 +456,19 @@ export function executionWorkspaceService(db: Db) {
const blockingIssues = linkedIssueSummaries.filter((issue) => !issue.isTerminal);
if (blockingIssues.length > 0) {
blockingReasons.push(
const linkedIssueMessage =
blockingIssues.length === 1
? "This workspace is still linked to an open issue."
: `This workspace is still linked to ${blockingIssues.length} open issues.`,
);
: `This workspace is still linked to ${blockingIssues.length} open issues.`;
if (isSharedWorkspace) {
warnings.push(`${linkedIssueMessage} Archiving it will detach this shared workspace session from those issues, but keep the underlying project workspace available.`);
} else {
blockingReasons.push(linkedIssueMessage);
}
}
if (isSharedWorkspace) {
blockingReasons.push("Shared execution workspaces are project infrastructure and cannot be destructively closed.");
warnings.push("This shared workspace session points at project workspace infrastructure. Archiving it only removes the session record.");
}
if (runtimeServices.some((service) => service.status !== "stopped")) {

View file

@ -118,7 +118,7 @@ export function ExecutionWorkspaceCloseDialog({
</div>
<div className="mt-1 text-xs opacity-80">
{readiness.isSharedWorkspace
? "This is the shared project workspace session, so destructive close is blocked."
? "This is a shared workspace session. Archiving it removes this session record but keeps the underlying project workspace."
: readiness.git?.workspacePath && readiness.git.repoRoot && readiness.git.workspacePath !== readiness.git.repoRoot
? "This execution workspace has its own checkout path and can be archived independently."
: readiness.isProjectPrimaryWorkspace