Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agents/system-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ When the user asks for "best of n" work, assume they want the \`task\` tool's \`
Before spawning the batch, do a small amount of preliminary analysis to capture shared context, constraints, or evaluation criteria that would otherwise be repeated by every child.
Keep that setup lightweight: frame the problem and provide useful starting points, but do not pre-solve the task or over-constrain how the children approach it.
Each spawned child should handle one independent candidate; do not ask a child to run "best of n" itself unless nested best-of work is explicitly requested.
Picking the best candidate requires every report, so await the full batch (pass \`task_await\` \`min_completed\` equal to the batch size, or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
Picking the best candidate requires every report, so await the full batch with \`task_await({ task_ids: result.taskIds, min_completed: result.taskIds.length })\` (or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
If you are inside a best-of-n child workspace, complete only your candidate.
</best-of-n>

Expand Down
42 changes: 21 additions & 21 deletions docs/hooks/tools.mdx

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/cli/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ function createWorkflowService(input: {
return new WorkflowService({
runStore: new WorkflowRunStore({ sessionDir: workspaceSessionDir }),
runtimeFactory: new QuickJSRuntimeFactory(),
withRunStartLock: (ownerWorkspaceId, operation) =>
input.ctx.services.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId, operation),
taskAdapterFactory: (runId) =>
new WorkflowTaskServiceAdapter({
taskService: input.ctx.services.taskService,
Expand All @@ -445,6 +447,8 @@ function createWorkflowService(input: {
experiments,
modelString: input.model,
thinkingLevel: input.thinkingLevel,
cleanupWorkspaceBackgroundProcesses: (taskWorkspaceId) =>
input.ctx.services.backgroundProcessManager.cleanup(taskWorkspaceId),
getProjectTrusted: () => input.ctx.projectTrusted,
patchToolConfig: {
workspaceId: input.ctx.workspaceId,
Expand Down
45 changes: 23 additions & 22 deletions src/common/utils/tools/toolDefinitions.ts

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions src/common/utils/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,14 @@ export interface ToolConfiguration {
run: unknown;
}) => Promise<void> | void;
}): Promise<{ runId: string; status: string; result: unknown }>;
interruptRun?(input: { workspaceId: string; runId: string }): Promise<unknown>;
interruptRun?(input: {
workspaceId: string;
runId: string;
deferTaskSweep?: boolean;
lockAlreadyHeld?: boolean;
retryTaskCleanup?: boolean;
onRunInterrupted?: (runId: string) => void;
}): Promise<unknown>;
resumeRun?(input: {
workspaceId: string;
runId: string;
Expand Down Expand Up @@ -765,7 +772,7 @@ export async function getToolsForModel(
task_remove: wrap(createTaskRemoveTool(config)),
task_list: wrap(createTaskListTool(config)),

// Bash execution (foreground/background). Manage background output via task_await/task_list/task_terminate.
// Bash execution (foreground/background). Manage background output via task_await/task_list/task_stop.
bash: wrap(createBashTool(config)),

// Legacy bash process tools (deprecated)
Expand Down
2 changes: 1 addition & 1 deletion src/node/builtinSkills/orchestrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ In a workflow, the verifier becomes `agent(prompt, { id, schema, onRefusal: "fai
## Sequential protocol (only for dependency chains)

1. Spawn the prerequisite `exec` implementation task with `run_in_background: false`.
2. If step 1 returns `queued`/`running` without a completed report, call `task_await` with the returned `taskId` before attempting any patch apply. If step 1 returns `status: completed` inline, that same `taskId` still requires patch application.
2. If step 1 returns `queued`/`running` without a completed report, call `task_await({ task_ids: [result.taskId] })` before attempting any patch apply. If step 1 returns `status: completed` inline, that same `taskId` still requires patch application.
3. Dry-run apply its patch (`dry_run: true`); then apply for real (`dry_run: false`). If either step fails, follow the conflict playbook above (including `git am --abort` only when a real apply leaves a git-am session in progress).
4. Only then spawn the dependent task.

Expand Down
4 changes: 4 additions & 0 deletions src/node/orpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@
options.notifyInterruptedBackgroundRunTerminal === true,
runStore: new WorkflowRunStore({ sessionDir: context.config.getSessionDir(workspaceId) }),
runtimeFactory: context.workflowRuntimeFactory,
withRunStartLock: (ownerWorkspaceId, operation) =>
context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId, operation),

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:660:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:659:23) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2111:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:639:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:638:35) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2105:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:639:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:638:35) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2105:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:639:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:638:35) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2105:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:639:35) at startWorkflowInBackground (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:638:35) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2105:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:660:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:659:23) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2111:33)

