fix(test-servers): address review on the cancellation showcase

- `slow_task` sleeps through a helper that removes both listeners on
  whichever outcome wins and returns early on an already-aborted signal.
  `{ once: true }` detaches on the event firing, not on the waiter losing
  interest, so the old loop left one listener per second attached and a
  60s run tripped MaxListenersExceededWarning.
- Report the outcome on stderr. A successful cancel closes the stream the
  result would travel on, so the tool's return value is undeliverable by
  construction — the README now points at the server's terminal rather
  than at a result panel that cannot show it.
- Drop a double cast the file's own next hunk shows is unnecessary, and a
  floating observer promise whose assertion was false anyway: `postSend`
  rethrows before awaiting the SSE wait, so the send rejects on abort
  whether or not that wait was cancelled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-28 01:11:30 -04:00
commit 56d3c04643

View file

@ -1219,6 +1219,35 @@ export function createCollectUrlElicitationTool(): ToolDefinition {
};
}
/**
* Sleep `ms`, resolving `false` the moment `signal` aborts instead.
*
* Both listeners are removed on whichever outcome wins. A caller that sleeps in
* a loop would otherwise leave one attached per iteration — `{ once: true }`
* detaches a listener when the event *fires*, not when the waiter loses
* interest — and enough of them trip Node's `MaxListenersExceededWarning`.
*/
function sleepUnlessAborted(
ms: number,
signal?: AbortSignal,
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
if (signal?.aborted) {
resolve(false);
return;
}
const onAbort = (): void => {
clearTimeout(timer);
resolve(false);
};
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve(true);
}, ms);
signal?.addEventListener("abort", onAbort, { once: true });
});
}
/**
* Create a "slow_task" tool that runs until the client cancels it (#2140).
*
@ -1229,10 +1258,14 @@ export function createCollectUrlElicitationTool(): ToolDefinition {
* The point is what it does on the way out. On the 2026-07-28 era a client
* cancels a Streamable HTTP request by closing that request's response stream,
* and the SDK surfaces the disconnect to this handler as `extra.signal`. So the
* tool reports which signal it actually received: cancelled through the stream,
* the progress stops immediately; cancelled by a `notifications/cancelled` the
* server is free to ignore, the progress keeps arriving until the tool
* completes on its own — the exact symptom reported.
* tool stops the moment the stream closes; cancelled by a
* `notifications/cancelled` the server is free to ignore, it keeps emitting
* progress until it completes on its own — the exact symptom reported.
*
* Both outcomes are written to **stderr**, because that is the only channel
* they can reach. A cancellation closes the very stream this handler's result
* would travel on, so its return value is undeliverable by construction: the
* server's terminal is where you watch this, not the Inspector's result panel.
*/
export function createSlowTaskTool(): ToolDefinition {
return {
@ -1255,22 +1288,14 @@ export function createSlowTaskTool(): ToolDefinition {
) => {
const total = typeof params.seconds === "number" ? params.seconds : 60;
const progressToken = extra?._meta?.progressToken;
const signal = extra?.signal;
for (let step = 1; step <= total; step++) {
const slept = await new Promise<boolean>((resolve) => {
const timer = setTimeout(() => resolve(true), 1000);
extra?.signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
resolve(false);
},
{ once: true },
);
});
if (!slept || extra?.signal?.aborted) {
if (!(await sleepUnlessAborted(1000, signal))) {
// Cancelled. Return rather than keep working — the whole point of
// the demo is that the work actually stops.
return toToolResult(`Cancelled after ${step - 1}s`);
const message = `[slow_task] cancelled after ${step - 1}s`;
console.error(message);
return toToolResult(message);
}
if (progressToken !== undefined) {
await extra?.sendNotification?.({
@ -1279,7 +1304,9 @@ export function createSlowTaskTool(): ToolDefinition {
});
}
}
return toToolResult(`Completed all ${total}s without being cancelled`);
const message = `[slow_task] completed all ${total}s without being cancelled`;
console.error(message);
return toToolResult(message);
},
};
}