authorgravatar for 96969209+DylanTX@users.noreply.github.comDylan Corso <96969209+DylanTX@users.noreply.github.com> 2026-10-10 06:43:39-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 06:43:39-05:00
loga5b482452dc8ca87625d5a80f83be0a9b56674de
treecd41202f89bdb02eef2634afb0b42b88f5443514
parenta418075154d1d4f4263d5fa55804b9288f61d3dd
signature Signed by PGP key B5690EEEBB952194

fix(server): say why a thread can't be settled (#17258)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 21 insertions(+), 6 deletions(-)

apps/server/src/orchestration-v2/Orchestrator.ts+16-6
......@@ -2605,10 +2605,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
26052605 const automaticQueuedRuns = projection.runs.filter(
26062606 (run) => run.status === "queued" && automaticMessageIds.has(run.userMessageId),
26072607 );
2608 const activeRunExists = projection.runs.some(
2609 (run) =>
2610 ["preparing", "queued", "starting", "running", "waiting"].includes(run.status) &&
2611 !automaticQueuedRuns.includes(run),
2608 const runningRunExists = projection.runs.some((run) =>
2609 ["preparing", "starting", "running", "waiting"].includes(run.status),
2610 );
2611 // Includes a queue held after a Stop, which otherwise shows no activity.
2612 const queuedMessageExists = projection.runs.some(
2613 (run) => run.status === "queued" && !automaticQueuedRuns.includes(run),
26122614 );
26132615 const pendingRequests = projection.runtimeRequests.filter(
26142616 (request) => request.status === "pending",
......@@ -2616,11 +2618,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
26162618 const blockingRequestExists = pendingRequests.some(
26172619 (request) => request.kind !== "user_input" || request.responseCapability.type !== "message",
26182620 );
2619 if (activeRunExists || blockingRequestExists) {
2621 // The reason is shown to the user, so it names what to clear first.
2622 const blocker = blockingRequestExists
2623 ? "is waiting on an approval or question. Answer it before settling."
2624 : runningRunExists
2625 ? "is still running. Stop it before settling."
2626 : queuedMessageExists
2627 ? "has a queued message. Send it or remove it from the queue before settling."
2628 : null;
2629 if (blocker !== null) {
26202630 return yield* new OrchestratorDispatchError({
26212631 commandId: command.commandId,
26222632 commandType: command.type,
2623 cause: `Thread ${command.threadId} has active or blocked work and cannot be settled.`,
2633 cause: `Thread ${command.threadId} ${blocker}`,
26242634 });
26252635 }
26262636
apps/server/src/orchestration-v2/runtimeLayer.test.ts+5
......@@ -3389,6 +3389,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
33893389 .pipe(Effect.flip);
33903390
33913391 assert.equal(error._tag, "OrchestratorDispatchError");
3392 assert.equal(error.cause, `Thread ${threadId} is still running. Stop it before settling.`);
33923393 const projection = yield* orchestrator.getThreadProjection(threadId);
33933394 assert.equal(projection.runs[0]?.status, "starting");
33943395 assert.isNull(projection.thread.settledOverride);
......@@ -3587,6 +3588,10 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
35873588 })
35883589 .pipe(Effect.flip);
35893590 assert.equal(error._tag, "OrchestratorDispatchError");
3591 assert.equal(
3592 error.cause,
3593 `Thread ${threadId} has a queued message. Send it or remove it from the queue before settling.`,
3594 );
35903595 }),
35913596 );
35923597