Skip to content

Commit f844ef9

Browse files
t3dotggclaude
andcommitted
fix: tell a missing background command apart from an outage
The wait_for_background_commands tool reported every dispatch failure as "no background command running". The orchestrator now rejects that case with its own error tag, and other failures stay retryable. Tool labels are neutral, since the same tool also stops waiting, and the work-log summary reads the wait input. Also fixes a test literal that failed typecheck. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 46d5190 commit f844ef9

6 files changed

Lines changed: 50 additions & 29 deletions

File tree

‎apps/server/src/mcp/toolkits/thread/handlers.ts‎

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -272,25 +272,31 @@ export const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
272272
queuedRunId: input.queuedRunId,
273273
targetRunId: input.targetRunId,
274274
})),
275-
wait_for_background_commands: (input) => {
276-
const held = input.wait ?? true;
277-
return dispatch(undefined, (common) => ({
278-
...common,
279-
type: "thread.background-work.hold",
280-
held,
281-
})).pipe(
282-
// The orchestrator rejects a hold when no background command runs.
283-
Effect.mapError((error) =>
284-
held && error.code === "orchestration_error"
285-
? new OrchestratorMcpFailure({
286-
code: "invalid_request",
287-
message:
288-
"Could not wait: this thread has no background command running. Start the command in the background first.",
289-
})
290-
: error,
291-
),
292-
);
293-
},
275+
wait_for_background_commands: (input) =>
276+
Effect.gen(function* () {
277+
const { threads, projection } = yield* readWritableThread();
278+
const result = yield* threads
279+
.dispatch({
280+
type: "thread.background-work.hold",
281+
commandId: yield* newCommandId(),
282+
threadId: projection.thread.id,
283+
held: input.wait ?? true,
284+
})
285+
.pipe(
286+
// The orchestrator rejects a hold before commit only when no
287+
// background command runs. Anything else stays retryable.
288+
Effect.mapError((error) =>
289+
error._tag === "OrchestratorCommandRejectedError"
290+
? new OrchestratorMcpFailure({
291+
code: "invalid_request",
292+
message:
293+
"Could not wait: this thread has no background command running. Start the command in the background first.",
294+
})
295+
: unavailable(),
296+
),
297+
);
298+
return { sequence: result.sequence };
299+
}),
294300
t3_thread_organize: (input) =>
295301
Effect.gen(function* () {
296302
const { threads, projection } = yield* readWritableThread(input.threadId);

‎apps/server/src/orchestration-v2/Orchestrator.backgroundWorkHold.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ it.layer(testLayer)("thread.background-work.hold", (it) => {
7474
creationSource: "web",
7575
});
7676

77-
// Nothing runs yet, so there is nothing to wait for.
78-
assert.equal((yield* Effect.exit(hold("hold-nothing", true)))._tag, "Failure");
77+
// Nothing runs yet, so there is nothing to wait for. The MCP tool relies
78+
// on this tag to tell the rejection apart from an outage.
79+
const nothing = yield* Effect.flip(hold("hold-nothing", true));
80+
assert.equal(nothing._tag, "OrchestratorCommandRejectedError");
7981

8082
// The turn ended and left its benchmark running in the background.
8183
yield* projections.apply({
@@ -130,7 +132,11 @@ it.layer(testLayer)("thread.background-work.hold", (it) => {
130132
output: "",
131133
},
132134
});
133-
const running = { taskId: "item:bench", description: "Run benchmarks", kind: "command" };
135+
const running = {
136+
taskId: "item:bench",
137+
description: "Run benchmarks",
138+
kind: "command" as const,
139+
};
134140
assert.deepEqual((yield* projections.getThreadShell(threadId))?.pendingBackgroundTasks, [
135141
running,
136142
]);

‎apps/server/src/orchestration-v2/Orchestrator.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7851,8 +7851,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
78517851
activeProviderThreadId: thread.activeProviderThreadId,
78527852
runs: projection.runs,
78537853
}).flatMap((task) => (task.kind === "command" ? [task.taskId] : []));
7854+
// Its own tag, so the MCP tool can tell this apart from an outage.
78547855
if (command.held && commandTaskIds.length === 0) {
7855-
return yield* new OrchestratorDispatchError({
7856+
return yield* new OrchestratorCommandRejectedError({
78567857
commandId: command.commandId,
78577858
commandType: command.type,
78587859
cause: `Thread ${command.threadId} has no background commands running.`,

‎packages/client-runtime/src/t3ToolSummary.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ describe("summarizeT3ToolCalls", () => {
2828
).toBe("Created 1 thread");
2929
});
3030

31+
it("names a release as the opposite of a wait for background commands", () => {
32+
expect(summarizeT3ToolCalls("background-wait", [completed({})]).label).toBe(
33+
"Set the thread to wait for background commands",
34+
);
35+
expect(summarizeT3ToolCalls("background-wait", [completed({ wait: false })]).label).toBe(
36+
"Stopped waiting for background commands",
37+
);
38+
});
39+
3140
it.each([
3241
["queue-read", "Read 1 queued message"],
3342
["queue-edit", "Edited 1 queued message"],

‎packages/client-runtime/src/t3ToolSummary.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,9 @@ export function summarizeT3ToolCalls(
228228
label = phrase("Organized", "organize", `threads ${times}`);
229229
break;
230230
case "background-wait":
231-
label = phrase(
232-
"Set the thread to wait for",
233-
"set the thread to wait for",
234-
"background commands",
235-
);
231+
label = selected.every((call) => call.input?.wait === false)
232+
? phrase("Stopped waiting for", "stop waiting for", "background commands")
233+
: phrase("Set the thread to wait for", "set the thread to wait for", "background commands");
236234
break;
237235
case "thread-update":
238236
label = phrase("Updated", "update", quantity(countEntities(threadIds), "thread"));

‎packages/shared/src/t3McpToolPresentation.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,9 @@ const T3_MCP_TOOLS: Readonly<Record<string, T3McpToolDefinition>> = {
257257
t3_thread_search: tool(["Search", "Searching", "Searched", "thread content"], "thread-search"),
258258
t3_thread_transfers: tool(["Read", "Reading", "Read", "thread transfers"], "thread-transfers"),
259259
t3_thread_organize: tool(["Organize", "Organizing", "Organized", "a thread"], "thread-organize"),
260+
// One tool both waits and stops waiting (wait: false), so the labels stay neutral.
260261
wait_for_background_commands: tool(
261-
["Wait for", "Waiting for", "Waiting for", "background commands"],
262+
["Update", "Updating", "Updated", "background command waiting"],
262263
"background-wait",
263264
),
264265
t3_thread_update: tool(["Update", "Updating", "Updated", "T3 thread metadata"], "thread-update"),

0 commit comments

Comments
 (0)