anthropics / anthropics/claude-agent-sdk-typescript

interrupt() never settles and the iterator never concludes when the query is mid-API-call (0.3.241)

Aperta
#425 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
bug
Lingua principale
Shell
Stelle
1.8k
Fork
226
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

## Summary

With a streaming-input `query()` whose Messages API request is still in flight (the SSE response has started but not finished), calling `handle.interrupt()`:

- the returned promise **never settles** (neither resolves nor rejects — observed 60s+),
- the query's async iterator **never yields again and never concludes**,
- no retry request is issued.

The consumer is left permanently hung with no signal. Reproduced against **0.3.241** with both a fully stalled SSE stream and a continuously flowing one (a text delta every 500ms — same hang). In the minimal repro below the in-flight HTTP request is not even aborted.

## Environment

- `@anthropic-ai/claude-agent-sdk` **0.3.241** (vendored CLI binary 2.1.241, linux-x64)
- Node v22.14.0, Linux x86_64

## Reproduction (no credentials needed)

A local fake Messages endpoint starts a valid SSE response and holds it open, so the query is deterministically mid-API-call when `interrupt()` fires.

`fake-api.mjs`:

```js
// Minimal fake Messages API: starts a valid SSE response, sends one text
// delta, then keeps the stream open forever (an in-flight request).
import http from "node:http";
const srv = http.createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
if (!(req.method === "POST" && req.url.startsWith("/v1/messages"))) {
res.writeHead(200, { "content-type": "application/json" });
return res.end("{}");
}
res.writeHead(200, { "content-type": "text/event-stream" });
const ev = (name, data) => res.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
ev("message_start", { type: "message_start", message: { id: "msg_1", type: "message", role: "assistant", model: "claude-opus-5", content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 1 } } });
ev("content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } });
ev("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "partial" } });
console.error("[fake-api] request in flight — holding the stream open");
res.on("close", () => console.error("[fake-api] client disconnected"));
// never finishes
});
});
srv.listen(0, "127.0.0.1", () => console.log(String(srv.address().port)));
```

`repro.mjs`:

```js
import { query } from "@anthropic-ai/claude-agent-sdk";
const t0 = Date.now();
const log = (m) => console.log(`[${((Date.now() - t0) / 1000).toFixed(1)}s] ${m}`);
const handle = query({
prompt: (async function* () {
yield { type: "user", message: { role: "user", content: "hello" }, parent_tool_use_id: null };
})(),
options: { permissionMode: "bypassPermissions" },
});
setTimeout(() => {
log("calling interrupt()");
handle.interrupt().then(() => log("interrupt() RESOLVED")).catch((e) => log(`interrupt() REJECTED: ${e}`));
}, 3000);
setTimeout(() => { log("60s: interrupt() never settled, iterator never concluded — hang"); process.exit(3); }, 60000);
for await (const msg of handle) {
log(`msg: ${msg.type}`);
if (msg.type === "result") break;
}
log("iterator ended");
```

Run:

```sh
npm install @anthropic-ai/claude-agent-sdk@0.3.241
node fake-api.mjs > port.txt 2>server.log &
export ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat port.txt)"
export ANTHROPIC_API_KEY="not-a-real-key"
export CLAUDE_CONFIG_DIR="$PWD/cfg"; mkdir -p cfg
node repro.mjs
```

Observed output:

```
[1.2s] msg: system
[3.5s] calling interrupt()
[60.5s] 60s: interrupt() never settled, iterator never concluded — hang
```

## Expected

`interrupt()` settles within a bounded time, and the query either yields a result (interrupted subtype) or the iterator concludes — some signal that the interrupt was processed, even when the underlying API request is slow or stalled.

## Impact / workaround

Any consumer that awaits `interrupt()` mid-turn can hang forever on a slow or stalled upstream response. Workaround we use: race `interrupt()` against a timeout, then dispose of the query with `close()` (note the caller-facing `options.abortController` path can't help a process that exits right after — its ~2s grace window never runs).

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.