Check failure on line 461 in src/node/orpc/router.ts

View workflow job for this annotation

GitHub Actions / Test / Unit

TypeError: context.taskService.withWorkspaceOwnedWorkStartLock is not a function. (In 'context.taskService.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId

at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:461:29) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:917:20) at createWorkflowRun (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:895:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:660:35) at startWorkflow (/home/runner/work/mux/mux/src/node/services/workflows/WorkflowService.ts:659:23) at <anonymous> (/home/runner/work/mux/mux/src/node/orpc/router.ts:2111:33)
taskAdapterFactory: (runId, workflowName) =>
new WorkflowTaskServiceAdapter({
taskService: context.taskService,
Expand All @@ -472,6 +474,8 @@
workspaceSessionDir: context.config.getSessionDir(workspaceId),
trusted: projectTrusted,
},
cleanupWorkspaceBackgroundProcesses: (taskWorkspaceId) =>
context.aiService.cleanupWorkspaceBackgroundProcesses(taskWorkspaceId),
getProjectTrusted: resolveWorkflowProjectTrusted,
experiments: {
dynamicWorkflows: true,
Expand Down
46 changes: 23 additions & 23 deletions src/node/services/agentSkills/builtInSkillContent.generated.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ export class AIService extends EventEmitter {
private analyticsService?: { executeRawQuery(sql: string): Promise<unknown> };
private desktopSessionManager?: DesktopSessionManager;

async cleanupWorkspaceBackgroundProcesses(workspaceId: string): Promise<void> {
await this.backgroundProcessManager?.cleanup(workspaceId);
}

constructor(
config: Config,
historyService: HistoryService,
Expand Down Expand Up @@ -2202,6 +2206,8 @@ export class AIService extends EventEmitter {
await this.onWorkflowRunStatusChanged?.(event);
},
runtimeFactory: new QuickJSRuntimeFactory(),
withRunStartLock: (ownerWorkspaceId, operation) =>
this.taskService!.withWorkspaceOwnedWorkStartLock(ownerWorkspaceId, operation),
taskAdapterFactory: (runId, workflowName) =>
new WorkflowTaskServiceAdapter({
taskService: this.taskService!,
Expand All @@ -2217,6 +2223,8 @@ export class AIService extends EventEmitter {
workspaceSessionDir: this.config.getSessionDir(workspaceId),
trusted: getWorkflowProjectTrusted(),
},
cleanupWorkspaceBackgroundProcesses: (taskWorkspaceId) =>
this.cleanupWorkspaceBackgroundProcesses(taskWorkspaceId),
getProjectTrusted: getWorkflowProjectTrusted,
experiments: {
...experiments,
Expand Down
2 changes: 1 addition & 1 deletion src/node/services/systemMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ When the user asks for "best of n" work, assume they want the \`task\` tool's \`
Before spawning the batch, do a small amount of preliminary analysis to capture shared context, constraints, or evaluation criteria that would otherwise be repeated by every child.
Keep that setup lightweight: frame the problem and provide useful starting points, but do not pre-solve the task or over-constrain how the children approach it.
Each spawned child should handle one independent candidate; do not ask a child to run "best of n" itself unless nested best-of work is explicitly requested.
Picking the best candidate requires every report, so await the full batch (pass \`task_await\` \`min_completed\` equal to the batch size, or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
Picking the best candidate requires every report, so await the full batch with \`task_await({ task_ids: result.taskIds, min_completed: result.taskIds.length })\` (or use a foreground grouped spawn) before selecting — but you may start setup-only work (e.g. preparing the evaluation rubric or integration scaffolding) as soon as the first candidate lands.
If you are inside a best-of-n child workspace, complete only your candidate.
</best-of-n>

Expand Down
153 changes: 144 additions & 9 deletions src/node/services/taskService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,18 @@ describe("TaskService", () => {
expect(aiMocks.stopStream).not.toHaveBeenCalled();
});

test("interruptWorkspaceTurn is idempotent after interruption", async () => {
const { parentId, taskService, aiMocks } = await startWorkspaceTurnForTest();

expect(await taskService.interruptWorkspaceTurn(parentId, "wst_handle")).toEqual(
Ok({ workspaceId: "childworkspace" })
);
expect(await taskService.interruptWorkspaceTurn(parentId, "wst_handle")).toEqual(
Ok({ workspaceId: "childworkspace", alreadyInactive: true })
);
expect(aiMocks.stopStream).toHaveBeenCalledTimes(1);
});

test("createWorkspaceTurn reserves a slot before queueing a manually busy existing workspace", async () => {
const config = await createTestConfig(rootDir);
stubStableIds(config, ["queuedhandle", "queuedturn"]);
Expand Down Expand Up @@ -5300,7 +5312,7 @@ describe("TaskService", () => {

test("late correlated stream-end does not resettle an explicitly interrupted workspace turn", async () => {
const { config, parentId, taskService } = await startWorkspaceTurnForTest();
// Explicit interrupt (user Esc / task_terminate): status interrupted WITHOUT the
// Explicit interrupt (user Esc / task_stop): status interrupted WITHOUT the
// stale-restart marker. An in-flight stream-end completing after the cancel must not
// make the canceled turn appear completed.
await new TaskHandleStore(config).upsertWorkspaceTurn({
Expand Down Expand Up @@ -11852,6 +11864,97 @@ describe("TaskService", () => {
expect(updateTitle).not.toHaveBeenCalled();
});

test("direct lifecycle operations reject workflow-owned descendants", async () => {
const config = await createTestConfig(rootDir);
const projectPath = path.join(rootDir, "repo");
const parentWorkspaceId = "parent-workflow-owned-lifecycle";
const workflowChildId = "workflow-owned-lifecycle-root";
const messageChildId = "workflow-owned-message-child";
const stopChildId = "workflow-owned-stop-child";
const removeChildId = "workflow-owned-remove-child";
await saveWorkspaces(
config,
projectPath,
[
projectWorkspace(projectPath, "parent", parentWorkspaceId),
projectWorkspace(projectPath, "workflow-child", workflowChildId, {
parentWorkspaceId,
taskStatus: "running",
workflowTask: { runId: "wfr_lifecycle", stepId: "step" },
}),
projectWorkspace(projectPath, "message-child", messageChildId, {
parentWorkspaceId: workflowChildId,
taskStatus: "queued",
taskPrompt: "Original workflow-owned assignment",
}),
projectWorkspace(projectPath, "stop-child", stopChildId, {
parentWorkspaceId: workflowChildId,
taskStatus: "running",
}),
projectWorkspace(projectPath, "remove-child", removeChildId, {
parentWorkspaceId: workflowChildId,
taskStatus: "reported",
}),
],
testTaskSettings()
);
const { taskService } = createTaskServiceHarness(config);

expect(
await taskService.sendMessageToDescendantAgentTask(
parentWorkspaceId,
messageChildId,
"Bypass the owning workflow",
"tool-end"
)
).toEqual(Err({ code: "invalid_scope" }));
expect(await taskService.stopDescendantAgentTask(parentWorkspaceId, stopChildId)).toEqual(
Err("Task is not a descendant of this workspace")
);
expect(
await taskService.removeInactiveDescendantAgentTask(parentWorkspaceId, removeChildId)
).toMatchObject({ success: true, data: { status: "invalid_scope" } });
});

test("stopDescendantAgentTask leaves workflow-owned descendant branches to WorkflowService", async () => {
const config = await createTestConfig(rootDir);
const projectPath = path.join(rootDir, "repo");
const parentWorkspaceId = "parent-stop-workflow-branch";
const childTaskId = "user-owned-stop-root";
const workflowChildId = "workflow-owned-stop-descendant";
await saveWorkspaces(
config,
projectPath,
[
projectWorkspace(projectPath, "parent", parentWorkspaceId),
projectWorkspace(projectPath, "child", childTaskId, {
parentWorkspaceId,
taskStatus: "running",
}),
projectWorkspace(projectPath, "workflow-child", workflowChildId, {
parentWorkspaceId: childTaskId,
taskStatus: "running",
workflowTask: { runId: "wfr_stop_branch", stepId: "step" },
}),
],
testTaskSettings()
);
const stopStream = mock((): Promise<Result<void>> => Promise.resolve(Ok(undefined)));
const { aiService } = createAIServiceMocks(config, {
isStreaming: mock(() => true),
stopStream,
});
const { taskService } = createTaskServiceHarness(config, { aiService });

expect(await taskService.stopDescendantAgentTask(parentWorkspaceId, childTaskId)).toEqual(
Ok({ stoppedTaskIds: [childTaskId] })
);
expect(stopStream).toHaveBeenCalledTimes(1);
expect(stopStream).toHaveBeenCalledWith(childTaskId, { abandonPartial: false });
expect(findWorkspaceInConfig(config, childTaskId)?.taskStatus).toBe("interrupted");
expect(findWorkspaceInConfig(config, workflowChildId)?.taskStatus).toBe("running");
});

test("retitleDescendantAgentTask surfaces title update failures", async () => {
const config = await createTestConfig(rootDir);
const projectPath = path.join(rootDir, "repo");
Expand Down Expand Up @@ -12901,13 +13004,30 @@ describe("TaskService", () => {
await stopStarted;

const creation = createAgentTask(taskService, childTaskId, "Spawn after stop");
const createWorkflowRun = mock(() => Promise.resolve("created"));
const workflowCreation = taskService.withWorkspaceOwnedWorkStartLock(
childTaskId,
createWorkflowRun
);
await Promise.resolve();
expect(create).not.toHaveBeenCalled();
expect(createWorkflowRun).not.toHaveBeenCalled();

releaseStop?.();
expect(await stopping).toEqual(Ok({ stoppedTaskIds: [childTaskId] }));
expect(await creation).toEqual(Err("Task.create: cannot spawn new tasks after task_stop"));
let workflowCreationError: unknown;
try {
await workflowCreation;
} catch (error: unknown) {
workflowCreationError = error;
}
expect(workflowCreationError).toBeInstanceOf(Error);
expect((workflowCreationError as Error).message).toBe(
"Cannot start workflow work after task_stop"
);
expect(create).not.toHaveBeenCalled();
expect(createWorkflowRun).not.toHaveBeenCalled();
});

test("bulk task creation waits for task stop and rejects the interrupted parent", async () => {
Expand Down Expand Up @@ -13863,12 +13983,23 @@ describe("TaskService", () => {
const { aiService } = createAIServiceMocks(config);
const { workspaceService } = createWorkspaceServiceMocks();
const { taskService } = createTaskServiceHarness(config, { aiService, workspaceService });
const cleanupWorkspaceBackgroundProcesses = mock(
(workspaceId: string): Promise<void> =>
workspaceId === workflowChildTaskId
? Promise.reject(new Error("background cleanup failed"))
: Promise.resolve()
);

const interruptedTaskIds = await taskService.terminateAllDescendantAgentTasks(rootWorkspaceId, {
workflowRunId: "wfr_target",
cleanupWorkspaceBackgroundProcesses,
});

expect(interruptedTaskIds).toEqual([workflowChildTaskId, workflowTaskId]);
expect(cleanupWorkspaceBackgroundProcesses.mock.calls.map((call) => call[0])).toEqual([
workflowChildTaskId,
workflowTaskId,
]);
const saved = config.loadConfigOrDefault();
const tasks = saved.projects.get(projectPath)?.workspaces ?? [];
expect(tasks.find((workspace) => workspace.id === workflowTaskId)?.taskStatus).toBe(
Expand Down Expand Up @@ -14774,7 +14905,7 @@ describe("TaskService", () => {
expect(userInterrupted?.taskStatus).toBe("interrupted");
});

test("terminateAllDescendantAgentTasks archives run-scoped interrupted children immediately", async () => {
test("terminateAllDescendantAgentTasks can defer run-scoped archive sweeps", async () => {
const config = await createTestConfig(rootDir);

const projectPath = path.join(rootDir, "repo");
Expand Down Expand Up @@ -14814,14 +14945,18 @@ describe("TaskService", () => {
const { workspaceService } = createWorkspaceServiceMocks({ archive });
const { taskService } = createTaskServiceHarness(config, { aiService, workspaceService });

// Run-scoped interrupt (WorkflowService.interruptRun path): the sweep archives the
// freshly interrupted workflow child even if the runner's onRunEnded hook already
// fired before the children were interrupted.
await taskService.terminateAllDescendantAgentTasks(rootId, { workflowRunId });
// task_stop can already hold the non-reentrant tree lock while interrupting the owning run.
// Defer archive work until that outer lock releases, then run the normal idempotent sweep.
await taskService.terminateAllDescendantAgentTasks(rootId, {
workflowRunId,
deferWorkflowSweep: true,
});

const workflowChild = findWorkspaceInConfig(config, workflowChildId);
expect(workflowChild?.taskStatus).toBe("interrupted");
expect(workflowChild?.archivedAt).toBeString();
expect(findWorkspaceInConfig(config, workflowChildId)?.taskStatus).toBe("interrupted");
expect(findWorkspaceInConfig(config, workflowChildId)?.archivedAt).toBeUndefined();

await taskService.markWorkflowRunEnded(workflowRunId);
expect(findWorkspaceInConfig(config, workflowChildId)?.archivedAt).toBeString();

// The run-scoped filter leaves the user-spawned sibling running and unarchived.
const userChild = findWorkspaceInConfig(config, userChildId);
Expand Down
Loading
Loading