diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 1d7f907a5196..016d6f2cf222 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -2602,10 +2602,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio const automaticQueuedRuns = projection.runs.filter( (run) => run.status === "queued" && automaticMessageIds.has(run.userMessageId), ); - const activeRunExists = projection.runs.some( - (run) => - ["preparing", "queued", "starting", "running", "waiting"].includes(run.status) && - !automaticQueuedRuns.includes(run), + const runningRunExists = projection.runs.some((run) => + ["preparing", "starting", "running", "waiting"].includes(run.status), + ); + // Includes a queue held after a Stop, which otherwise shows no activity. + const queuedMessageExists = projection.runs.some( + (run) => run.status === "queued" && !automaticQueuedRuns.includes(run), ); const pendingRequests = projection.runtimeRequests.filter( (request) => request.status === "pending", @@ -2613,11 +2615,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio const blockingRequestExists = pendingRequests.some( (request) => request.kind !== "user_input" || request.responseCapability.type !== "message", ); - if (activeRunExists || blockingRequestExists) { + // The reason is shown to the user, so it names what to clear first. + const blocker = blockingRequestExists + ? "is waiting on an approval or question. Answer it before settling." + : runningRunExists + ? "is still running. Stop it before settling." + : queuedMessageExists + ? "has a queued message. Send it or remove it from the queue before settling." + : null; + if (blocker !== null) { return yield* new OrchestratorDispatchError({ commandId: command.commandId, commandType: command.type, - cause: `Thread ${command.threadId} has active or blocked work and cannot be settled.`, + cause: `Thread ${command.threadId} ${blocker}`, }); } diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 79411e6fcc55..3290b36c95b4 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -3365,6 +3365,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { .pipe(Effect.flip); assert.equal(error._tag, "OrchestratorDispatchError"); + assert.equal(error.cause, `Thread ${threadId} is still running. Stop it before settling.`); const projection = yield* orchestrator.getThreadProjection(threadId); assert.equal(projection.runs[0]?.status, "starting"); assert.isNull(projection.thread.settledOverride); @@ -3563,6 +3564,10 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { }) .pipe(Effect.flip); assert.equal(error._tag, "OrchestratorDispatchError"); + assert.equal( + error.cause, + `Thread ${threadId} has a queued message. Send it or remove it from the queue before settling.`, + ); }), );