From a5b482452dc8ca87625d5a80f83be0a9b56674de Mon Sep 17 00:00:00 2001 From: Dylan Corso <96969209+DylanTX@users.noreply.github.com> Date: Sat, 10 Oct 2026 06:43:39 -0500 Subject: [PATCH] fix(server): say why a thread can't be settled (#17258) Co-authored-by: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 22 ++++++++++++++----- .../src/orchestration-v2/runtimeLayer.test.ts | 5 +++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 3f17a6faa55d6538b9115dd3d376a6ea9c648ca6..94560f62f6f5ce16612764d8fd116e2d234b28de 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -2605,10 +2605,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", @@ -2616,11 +2618,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 3d4bf4c2a64a570015fe574407170cc297fa9b44..42c47bba51c02ddd7c76b921ec12bca3fdc3f83f 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -3389,6 +3389,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); @@ -3587,6 +3588,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.`, + ); }), ); -- 2.54.0