Repository navigation
Conversation
handleTasksResult aborts its controller up front when the client has already disconnected, but TaskWaiters.wait still registered the waiter and attached an abort listener that could never fire. Only a local notify() removed the entry, which never happens for tasks finishing on another instance or expiring, so the waiters leaked. TaskWaiters.wait now rejects at once and registers nothing when the signal is already aborted, and drops its abort listener when notified. awaitTaskTerminal also bails out before waiting when the signal has already aborted. Fixes #333 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
tasks/resultrequest whose client had already disconnected left an entry inTaskWaitersthat was never removed.handleTasksResultaborts its controller before waiting when the response is already destroyed.TaskWaiters.waitthen still registered the waiter and attached anabortlistener to a signal that had already fired, so the listener never ran. Only a localnotify()could remove the entry, and that never happens for a task that finishes on another instance or expires. A caller with one long-running task could grow server memory this way.Fix
TaskWaiters.wait(src/stores/task-store.ts): whensignal.abortedis already true, it rejects at once and registers nothing. It also removes its abort listener when anotify()resolves it, so a long-lived signal does not keep listeners from settled waits.awaitTaskTerminal(src/handlers.ts): callssignal.throwIfAborted()before waiting, so a client that is already gone causes no waiter, poll or store read. The existingcatchinhandleTasksResultreturns the usual "Client disconnected" or "Server shutting down" error.Both changes are needed. The first makes
TaskWaiterssafe for every caller. The second makes the handler skip the wait for a client that is already gone.Tests (
test/tasks-result-lifecycle.test.ts)wait()with an already-aborted signal rejects and leaves the waiter map empty. Before the fix it never settled and left one entry. Another unit test checks that an abort after registration still removes the entry.getis patched to destroy the server-side socket right afterhandleTasksResultreads the task, which reproduces the window where the response is already destroyed. The test then checks that no waiter is left while the task is still running. It failed before the fix with 1 waiter left on each backend.npm run lint,npm run typecheckand the fullnpm run testall pass (770/770).Fixes #333
🤖 Generated with Claude Code
https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9