Skip to content

Commit e2de2ca

Browse files
kapaleshreyasclaude
andcommitted
harness: SSE keepalive every 20s to survive long-running tool calls
Long tool calls (e.g. Exa deep_search_exa with type:"deep" and numResults:50 can run 90-120s, plural in parallel) emit zero SSE events during the wait. Without any bytes on the wire, the SDK's fetch GET to /v1/sessions/:id/events idle-times-out and throws DOMException TIMEOUT_ERR on the client side — Bun fetch and most intermediaries kill sockets after ~5min of silence. Fix: send `:keepalive\n\n` (SSE comment, ignored by spec-compliant clients) every 20s while the response is open. Cleared in finally so the response can still terminate cleanly when iterate() returns. Validated live against github.com/shreyas-lyzr/exa-lead-gen-agent — agent now completes a full pipeline (3 sequential deep_search_exa calls + Write + Bash) and emits real leads as a CSV. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b31b91c commit e2de2ca

1 file changed

Lines changed: 10 additions & 0 deletions

File tree

packages/harness-server/src/routes/events.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ export function eventsRoute(ctx: ServerContext): Hono {
3636

3737
return streamSSE(c, async (stream) => {
3838
stream.onAbort(() => session.detachSubscriber());
39+
// Periodic SSE comment to keep idle connections alive during long-running
40+
// tool calls (e.g. Exa deep_search can run 90-120s emitting zero events).
41+
// The colon-prefixed line is an SSE comment per the spec — clients ignore
42+
// it but the bytes prevent intermediaries and OS-level TCP idle timeouts
43+
// from dropping the connection. ~20s cadence is safe under most defaults.
44+
const keepalive = setInterval(() => {
45+
// Best-effort. If the socket is already gone, writeln throws — swallow.
46+
void stream.writeln(":keepalive").catch(() => {});
47+
}, 20_000);
3948
try {
4049
for await (const { id: eventId, event } of session.events.iterate({ since: lastEventId })) {
4150
await stream.writeSSE({
@@ -49,6 +58,7 @@ export function eventsRoute(ctx: ServerContext): Hono {
4958
// exit with code 18 ("partial file") even on a clean run.
5059
await stream.sleep(50);
5160
} finally {
61+
clearInterval(keepalive);
5262
session.detachSubscriber();
5363
}
5464
});

0 commit comments

Comments
 (0)