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...@@ -2605,10 +2605,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
2605 const automaticQueuedRuns = projection.runs.filter(2605 const automaticQueuedRuns = projection.runs.filter(
2606 (run) => run.status === "queued" && automaticMessageIds.has(run.userMessageId),2606 (run) => run.status === "queued" && automaticMessageIds.has(run.userMessageId),
2607 );2607 );
2608 const activeRunExists = projection.runs.some(2608 const runningRunExists = projection.runs.some((run) =>
2609 (run) =>2609 ["preparing", "starting", "running", "waiting"].includes(run.status),
2610 ["preparing", "queued", "starting", "running", "waiting"].includes(run.status) &&2610 );
2611 !automaticQueuedRuns.includes(run),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),
2612 );2614 );
2613 const pendingRequests = projection.runtimeRequests.filter(2615 const pendingRequests = projection.runtimeRequests.filter(
2614 (request) => request.status === "pending",2616 (request) => request.status === "pending",
...@@ -2616,11 +2618,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio...@@ -2616,11 +2618,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
2616 const blockingRequestExists = pendingRequests.some(2618 const blockingRequestExists = pendingRequests.some(
2617 (request) => request.kind !== "user_input" || request.responseCapability.type !== "message",2619 (request) => request.kind !== "user_input" || request.responseCapability.type !== "message",
2618 );2620 );
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) {
2620 return yield* new OrchestratorDispatchError({2630 return yield* new OrchestratorDispatchError({
2621 commandId: command.commandId,2631 commandId: command.commandId,
2622 commandType: command.type,2632 commandType: command.type,
2623 cause: `Thread ${command.threadId} has active or blocked work and cannot be settled.`,2633 cause: `Thread ${command.threadId} ${blocker}`,
2624 });2634 });
2625 }2635 }
26262636
apps/server/src/orchestration-v2/runtimeLayer.test.ts+5
...@@ -3389,6 +3389,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {...@@ -3389,6 +3389,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
3389 .pipe(Effect.flip);3389 .pipe(Effect.flip);
33903390
3391 assert.equal(error._tag, "OrchestratorDispatchError");3391 assert.equal(error._tag, "OrchestratorDispatchError");
3392 assert.equal(error.cause, `Thread ${threadId} is still running. Stop it before settling.`);
3392 const projection = yield* orchestrator.getThreadProjection(threadId);3393 const projection = yield* orchestrator.getThreadProjection(threadId);
3393 assert.equal(projection.runs[0]?.status, "starting");3394 assert.equal(projection.runs[0]?.status, "starting");
3394 assert.isNull(projection.thread.settledOverride);3395 assert.isNull(projection.thread.settledOverride);
...@@ -3587,6 +3588,10 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {...@@ -3587,6 +3588,10 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
3587 })3588 })
3588 .pipe(Effect.flip);3589 .pipe(Effect.flip);
3589 assert.equal(error._tag, "OrchestratorDispatchError");3590 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 );
3590 }),3595 }),
3591 );3596 );
35923597