diff --git a/docs/hooks/tools.mdx b/docs/hooks/tools.mdx
index b92cfb76d3..6c34783694 100644
--- a/docs/hooks/tools.mdx
+++ b/docs/hooks/tools.mdx
@@ -595,7 +595,7 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
-task (8)
+task (9)
| Env var | JSON path | Type | Description |
| ---------------------------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -603,6 +603,7 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |
| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |
| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |
+| `MUX_TOOL_INPUT_STICKY` | `sticky` | boolean | Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted. |
| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |
| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |
| `MUX_TOOL_INPUT_VARIANTS_` | `variants[]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |
diff --git a/src/common/orpc/schemas/workspace.ts b/src/common/orpc/schemas/workspace.ts
index 7f1a4326b9..7b80087795 100644
--- a/src/common/orpc/schemas/workspace.ts
+++ b/src/common/orpc/schemas/workspace.ts
@@ -111,6 +111,10 @@ export const WorkspaceMetadataSchema = z.object({
bestOf: BestOfGroupSchema.optional().meta({
description: "Grouping metadata for child tasks spawned from the same parent tool call.",
}),
+ taskSticky: z.boolean().optional().meta({
+ description:
+ "If true, this completed child task workspace is preserved instead of being auto-deleted.",
+ }),
taskStatus: z
.enum(["queued", "running", "awaiting_report", "interrupted", "reported"])
.optional()
diff --git a/src/common/schemas/project.ts b/src/common/schemas/project.ts
index fbe2d8f02d..8fb0cc8871 100644
--- a/src/common/schemas/project.ts
+++ b/src/common/schemas/project.ts
@@ -105,6 +105,10 @@ export const WorkspaceConfigSchema = z.object({
bestOf: BestOfGroupSchema.optional().meta({
description: "Grouping metadata for child tasks spawned from the same parent tool call.",
}),
+ taskSticky: z.boolean().optional().meta({
+ description:
+ "If true, this completed child task workspace is preserved instead of being auto-deleted.",
+ }),
taskStatus: z
.enum(["queued", "running", "awaiting_report", "interrupted", "reported"])
.optional()
diff --git a/src/common/utils/tools/toolDefinitions.test.ts b/src/common/utils/tools/toolDefinitions.test.ts
index 9011bb01db..11e5a5694b 100644
--- a/src/common/utils/tools/toolDefinitions.test.ts
+++ b/src/common/utils/tools/toolDefinitions.test.ts
@@ -35,6 +35,20 @@ describe("TOOL_DEFINITIONS", () => {
}
});
+ it("accepts optional sticky task tool flag", () => {
+ const parsed = TaskToolArgsSchema.safeParse({
+ subagent_type: "explore",
+ prompt: "do the thing",
+ title: "Test",
+ sticky: true,
+ });
+
+ expect(parsed.success).toBe(true);
+ if (parsed.success) {
+ expect(parsed.data.sticky).toBe(true);
+ }
+ });
+
it("accepts task tool best-of counts between 1 and 20", () => {
expect(
TaskToolArgsSchema.safeParse({
diff --git a/src/common/utils/tools/toolDefinitions.ts b/src/common/utils/tools/toolDefinitions.ts
index bd5439e5a2..6d255e8c15 100644
--- a/src/common/utils/tools/toolDefinitions.ts
+++ b/src/common/utils/tools/toolDefinitions.ts
@@ -216,7 +216,8 @@ export function buildTaskToolDescription(runtimeMode: RuntimeMode | undefined):
"Spawn a sub-agent task (child workspace). " +
"\n\nIMPORTANT: Whether a sub-agent can see uncommitted changes depends on the runtime. " +
`${getTaskRuntimeVisibilityGuidance(runtimeMode)} ` +
- "\n\nProvide agentId (preferred) or subagent_type, prompt, title, run_in_background, and optional n or variants. " +
+ "\n\nProvide agentId (preferred) or subagent_type, prompt, title, run_in_background, optional sticky, and optional n or variants. " +
+ "Do not set sticky on your own; set sticky=true only when the user explicitly asks to keep or preserve the child workspace after reporting. Sticky sub-agents are not auto-deleted. " +
`Use n when you want several agents to try the same prompt independently. Use variants when you want several agents to run the same prompt template with a different ${TASK_VARIANT_PLACEHOLDER} substituted into each run. ` +
"Examples: solve GitHub issues 45, 32, and 69 with one shared issue-solving template; investigate a regression across commit windows like A..B and B..C with one shared investigation template; or split a review into frontend/backend/tests/docs lanes with one shared review template. " +
`For variants, keep the shared template in the prompt and put the per-lane difference into ${TASK_VARIANT_PLACEHOLDER}. ` +
@@ -244,6 +245,12 @@ const TaskToolAgentArgsSchema = z
prompt: z.string().min(1),
title: z.string().min(1),
run_in_background: z.boolean().default(false),
+ sticky: z
+ .boolean()
+ .nullish()
+ .describe(
+ "Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted."
+ ),
n: TaskToolBestOfCountSchema.nullish().describe(
"Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore."
),
@@ -744,6 +751,7 @@ export const TaskListToolTaskSchema = z
createdAt: z.string().optional(),
modelString: z.string().optional(),
thinkingLevel: TaskListThinkingLevelSchema.optional(),
+ sticky: z.boolean().optional(),
depth: z.number().int().min(0),
})
.strict();
diff --git a/src/node/config.ts b/src/node/config.ts
index 25fe09a3d3..f498b3df17 100644
--- a/src/node/config.ts
+++ b/src/node/config.ts
@@ -1506,6 +1506,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
+ taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
@@ -1604,6 +1605,7 @@ export class Config {
metadata.agentType ??= workspace.agentType;
metadata.agentId ??= workspace.agentId;
metadata.bestOf ??= workspace.bestOf;
+ metadata.taskSticky ??= workspace.taskSticky;
metadata.taskStatus ??= workspace.taskStatus;
metadata.reportedAt ??= workspace.reportedAt;
metadata.taskModelString ??= workspace.taskModelString;
@@ -1633,6 +1635,7 @@ export class Config {
workspace.createdAt = metadata.createdAt;
workspace.runtimeConfig = metadata.runtimeConfig;
workspace.forkFamilyBaseName = metadata.forkFamilyBaseName;
+ workspace.taskSticky = metadata.taskSticky;
configModified = true;
if (!workspace.projects && metadata.projects) {
@@ -1672,6 +1675,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
+ taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
@@ -1722,6 +1726,7 @@ export class Config {
agentType: workspace.agentType,
agentId: workspace.agentId,
bestOf: workspace.bestOf,
+ taskSticky: workspace.taskSticky,
taskStatus: workspace.taskStatus,
reportedAt: workspace.reportedAt,
taskModelString: workspace.taskModelString,
@@ -1790,6 +1795,7 @@ export class Config {
agentType: metadata.agentType,
agentId: metadata.agentId,
bestOf: metadata.bestOf,
+ taskSticky: metadata.taskSticky,
taskStatus: metadata.taskStatus,
reportedAt: metadata.reportedAt,
taskModelString: metadata.taskModelString,
diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts
index 187e714652..e058848d17 100644
--- a/src/node/services/agentSkills/builtInSkillContent.generated.ts
+++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts
@@ -4242,7 +4242,7 @@ export const BUILTIN_SKILL_FILES: Record> = {
" ",
"",
"",
- "task (8)
",
+ "task (9)
",
"",
"| Env var | JSON path | Type | Description |",
"| ---------------------------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
@@ -4250,6 +4250,7 @@ export const BUILTIN_SKILL_FILES: Record> = {
"| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |",
"| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |",
"| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |",
+ "| `MUX_TOOL_INPUT_STICKY` | `sticky` | boolean | Only set this when the user explicitly asks to keep or preserve the child workspace after reporting; do not decide to set it on your own. When true, the completed sub-agent workspace is preserved for inspection instead of being auto-deleted. |",
"| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |",
"| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |",
"| `MUX_TOOL_INPUT_VARIANTS_` | `variants[]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |",
diff --git a/src/node/services/taskService.test.ts b/src/node/services/taskService.test.ts
index c0a9d68ca3..fc4f6f44e3 100644
--- a/src/node/services/taskService.test.ts
+++ b/src/node/services/taskService.test.ts
@@ -1746,6 +1746,29 @@ describe("TaskService", () => {
);
}, 20_000);
+ test("create persists sticky task preference on the child workspace", async () => {
+ const config = await createTestConfig(rootDir);
+ stubStableIds(config, ["aaaaaaaaaa"], "bbbbbbbbbb");
+ const { parentId } = await saveLocalParentWorkspace(config, rootDir);
+
+ const { workspaceService } = createWorkspaceServiceMocks();
+ const { taskService } = createTaskServiceHarness(config, { workspaceService });
+
+ const created = await taskService.create({
+ parentWorkspaceId: parentId,
+ kind: "agent",
+ agentType: "exec",
+ prompt: "run sticky exec task",
+ title: "Sticky task",
+ sticky: true,
+ });
+ expect(created.success).toBe(true);
+ if (!created.success) return;
+
+ const childEntry = findWorkspaceInConfig(config, created.data.taskId);
+ expect(childEntry?.taskSticky).toBe(true);
+ }, 20_000);
+
test("parent runtime AI settings outrank persisted parent workspace settings", async () => {
const config = await createTestConfig(rootDir);
stubStableIds(config, ["aaaaaaaaaa"], "bbbbbbbbbb");
@@ -8888,6 +8911,7 @@ describe("TaskService", () => {
name: string;
agentType: string;
taskStatus?: "reported" | "interrupted";
+ taskSticky?: boolean;
reportedAt?: string;
}
@@ -8960,6 +8984,7 @@ describe("TaskService", () => {
parentWorkspaceId,
agentType: task.agentType,
taskStatus: task.taskStatus ?? "reported",
+ taskSticky: task.taskSticky,
reportedAt: task.reportedAt,
});
parentWorkspaceId = task.id;
@@ -9128,6 +9153,46 @@ describe("TaskService", () => {
}
});
+ test("sticky completed descendants are never auto-cleaned", async () => {
+ const parentTaskId = "parent-222";
+ const childTaskId = "child-333";
+ const { config, remove, rootWorkspaceId, taskService, internal } =
+ await setupReportedTaskChain({
+ preserveSubagentsUntilArchive: false,
+ taskChain: [
+ {
+ id: parentTaskId,
+ directoryName: "parent-task",
+ name: "agent_exec_parent",
+ agentType: "exec",
+ taskStatus: "reported",
+ },
+ {
+ id: childTaskId,
+ directoryName: "child-task",
+ name: "agent_explore_child",
+ agentType: "explore",
+ taskStatus: "reported",
+ taskSticky: true,
+ },
+ ],
+ });
+
+ await archiveWorkspaceInTestConfig(config, rootWorkspaceId);
+
+ const cleanupEligibility = await internal.canCleanupReportedTask(childTaskId);
+ expect(cleanupEligibility).toEqual({ ok: false, reason: "sticky" });
+
+ await internal.cleanupReportedLeafTask(childTaskId);
+ await taskService.cleanupReportedDescendantsAfterArchive(rootWorkspaceId);
+
+ expect(remove).not.toHaveBeenCalled();
+ expect(findWorkspaceInConfig(config, childTaskId)).toBeTruthy();
+ expect(findWorkspaceInConfig(config, parentTaskId)).toBeTruthy();
+ expect(taskService.hasStickyCompletedDescendants(rootWorkspaceId)).toBe(true);
+ expect(taskService.hasPreservedCompletedDescendants(rootWorkspaceId)).toBe(true);
+ });
+
test("with toggle off, current cleanup behavior remains unchanged", async () => {
const { config, remove, taskChain, internal } = await setupReportedTaskChain({
preserveSubagentsUntilArchive: false,
diff --git a/src/node/services/taskService.ts b/src/node/services/taskService.ts
index bea5c35487..c08fea1df6 100644
--- a/src/node/services/taskService.ts
+++ b/src/node/services/taskService.ts
@@ -108,6 +108,8 @@ export interface TaskCreateArgs {
prompt: string;
/** Human-readable title for the task (displayed in sidebar) */
title: string;
+ /** Preserve this completed child workspace instead of auto-deleting it after report delivery. */
+ sticky?: boolean;
modelString?: string;
thinkingLevel?: ThinkingLevel;
parentRuntimeAiSettings?: { modelString?: string; thinkingLevel?: ThinkingLevel };
@@ -148,6 +150,7 @@ export interface DescendantAgentTaskInfo {
createdAt?: string;
modelString?: string;
thinkingLevel?: ThinkingLevel;
+ sticky?: boolean;
depth: number;
}
@@ -1074,6 +1077,8 @@ export class TaskService {
agentId,
agentType,
bestOf: normalizedBestOf,
+ // Sticky task workspaces remain inspectable after agent_report instead of being auto-deleted.
+ taskSticky: args.sticky === true ? true : undefined,
taskStatus: "queued",
taskPrompt: prompt,
taskTrunkBranch: trunkBranch,
@@ -1191,6 +1196,8 @@ export class TaskService {
parentWorkspaceId,
agentType,
bestOf: normalizedBestOf,
+ // Sticky task workspaces remain inspectable after agent_report instead of being auto-deleted.
+ taskSticky: args.sticky === true ? true : undefined,
taskStatus: "running",
taskTrunkBranch: trunkBranch,
taskBaseCommitSha: taskBaseCommitSha ?? undefined,
@@ -1960,6 +1967,16 @@ export class TaskService {
return this.hasActiveDescendantAgentTasks(cfg, workspaceId);
}
+ hasStickyCompletedDescendants(workspaceId: string): boolean {
+ assert(workspaceId.length > 0, "hasStickyCompletedDescendants: workspaceId must be non-empty");
+
+ const cfg = this.config.loadConfigOrDefault();
+ const index = this.buildAgentTaskIndex(cfg);
+ return this.listCompletedDescendantAgentTaskIds(index, workspaceId).some(
+ (descendantId) => index.byId.get(descendantId)?.taskSticky === true
+ );
+ }
+
hasPreservedCompletedDescendants(workspaceId: string): boolean {
assert(
workspaceId.length > 0,
@@ -1967,13 +1984,19 @@ export class TaskService {
);
const cfg = this.config.loadConfigOrDefault();
+ const index = this.buildAgentTaskIndex(cfg);
+ const completedDescendants = this.listCompletedDescendantAgentTaskIds(index, workspaceId);
+ if (
+ completedDescendants.some((descendantId) => index.byId.get(descendantId)?.taskSticky === true)
+ ) {
+ return true;
+ }
+
const taskSettings = normalizeTaskSettings(cfg.taskSettings);
if (!taskSettings.preserveSubagentsUntilArchive) {
return false;
}
- const index = this.buildAgentTaskIndex(cfg);
- const completedDescendants = this.listCompletedDescendantAgentTaskIds(index, workspaceId);
return completedDescendants.some(
(descendantId) => !this.hasArchivedAncestor(index, cfg, descendantId)
);
@@ -2058,6 +2081,7 @@ export class TaskService {
createdAt: entry.createdAt,
modelString: entry.aiSettings?.model,
thinkingLevel: entry.aiSettings?.thinkingLevel,
+ sticky: entry.taskSticky === true ? true : undefined,
depth: next.depth,
});
}
@@ -4556,6 +4580,10 @@ export class TaskService {
return { ok: false, reason: "task_not_reported" };
}
+ if (entry.workspace.taskSticky === true) {
+ return { ok: false, reason: "sticky" };
+ }
+
if (entry.workspace.bestOf?.total != null && entry.workspace.bestOf.total > 1) {
if (
await this.shouldDeferBestOfFallback({
diff --git a/src/node/services/tools/task.test.ts b/src/node/services/tools/task.test.ts
index 9f6d7c52b4..b4843189ac 100644
--- a/src/node/services/tools/task.test.ts
+++ b/src/node/services/tools/task.test.ts
@@ -98,6 +98,39 @@ describe("task tool", () => {
expectQueuedOrRunningTaskToolResult(result, { status: "queued", taskId: "child-task" });
});
+ it("passes sticky preference when spawning a task", async () => {
+ using tempDir = new TestTempDir("test-task-tool-sticky");
+ const baseConfig = createTestToolConfig(tempDir.path, { workspaceId: "parent-workspace" });
+
+ const create = mock((_args: { sticky?: boolean }) =>
+ Ok({ taskId: "child-task", kind: "agent" as const, status: "queued" as const })
+ );
+ const waitForAgentReport = mock(() => Promise.resolve({ reportMarkdown: "ignored" }));
+ const taskService = { create, waitForAgentReport } as unknown as TaskService;
+
+ const tool = createTaskTool({
+ ...baseConfig,
+ taskService,
+ });
+
+ await Promise.resolve(
+ tool.execute!(
+ {
+ agentId: "explore",
+ prompt: "do it",
+ title: "Sticky child task",
+ run_in_background: true,
+ sticky: true,
+ },
+ mockToolCallOptions
+ )
+ );
+
+ expect(create).toHaveBeenCalledTimes(1);
+ expect(create.mock.calls[0]?.[0].sticky).toBe(true);
+ expect(waitForAgentReport).not.toHaveBeenCalled();
+ });
+
it("passes parent MUX_MODEL_STRING/MUX_THINKING_LEVEL as a runtime fallback hint", async () => {
using tempDir = new TestTempDir("test-task-tool-parent-ai-env");
const baseConfig = createTestToolConfig(tempDir.path, { workspaceId: "parent-workspace" });
diff --git a/src/node/services/tools/task.ts b/src/node/services/tools/task.ts
index 18feff807b..a70a23cb60 100644
--- a/src/node/services/tools/task.ts
+++ b/src/node/services/tools/task.ts
@@ -280,7 +280,7 @@ export const createTaskTool: ToolFactory = (config: ToolConfiguration) => {
throw new Error("Interrupted");
}
- const { agentId, subagent_type, prompt, title, run_in_background, n, variants } =
+ const { agentId, subagent_type, prompt, title, run_in_background, sticky, n, variants } =
validatedArgs;
const requestedAgentId =
typeof agentId === "string" && agentId.trim().length > 0 ? agentId : subagent_type;
@@ -321,6 +321,9 @@ export const createTaskTool: ToolFactory = (config: ToolConfiguration) => {
agentType: requestedAgentId,
prompt: launch.prompt,
title,
+ // Sticky sub-agents are intentionally preserved after reporting so the
+ // agent can leave an inspectable child workspace when the task outcome warrants it.
+ ...(sticky === true ? { sticky: true } : {}),
experiments: config.experiments,
...(parentRuntimeAiSettings != null ? { parentRuntimeAiSettings } : {}),
bestOf:
diff --git a/src/node/services/tools/task_list.test.ts b/src/node/services/tools/task_list.test.ts
index 8c1a782eb5..d330bd5fa2 100644
--- a/src/node/services/tools/task_list.test.ts
+++ b/src/node/services/tools/task_list.test.ts
@@ -62,6 +62,7 @@ describe("task_list tool", () => {
createdAt: "2025-01-01T00:00:00.000Z",
modelString: "anthropic:claude-haiku-4-5",
thinkingLevel: "low",
+ sticky: true,
depth: 1,
},
]);
@@ -83,6 +84,7 @@ describe("task_list tool", () => {
createdAt: "2025-01-01T00:00:00.000Z",
modelString: "anthropic:claude-haiku-4-5",
thinkingLevel: "low",
+ sticky: true,
depth: 1,
},
],
diff --git a/src/node/services/workspaceService.test.ts b/src/node/services/workspaceService.test.ts
index 4d4084ad4e..1d1f451622 100644
--- a/src/node/services/workspaceService.test.ts
+++ b/src/node/services/workspaceService.test.ts
@@ -3269,6 +3269,38 @@ describe("WorkspaceService remove preserved descendants", () => {
await cleanupHistory();
});
+ test("remove() blocks parent removal when sticky completed descendants exist", async () => {
+ const hasStickyCompletedDescendants = mock(() => true);
+ const hasCompletedDescendants = mock(() => false);
+ workspaceService.setTaskService({
+ hasStickyCompletedDescendants,
+ hasCompletedDescendants,
+ } as unknown as TaskService);
+ const createRuntimeSpy = spyOn(runtimeFactory, "createRuntime").mockReturnValue({
+ deleteWorkspace: deleteWorkspaceMock,
+ } as unknown as ReturnType);
+
+ try {
+ const result = await workspaceService.remove(workspaceId);
+
+ expect(result).toEqual(
+ Err(
+ "This workspace has sticky completed sub-agent workspaces. Remove those sub-agents first, or force-remove the workspace."
+ )
+ );
+ expect(hasStickyCompletedDescendants).toHaveBeenCalledTimes(1);
+ expect(hasStickyCompletedDescendants).toHaveBeenCalledWith(workspaceId);
+ expect(hasCompletedDescendants).not.toHaveBeenCalled();
+ expect(stopStreamMock).not.toHaveBeenCalled();
+ expect(getWorkspaceMetadataMock).not.toHaveBeenCalled();
+ expect(createRuntimeSpy).not.toHaveBeenCalled();
+ expect(deleteWorkspaceMock).not.toHaveBeenCalled();
+ expect(removeWorkspaceMock).not.toHaveBeenCalled();
+ } finally {
+ createRuntimeSpy.mockRestore();
+ }
+ });
+
test("remove() blocks direct removal of unarchived workspace with preserved completed descendants", async () => {
const hasCompletedDescendants = mock(() => true);
workspaceService.setTaskService({
diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts
index 0bb166a244..79c8634a1f 100644
--- a/src/node/services/workspaceService.ts
+++ b/src/node/services/workspaceService.ts
@@ -2799,6 +2799,12 @@ export class WorkspaceService extends EventEmitter {
// Try to remove from runtime (filesystem)
try {
if (!force) {
+ if (this.taskService?.hasStickyCompletedDescendants?.(workspaceId)) {
+ return Err(
+ "This workspace has sticky completed sub-agent workspaces. Remove those sub-agents first, or force-remove the workspace."
+ );
+ }
+
const config = this.config.loadConfigOrDefault();
const taskSettings = normalizeTaskSettings(config.taskSettings);
if (